
为什么你拿不到所有合约代码
很多刚接触程序化交易的朋友,打开CTP接口文档,看到ReqQryInstrument这个函数,以为调用一下就能拿到全市场合约。结果代码跑起来,回调函数一动不动,或者只返回了几十个合约。问题出在CTP的查询机制上。
CTP不是数据库,它不会主动把几千个合约推给你。你需要先建立连接、登录认证,然后主动发起查询请求,CTP才会通过回调函数把合约信息一条一条吐出来。整个过程是异步的,而且有流量控制。

交易接口和行情接口的区别
CTP有两套前置:交易前置和行情前置。这两套都能查合约,但返回的内容不一样。
交易前置查询的是你可以交易的合约,包含合约代码、交易所、合约乘数、最小变动价位、保证金率等完整信息。
行情前置查询的是有行情推送的合约,只返回合约代码和交易所,没有交易参数。
做程序化交易,你肯定要用交易前置查合约。行情前置查出来的合约列表,可能包含已经到期的期货合约,或者不让你交易的品种。
查询合约的完整流程
下面用C++接口举例,其他语言接口逻辑一样。
第一步 创建API实例并加载动态库
CThostFtdcTraderApi* pTraderApi = CThostFtdcTraderApi::CreateFtdcTraderApi("./flow/");
pTraderApi->RegisterSpi(this);
pTraderApi->SubscribePublicTopic(THOST_TERT_QUICK);
pTraderApi->SubscribePrivateTopic(THOST_TERT_QUICK);
pTraderApi->RegisterFront("tcp://180.168.146.187:10130");
pTraderApi->Init();
第二步 等待连接和登录完成
在OnFrontConnected回调里发起认证请求,在OnRspAuthenticate里发起登录请求,在OnRspUserLogin里确认登录成功。登录成功后,你才能查合约。
第三步 发起查询请求
CThostFtdcQryInstrumentField req;
memset(&req, 0, sizeof(req));
int ret = pTraderApi->ReqQryInstrument(&req, ++requestId);
这里req结构体全部填0,表示查询所有合约。你也可以指定ExchangeID或InstrumentID来过滤。
第四步 在回调里收集数据
void OnRspQryInstrument(CThostFtdcInstrumentField *pInstrument, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast) {
if (pInstrument) {
// 把pInstrument->InstrumentID存起来
instrumentList.push_back(pInstrument->InstrumentID);
}
if (bIsLast) {
// 查询结束,bIsLast为true表示最后一条
std::cout << "总共收到 " << instrumentList.size() << " 个合约" << std::endl;
}
}
bIsLast这个参数很关键。CTP不会一次性把几千个合约推给你,而是分批推送,最后一条的bIsLast才会是true。你必须等到bIsLast为true,才能确认查询完成。
为什么你只拿到部分合约
常见原因有三个。
第一,查询频率超限。 CTP对查询请求有流控,一秒内查询次数太多会被拒绝。你可以在每次查询之间加一点延迟,或者用ReqQryInstrument只查一次,等回调全部结束再查别的。
第二,登录未完成就查询。 如果OnRspUserLogin还没回调,你就调用了ReqQryInstrument,请求会被丢弃。必须在登录成功后再查。
第三,前置地址不对。 有些模拟环境的前置地址只开放部分合约。换一个正式环境的前置,或者用生产环境的地址试试。
拿到合约代码之后做什么
合约代码到手,下一步是过滤。全市场几千个合约,你不可能每个都交易。
按交易所过滤,只留上期所、大商所、郑商所、中金所的主力品种。
按合约月份过滤,只留最近三个月的合约。
按品种代码过滤,只留你策略涉及的品种。
过滤完的合约列表,存到本地数据库或者内存缓存里。每次程序启动时重新查询一次,避免CTP更新合约后你还在用旧列表。
用Python怎么查
如果你用Python的vnpy或者ctpbee,逻辑一样。以vnpy为例,登录成功后调用query_instrument,在on_instrument回调里接收合约。
def on_instrument(self, instrument: InstrumentData):
self.instrument_list.append(instrument.instrument_id)
vnpy底层帮你处理了分页和流控,你只需要在回调里收集数据。
合约代码的格式
CTP返回的InstrumentID是合约代码,比如rb2405、IF2406、m2409。交易所代码在ExchangeID字段里,比如SHFE、CFFEX、DCE。
不同交易所的合约代码规则不一样。上期所的螺纹钢是rb加月份,中金所的沪深300是IF加月份,大商所的豆粕是m加月份。你写策略的时候,不要把交易所代码和合约代码搞混。
常见坑点
合约代码大小写。 CTP返回的合约代码有时候是大写,有时候是小写。你下单的时候必须和CTP返回的完全一致,否则报错“合约不存在”。
过期合约还在列表里。 已经到期的合约,CTP可能还会返回,但你不能交易。需要根据当前日期过滤掉已经进入交割月的合约。
查询返回空。 如果OnRspQryInstrument回调里pInstrument是空指针,说明CTP没有更多合约了,或者你的查询请求被拒绝。检查pRspInfo里的错误码和错误信息。
流控导致断线。 短时间内大量查询请求,CTP会断开你的连接。查询合约不要放在循环里反复调,一次查询就够了。
创建API实例,注册前置地址。
等待
OnFrontConnected,发起认证。等待
OnRspAuthenticate,发起登录。等待
OnRspUserLogin,确认登录成功。调用
ReqQryInstrument,请求所有合约。在
OnRspQryInstrument里逐条收集,等到bIsLast为true。把合约列表存下来,过滤出你需要的品种。
这套流程走通,你就能拿到CTP当前支持的所有可交易合约代码。