3. CTP API核心对象:MdApi与TraderApi的生命周期、初始化流程与回调机制
CTP API里最核心的两个对象,就是MdApi和TraderApi。一个管行情,一个管交易。说白了,一个负责看盘,一个负责下单。这两个对象搞明白了,CTP对接就算入门了。
我个人习惯把MdApi比作“收音机”,TraderApi比作“电话机”。收音机只管接收广播信号,电话机需要你拨号、通话、挂断。嗯,这个比喻虽然糙了点,但道理是通的。
3.1 MdApi:行情接收的生命周期
MdApi的生命周期其实很简单,就四个阶段:创建、连接、登录、接收。我见过不少新手一上来就想着怎么处理行情数据,结果连初始化都没搞对,折腾半天收不到数据。
3.1.1 创建与初始化
创建MdApi实例,只需要一行代码:
# 创建行情API实例
md_api = MdApi.CreateMdApi()
# 注册行情前置地址
md_api.RegisterFront("tcp://180.168.146.187:10131")
# 注册回调类
md_api.RegisterSpi(self)
# 初始化
md_api.Init()
这里有个坑,我曾经踩过——RegisterSpi必须在Init之前调用。为什么?因为Init会立即尝试连接,如果回调还没注册,连接成功后的回调事件你就收不到了。你想想看,连接成功了,但没人通知你,是不是很尴尬?
3.1.2 登录流程
Init之后,MdApi会自动连接前置机。连接成功后,会触发OnFrontConnected回调。这时候你才能发起登录请求:
def OnFrontConnected(self):
print("行情前置连接成功")
# 构造登录请求
req = CThostFtdcReqUserLoginField()
req.BrokerID = "9999"
req.UserID = "your_user_id"
req.Password = "your_password"
# 发起登录
self.md_api.ReqUserLogin(req, 0)
登录成功后,会触发OnRspUserLogin回调。这时候才算真正准备好接收行情了。
3.1.3 订阅行情
登录成功后,就可以订阅合约了:
def OnRspUserLogin(self, pRspUserLogin, pRspInfo, nRequestID, bIsLast):
if pRspInfo.ErrorID == 0:
print("行情登录成功")
# 订阅合约
instruments = ["rb2401", "rb2405"]
self.md_api.SubscribeMarketData(instruments, len(instruments))
else:
print(f"登录失败: {pRspInfo.ErrorMsg}")
订阅成功后,行情数据会通过OnRtnDepthMarketData回调源源不断地推送过来。这里要注意,行情推送的频率很高,尤其是高频行情,回调函数里千万别做耗时操作。
3.2 TraderApi:交易的生命周期
TraderApi比MdApi复杂一些。它不仅要连接、登录,还要处理查询、下单、撤单、成交回报等一系列操作。说白了,MdApi是单向的,TraderApi是双向的。
3.2.1 初始化流程
TraderApi的初始化流程和MdApi类似,但多了一个“确认结算单”的步骤:
# 创建交易API实例
trade_api = TraderApi.CreateTraderApi()
# 注册交易前置地址
trade_api.RegisterFront("tcp://180.168.146.187:10130")
# 注册回调类
trade_api.RegisterSpi(self)
# 初始化
trade_api.Init()
连接成功后,同样需要登录:
def OnFrontConnected(self):
print("交易前置连接成功")
req = CThostFtdcReqUserLoginField()
req.BrokerID = "9999"
req.UserID = "your_user_id"
req.Password = "your_password"
self.trade_api.ReqUserLogin(req, 0)
3.2.2 确认结算单
登录成功后,有个关键步骤——确认结算单。这是CTP特有的机制,每天第一次登录时必须确认前一天的结算单,否则无法进行交易操作。
def OnRspUserLogin(self, pRspUserLogin, pRspInfo, nRequestID, bIsLast):
if pRspInfo.ErrorID == 0:
print("交易登录成功")
# 查询结算单
req = CThostFtdcQrySettlementInfoField()
req.BrokerID = "9999"
req.InvestorID = "your_investor_id"
self.trade_api.ReqQrySettlementInfo(req, 0)
else:
print(f"登录失败: {pRspInfo.ErrorMsg}")
def OnRspQrySettlementInfo(self, pSettlementInfo, pRspInfo, nRequestID, bIsLast):
if bIsLast:
# 确认结算单
req = CThostFtdcSettlementInfoConfirmField()
req.BrokerID = "9999"
req.InvestorID = "your_investor_id"
self.trade_api.ReqSettlementInfoConfirm(req, 0)
3.3 回调机制详解
CTP的回调机制,说白了就是“你注册一个函数,CTP在特定事件发生时调用它”。这是典型的观察者模式。回调函数都是异步的,你永远不知道下一个回调什么时候来。
3.3.1 回调的分类
回调可以分为三类:
| 回调类型 | 命名规则 | 触发时机 | 示例 |
|---|---|---|---|
| 连接回调 | OnFront* | 网络连接状态变化 | OnFrontConnected |
| 请求回调 | OnRsp* | 请求的响应到达 | OnRspUserLogin |
| 推送回调 | OnRtn* | 服务器主动推送 | OnRtnDepthMarketData |
我个人习惯把回调函数分成两个线程处理:一个线程专门处理连接和请求回调,另一个线程处理推送回调。这样可以避免推送数据阻塞了请求响应。
3.3.2 回调的线程模型
CTP的回调是在API内部的工作线程中触发的。这意味着:
- 回调函数里不能做耗时操作,否则会阻塞后续回调
- 回调函数里不能直接操作UI,需要跨线程同步
- 多个回调可能同时触发,需要考虑线程安全
3.4 生命周期管理的最佳实践
在实际项目中,我总结了几条经验:
- 单例模式管理API实例:一个进程里只创建一个MdApi和一个TraderApi实例,避免重复创建导致资源浪费。
- 心跳检测:CTP有自动心跳机制,但建议自己加一个定时器,定期检查连接状态。如果发现断开,自动重连。
- 重连策略:重连时不要立即重试,建议指数退避。第一次等1秒,第二次等2秒,第三次等4秒,最多等30秒。
- 日志记录:所有回调函数的入参都要记录日志,方便排查问题。我一般用Python的logging模块,设置不同的日志级别。
3.5 知识体系总览
下面这张图展示了MdApi和TraderApi的核心生命周期和回调关系:
这张图把两个API的生命周期并列展示,方便对比。你会发现,前半段流程几乎一模一样,区别在于登录后的操作——MdApi订阅行情,TraderApi确认结算单后才能交易。
嗯,到这里,MdApi和TraderApi的核心内容就讲完了。记住一句话:初始化顺序不能乱,回调函数不能堵,结算单不能不确认。这三条做到了,CTP对接就稳了。