10、期货账户对接:CTP接口接入、交易指令封装、账户资金与持仓管理
好,咱们进入实战环节最硬核的部分——账户对接。
说实话,很多做策略的人,策略回测跑得飞起,一到实盘就卡在接口对接上。CTP(综合交易平台)是上期技术开发的,国内期货公司几乎都在用。你想想看,不把它搞定,策略写得再好也白搭。
10.1 CTP接口接入:从零开始连上交易所
CTP接口其实是一套C++的动态库。我个人习惯用Python封装,因为策略开发效率高。但底层逻辑是一样的——你得先搞定三个核心对象:交易接口、行情接口、回调处理。
核心流程:
- 创建交易API实例(CThostFtdcTraderApi)
- 注册回调类(继承CThostFtdcTraderSpi)
- 订阅私有流(订单、成交、资金变动)
- 调用Init()连接前置机
- 等待OnFrontConnected回调
- 发起登录认证(ReqAuthenticate + ReqUserLogin)
- 等待OnRspUserLogin回调,确认登录成功
我在项目中遇到过一个问题:CTP前置机地址配错了,结果连了半小时没反应。后来发现是端口号写成了TCP的,实际应该用UDP的。嗯,这里要注意——交易和行情前置机地址是分开的,别搞混。
// 伪代码示例:CTP初始化
TraderApi = CThostFtdcTraderApi::CreateFtdcTraderApi();
TraderSpi = new MyTraderSpi();
TraderApi->RegisterSpi(TraderSpi);
TraderApi->SubscribePrivateTopic(THOST_TERT_QUICK);
TraderApi->RegisterFront("tcp://192.168.1.100:41205");
TraderApi->Init();
避坑指南:我曾经因为忘记调用SubscribePrivateTopic,导致订单状态回调收不到。CTP默认不订阅私有流,你得主动告诉它「我要听」。另外,登录时记得先做认证(ReqAuthenticate),再登录(ReqUserLogin),顺序反了会被踢。
10.2 交易指令封装:把CTP的原始接口变成策略能用的
CTP原生的接口参数太多了。一个报单函数,参数列表能写满一屏幕。你想想看,策略里每次下单都要填那么多字段,不出错才怪。
我的做法是——封装一层交易指令对象。把常用的下单模式抽象成几个方法:
- 限价单:指定价格、数量、开平标志
- 市价单:只给数量,价格由交易所撮合
- 止损单:触发条件价格,到达后转市价
- 止盈单:类似止损,方向相反
说白了,就是把CTP的CThostFtdcInputOrderField结构体,包装成一个Order对象。策略层只需要传「品种、方向、数量、价格类型」四个字段就够了。
class Order:
def __init__(self, symbol, direction, volume, price_type, price=0):
self.symbol = symbol # 合约代码
self.direction = direction # 'buy' or 'sell'
self.volume = volume # 手数
self.price_type = price_type # 'limit' or 'market'
self.price = price # 限价单才需要
def to_ctp_field(self):
# 转换成CTP的InputOrder结构
field = CThostFtdcInputOrderField()
field.InstrumentID = self.symbol
field.Direction = THOST_FTDC_D_Buy if self.direction == 'buy' else THOST_FTDC_D_Sell
field.VolumeTotalOriginal = self.volume
field.OrderPriceType = THOST_FTDC_OPT_LimitPrice if self.price_type == 'limit' else THOST_FTDC_OPT_AnyPrice
field.LimitPrice = self.price
return field
个人经验:我习惯在封装层加一个「订单ID生成器」。CTP的OrderRef字段需要唯一,我直接用「策略ID + 时间戳 + 自增序号」拼接。这样出了问题,查日志一眼就能定位是哪个策略下的单。
10.3 账户资金与持仓管理:实时盯住你的钱和货
账户管理这块,很多人只关注下单,忽略了资金和持仓的同步。我见过最惨的情况——策略以为账户里还有100万,实际只剩10万了,结果连续报单被拒,仓位全乱套。
CTP提供了两个关键回调:
- OnRspQryTradingAccount:查询资金账户信息
- OnRspQryInvestorPosition:查询持仓信息
但注意,这两个是查询接口,不是推送接口。你得自己定时轮询,或者利用成交回报来更新。
我的做法是维护一个本地账户快照:
| 字段 | 来源 | 更新时机 |
|---|---|---|
| 可用资金 | OnRspQryTradingAccount | 每30秒轮询一次 |
| 持仓数量 | OnRspQryInvestorPosition | 每30秒轮询一次 |
| 冻结资金 | OnRtnOrder(订单状态变化) | 实时更新 |
| 持仓盈亏 | OnRtnTrade(成交回报) | 实时更新 |
为什么会这样设计?因为CTP的查询接口有频率限制,你每秒查一次会被封IP。但订单和成交的回调是实时的,可以用来做增量更新。
我曾经踩过的坑:只依赖轮询更新持仓,结果在快速行情下,轮询间隔内发生了多笔成交,持仓数据滞后了3秒。策略以为还有仓位,继续开仓,结果超仓了。后来改成「成交回报实时更新 + 轮询兜底校验」的双保险模式,再没出过问题。
10.4 核心逻辑流程图
下面这张图,是我自己画的一个CTP对接的核心流程。你看一眼,基本就知道整个链路怎么走了。
嗯,这张图基本把CTP对接的骨架画出来了。你照着这个流程走,至少不会漏掉关键步骤。
10.5 实战中的几个细节
最后聊几个我实战中反复踩过的坑:
- 断线重连:CTP前置机偶尔会断,你得在OnFrontDisconnected回调里启动重连逻辑。我一般等5秒再重连,连续失败3次就报警。
- 订单状态机:CTP的订单状态有十几种(已报、已撤、部分成交、全部成交...)。我建议只关注三个状态:全部成交、已撤单、废单。其他中间状态一律视为「等待中」。
- 撤单逻辑:撤单需要传入原订单的OrderRef和FrontID。我习惯在本地维护一个「订单字典」,key是OrderRef,value是订单详情。撤单时直接查字典,不用再调查询接口。
一个小技巧:CTP的成交回报(OnRtnTrade)里带的有成交价格和数量。我每次收到成交回报,就立刻更新本地持仓和资金快照。这样即使轮询还没到,策略也能拿到最新的账户状态。
好了,CTP对接这块就聊到这儿。说白了,就是「连上去、发指令、盯账户」这三件事。但每一件都有不少细节,你多跑几次模拟盘,自然就熟了。
公众号:蓝海资料掘金营,微信deep3321