
为什么不能一次订阅所有合约
CTPAPI 本身没有提供“一键订阅全市场”的接口。交易所有上千个合约,行情前置机对每个连接能订阅的合约数量有硬性限制,每个连接最多 2000 个合约。你如果硬塞超过这个数字,服务器会直接断开连接,返回“订阅数量超限”的错误。
另一个限制来自流控。CTP 的订阅请求走的是请求-响应模式,一秒钟内发几百个订阅请求,柜台会认为你在攻击,直接掐断链路。所以订阅全市场行情不是写一行代码的事,得按规矩分批、限速。
先搞定登录和前置机连接
订阅行情前必须完成两件事:行情前置机连接和用户登录。行情 API 不需要交易编码,但需要账号密码。

CThostFtdcMdApi* mdApi = CThostFtdcMdApi::CreateFtdcMdApi("./flow/", false, false);
mdApi->RegisterFront("tcp://180.168.146.187:10131");
mdApi->Init();
// 等待连接成功回调 OnFrontConnected 后再登录
CThostFtdcReqUserLoginField loginReq = {0};
strcpy(loginReq.BrokerID, "9999");
strcpy(loginReq.UserID, "your_user");
strcpy(loginReq.Password, "your_pass");
mdApi->ReqUserLogin(&loginReq, 1);
登录成功回调 OnRspUserLogin 之后才能发起订阅。这一步没完成就去订阅,返回的错误码是“未登录”。
批量订阅的核心逻辑
全市场合约大概在 5000 到 10000 个之间(商品期货+金融期货+期权)。按每个连接 2000 个上限,至少需要 3 到 5 个行情连接。很多新手只用一个连接就想订阅全部,结果就是反复断线。
正确的做法是:启动多个 CThostFtdcMdApi 实例,每个实例连接同一个行情前置,分别登录同一个账号,然后把合约列表切成每份不超过 2000 个,分配给不同实例去订阅。
// 假设 allInstruments 是全部合约代码的 vector
const int batchSize = 1800; // 留点余量,别卡 2000 上限
int totalBatches = (allInstruments.size() + batchSize - 1) / batchSize;
for (int i = 0; i < totalBatches; ++i) {
CThostFtdcMdApi* api = CThostFtdcMdApi::CreateFtdcMdApi("./flow_" + std::to_string(i) + "/");
api->RegisterFront("tcp://180.168.146.187:10131");
api->Init();
// 每个 api 实例登录成功后,订阅自己负责的那一批
// 订阅时用 SubscribeMarketData 批量传入合约数组
}
每个实例的 OnRspSubMarketData 回调里检查错误,如果某个合约订阅失败(比如合约已下市),记录下来但别中断整个流程。
合约列表从哪里来
订阅全市场的前提是你得知道全市场有哪些合约。CTP 行情 API 不提供查询合约列表的接口,合约列表要从交易 API 拿。
用 CThostFtdcTraderApi 登录交易前置,调用 ReqQryInstrument 查询所有合约。返回的合约里包含交易所代码、合约代码、合约类型。把期权、期货分开处理,期权合约数量巨大,订阅时更要小心流控。
CThostFtdcQryInstrumentField req = {0};
traderApi->ReqQryInstrument(&req, 1);
OnRspQryInstrument 回调会分多次返回,每次返回一批,直到 bIsLast 为 true。把所有的 InstrumentID 收集起来,去重,然后才进入订阅环节。
订阅频率怎么控制
CTP 的流控规则是:1 秒内最多 1 个请求?那是对普通查询的误解。订阅行情的流控比查询松一些,但也不是无限。实测下来,每秒发 50 到 100 个订阅请求比较安全。每个订阅请求可以带多个合约,所以不用一个合约发一次。
// 每次订阅请求带 100 个合约
const int subBatch = 100;
for (int i = 0; i < batch.size(); i += subBatch) {
int count = std::min(subBatch, (int)batch.size() - i);
mdApi->SubscribeMarketData(&batch[i], count);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
每批之间 sleep 100 毫秒,相当于每秒 10 批,每批 100 个合约,一秒 1000 个合约。这个速度大多数柜台能扛住。如果柜台返回“流控”错误,把 sleep 时间翻倍,再试。
断线重连后的订阅恢复
行情连接不是永久的。网络抖动、柜台重启都会导致连接断开。OnFrontDisconnected 回调触发后,你得重新连接、重新登录、重新订阅。
关键点:重连后不要重新查询合约列表,直接用内存里缓存的合约列表重新订阅。重新订阅时同样要分批、限速。
void OnFrontDisconnected(int nReason) {
// 触发重连逻辑
ReconnectAndResubscribe();
}
重连和首次订阅的逻辑可以复用,唯一区别是合约列表已经存在内存里,跳过查询步骤。
多线程与回调处理
CTP 的行情回调是在 API 内部的网络线程里触发的。OnRtnDepthMarketData 里如果做复杂计算,会阻塞后续行情接收。正确做法是把行情数据扔进无锁队列,由另一个线程消费。
订阅全市场时,行情推送量很大,高峰时段每秒几万笔 Tick。回调里直接写数据库或者做策略计算,必然丢包。队列长度要设大一些,消费线程用批量处理的方式写盘或计算。
void OnRtnDepthMarketData(CThostFtdcDepthMarketDataField* pDepthMarketData) {
// 只做深拷贝和入队,不做任何耗时操作
MarketData md = Convert(*pDepthMarketData);
queue.push(md);
}
实际部署时的坑
第一个坑:每个 CThostFtdcMdApi 实例需要独立的 flow 目录。如果多个实例共用一个目录,文件锁会冲突,导致其中一个实例初始化失败。创建实例时传入的路径要互相隔离。
第二个坑:同一个账号在多个实例上登录,有些柜台会限制并发登录数。如果柜台报“重复登录”,联系期货公司放开限制,或者用多个账号分别登录不同实例。
第三个坑:订阅成功后,OnRspSubMarketData 回调里返回的合约不一定马上有行情推送。非交易时段订阅,行情要等开盘才来。别在回调里判断“订阅失败”,要看错误码。
第四个坑:期权合约的命名规则和期货不同,有些合约代码带空格或者特殊字符,订阅前做一下 trim 处理,避免因为空格导致订阅失败。
订阅全市场行情是个体力活,核心就三件事:分连接、限速度、缓存合约列表。把这三点做到位,CTPAPI 跑全市场行情稳稳当当。