19. 实盘交易接口:CTP接口对接、自动下单、持仓管理
各位同学,今天咱们聊点真家伙——CTP接口。说实话,很多做量化的人,策略写得花里胡哨,一到对接实盘就卡壳。我当年第一次对接CTP时,也踩了不少坑。今天我把这些经验掰开揉碎了讲给你听。
19.1 CTP接口到底是什么?
CTP,全称是综合交易平台。说白了,它就是期货公司跟交易所之间的桥梁。你写的策略要下单,得先过CTP这道门。
我个人习惯把CTP接口分成两层:
- 交易接口:负责下单、撤单、查持仓、查资金
- 行情接口:负责接收实时行情数据
嗯,这里要注意:CTP是C++写的,但咱们用Python做量化,怎么办?用封装好的Python接口,比如vnpy的CTP网关,或者ctp-python这个库。
核心要点:CTP接口是同步请求+异步回调的模式。你发一个下单请求,它不会立刻告诉你结果,而是通过回调函数告诉你。这个设计理念,跟咱们平时写代码的思维不太一样。
19.2 环境准备与初始化
先搞定环境。我建议你用Python 3.8以上版本,别用太老的。
# 安装ctp-python库
pip install ctp-python
# 或者用vnpy的CTP网关
pip install vnpy_ctp
初始化交易接口,代码长这样:
from ctp import MdApi, TraderApi
class MyTrader(TraderApi):
def __init__(self):
super().__init__()
self.front_addr = "tcp://180.168.146.187:10130" # 仿真环境
self.broker_id = "9999"
self.user_id = "你的账号"
self.password = "你的密码"
def OnFrontConnected(self):
print("交易接口连接成功")
self.ReqUserLogin()
def OnRspUserLogin(self, pRspUserLogin, pRspInfo, nRequestID, bIsLast):
if pRspInfo.ErrorID == 0:
print(f"登录成功,交易日:{pRspUserLogin.TradingDay}")
else:
print(f"登录失败:{pRspInfo.ErrorMsg}")
# 启动
trader = MyTrader()
trader.RegisterFront(trader.front_addr)
trader.Init()
我的经验:仿真环境跟实盘环境地址不一样,千万别搞混。我曾经有一次在仿真环境调试了三天,结果发现连的是实盘地址,差点酿成大祸。
19.3 自动下单:从发单到成交
下单是核心操作。CTP的下单接口叫ReqOrderInsert,参数很多,但常用的就这几个:
| 参数名 | 说明 | 示例值 |
|---|---|---|
| InstrumentID | 合约代码 | rb2501 |
| Direction | 买卖方向 | '0'买 / '1'卖 |
| CombOffsetFlag | 开平标志 | '0'开仓 / '1'平仓 |
| LimitPrice | 价格 | 3500.0 |
| VolumeTotalOriginal | 数量 | 1 |
写个下单函数:
def place_order(self, instrument, direction, offset, price, volume):
"""下单函数"""
order = {
'InstrumentID': instrument,
'Direction': direction,
'CombOffsetFlag': offset,
'LimitPrice': price,
'VolumeTotalOriginal': volume,
'OrderPriceType': '2', # 限价单
'TimeCondition': '1', # 当日有效
'VolumeCondition': '1', # 任意数量
'MinVolume': 1,
'ContingentCondition': '1',
'ForceCloseReason': '0',
'IsAutoSuspend': 0,
'UserForceClose': 0,
}
self.ReqOrderInsert(order, 0)
print(f"下单请求已发送:{instrument} {direction} {volume}手 @ {price}")
避坑指南:我曾经在实盘时忘记设置CombOffsetFlag,结果平仓变成了开仓,多了一倍仓位。记住:平仓一定要明确指定开平标志。
19.4 持仓管理:别让你的仓位失控
持仓管理,说白了就是搞清楚你现在手里有什么、有多少、成本多少。CTP通过QryInvestorPosition查询持仓。
我一般这样管理持仓:
class PositionManager:
def __init__(self):
self.positions = {} # 合约 -> 持仓信息
def on_position(self, position):
"""处理持仓回报"""
instrument = position.InstrumentID
self.positions[instrument] = {
'long_volume': position.LongVolume,
'short_volume': position.ShortVolume,
'long_cost': position.LongCost,
'short_cost': position.ShortCost,
'profit': position.PositionProfit,
}
def get_position(self, instrument):
"""获取某个合约的持仓"""
return self.positions.get(instrument, None)
def check_position_limit(self, instrument, max_volume=100):
"""检查是否超仓"""
pos = self.get_position(instrument)
if pos:
total = pos['long_volume'] + pos['short_volume']
if total >= max_volume:
print(f"警告:{instrument} 持仓已达上限 {total}/{max_volume}")
return False
return True
核心逻辑:持仓管理不是查一次就完事了。行情在变,持仓也在变。我建议每5秒轮询一次持仓,或者用CTP的OnRtnPosition回调实时更新。
19.5 完整的交易流程
把上面这些串起来,一个完整的交易流程是这样的:
- 连接CTP:初始化交易接口,登录
- 查询初始持仓:获取当前持仓状态
- 策略信号触发:比如基差突破阈值
- 检查持仓限制:别超仓,别违反风控
- 发送下单请求:调用
place_order - 处理成交回报:更新持仓信息
- 循环监控:持续检查持仓和资金
我画了一张流程图,帮你理清思路:
19.6 实战中的坑与对策
我做了这么多年,总结几个最常见的坑:
- 网络断开:CTP连接断了,你不知道,还在傻傻等回报。解决方案:加心跳检测,5秒没收到回报就重连。
- 重复下单:回调延迟导致你以为没成交,又发了一单。解决方案:用订单ID去重,每个订单生成唯一ID。
- 价格滑点:你挂的限价单没成交,行情跑了。解决方案:用市价单或者追单逻辑。
我的小技巧:实盘前先用仿真环境跑一周,把各种异常情况都模拟一遍。比如断网、登录失败、下单超时。我每次上线新策略,都会在仿真环境里故意制造这些故障,确保程序能扛得住。
19.7 代码整合:一个最小可用的交易系统
最后,给你一个最小可用的交易系统骨架:
class CTPTradingSystem:
def __init__(self):
self.trader = MyTrader()
self.pos_mgr = PositionManager()
self.running = False
def start(self):
"""启动系统"""
self.trader.RegisterFront(self.trader.front_addr)
self.trader.Init()
self.running = True
print("交易系统已启动")
def on_tick(self, tick):
"""行情回调,触发策略"""
instrument = tick.InstrumentID
# 检查持仓限制
if not self.pos_mgr.check_position_limit(instrument):
return
# 策略逻辑(简化)
if tick.LastPrice > 3500:
self.trader.place_order(instrument, '0', '0', tick.LastPrice, 1)
def on_order(self, order):
"""订单回报"""
print(f"订单状态:{order.OrderStatus}")
def on_trade(self, trade):
"""成交回报"""
print(f"成交:{trade.InstrumentID} {trade.Direction} {trade.Volume}手")
# 更新持仓
self.pos_mgr.on_position(trade)
def stop(self):
"""停止系统"""
self.trader.Release()
self.running = False
print("交易系统已停止")
嗯,这套代码虽然简单,但核心逻辑都在了。你可以在上面加风控、加日志、加监控。记住一点:实盘交易,稳定比什么都重要。
最后提醒:CTP接口的API文档一定要看。我见过太多人只看示例代码不看文档,结果漏掉了关键参数。API文档虽然枯燥,但它是你的救命稻草。
好了,这一章的内容就到这。CTP对接不难,但细节决定成败。你回去把代码跑一遍,有问题随时交流。