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会立即尝试连接,如果回调还没注册,连接成功后的回调事件你就收不到了。你想想看,连接成功了,但没人通知你,是不是很尴尬?

注意: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回调。这时候才算真正准备好接收行情了。

小技巧:登录请求的RequestID可以随便填,但建议用递增的数字,方便后续排查问题。我一般从1开始,每次加1。

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,需要跨线程同步
  • 多个回调可能同时触发,需要考虑线程安全
建议:回调函数里只做数据入队操作,真正的业务逻辑放在另一个线程里处理。我一般用Python的queue.Queue来做数据中转。

3.4 生命周期管理的最佳实践

在实际项目中,我总结了几条经验:

  1. 单例模式管理API实例:一个进程里只创建一个MdApi和一个TraderApi实例,避免重复创建导致资源浪费。
  2. 心跳检测:CTP有自动心跳机制,但建议自己加一个定时器,定期检查连接状态。如果发现断开,自动重连。
  3. 重连策略:重连时不要立即重试,建议指数退避。第一次等1秒,第二次等2秒,第三次等4秒,最多等30秒。
  4. 日志记录:所有回调函数的入参都要记录日志,方便排查问题。我一般用Python的logging模块,设置不同的日志级别。
核心要点:MdApi和TraderApi的生命周期管理,关键在于理解“异步”二字。所有操作都是异步的,你不能假设调用一个函数后立即得到结果。回调函数就是CTP给你的“回执”,告诉你操作的结果。

3.5 知识体系总览

下面这张图展示了MdApi和TraderApi的核心生命周期和回调关系:

CTP API核心对象生命周期与回调机制 MdApi(行情) TraderApi(交易) CreateMdApi() + RegisterSpi() Init() → 异步连接 OnFrontConnected() ReqUserLogin() OnRspUserLogin() SubscribeMarketData() CreateTraderApi() + RegisterSpi() Init() → 异步连接 OnFrontConnected() ReqUserLogin() OnRspUserLogin() 确认结算单 → 下单/查询

这张图把两个API的生命周期并列展示,方便对比。你会发现,前半段流程几乎一模一样,区别在于登录后的操作——MdApi订阅行情,TraderApi确认结算单后才能交易。

嗯,到这里,MdApi和TraderApi的核心内容就讲完了。记住一句话:初始化顺序不能乱,回调函数不能堵,结算单不能不确认。这三条做到了,CTP对接就稳了。

专注资料整理