交易接口适配:从模拟API切换到CTP/UST/FIX等实盘协议
模拟盘跑得再漂亮,最终还是要面对实盘。这一步,说白了就是把你代码里那些「假数据」的接口,换成交易所真正认的协议。
我见过不少团队,模拟盘回测曲线漂亮得不行,一上实盘就各种断连、超时、乱码。为什么?因为模拟API和实盘协议,根本就是两码事。
模拟API与实盘协议的核心差异
先看一张对比图,你就明白了。
你看,模拟API就像在自家院子里练车,实盘协议是上高速。规则完全不同。
CTP接口适配要点
CTP(中国期货市场交易接口)是国内期货做市的主流协议。我个人习惯把它分成三层来理解:
- 网络层:TCP长连接,需要处理心跳、重连、粘包
- 协议层:二进制编码,字段定长/变长混合
- 业务层:报单、撤单、查询、成交回报
我在项目中遇到过最坑的事:CTP的OnRtnOrder和OnRtnTrade回调,在极端行情下可能乱序到达。你收到成交回报时,订单状态可能还是「未成交」。嗯,这里必须加一个序列号排序逻辑。
CTP适配核心代码骨架
class CTPTraderAdapter:
def __init__(self, front_addr, broker_id, user_id, password):
self.api = CThostFtdcTraderApi.CreateFtdcTraderApi()
self.api.RegisterSpi(self)
self.api.RegisterFront(front_addr)
self.api.Init()
def OnFrontConnected(self):
# 登录请求
req = CThostFtdcReqUserLoginField()
req.BrokerID = self.broker_id
req.UserID = self.user_id
req.Password = self.password
self.api.ReqUserLogin(req, 0)
def OnRtnOrder(self, order):
# 订单状态更新 - 注意乱序问题
self.order_manager.update_order(order)
def send_order(self, symbol, price, volume, direction):
req = CThostFtdcInputOrderField()
req.InstrumentID = symbol
req.LimitPrice = price
req.VolumeTotalOriginal = volume
req.Direction = direction # '0'买 '1'卖
req.OrderPriceType = '2' # 限价
return self.api.ReqOrderInsert(req, self.request_id++)
注意:CTP的ReqOrderInsert返回的并不是订单编号,而是一个请求编号。真正的交易所订单号在OnRtnOrder回调里才能拿到。我曾经见过新手直接拿请求编号当订单号去撤单,结果撤了个寂寞。
UST接口适配要点
UST(UDP-based Securities Trading)协议,说白了就是基于UDP的极速交易协议。它牺牲了一定的可靠性,换来了更低的延迟。
你想想看,UDP本身不保证有序、不保证不丢包。那怎么做交易?
答案是:应用层自己实现可靠传输。UST协议里每个报文都带序列号,接收方需要回复ACK。如果发现丢包,就请求重传。
个人经验:UST的心跳间隔建议设为100ms,比CTP的30秒短得多。因为UDP没有TCP那样的保活机制,必须靠高频心跳来检测连接状态。我曾经把心跳设成500ms,结果在高频行情下连续丢包3次,系统自动断连了。
class USTAdapter:
def __init__(self, multicast_ip, port):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.setsockopt(socket.IPPROTO_UDP, socket.UDP_CHECKSUM, 1)
self.seq_num = 0
self.ack_buffer = {}
def send_order(self, order):
# 构造UST报文
msg = self.encode_order(order, self.seq_num)
self.sock.sendto(msg, (self.multicast_ip, self.port))
self.ack_buffer[self.seq_num] = (order, time.time())
self.seq_num += 1
def on_ack(self, seq):
# 收到确认,从缓冲区移除
if seq in self.ack_buffer:
del self.ack_buffer[seq]
def check_timeout(self):
# 超时重传逻辑
now = time.time()
for seq, (order, ts) in list(self.ack_buffer.items()):
if now - ts > 0.05: # 50ms超时
self.send_order(order) # 重传
FIX协议适配要点
FIX(Financial Information eXchange)是国际通用的金融协议。它的特点是文本编码、tag=value格式,可读性强但解析开销大。
一个典型的FIX报单消息长这样:
8=FIX.4.2|9=78|35=D|49=SENDER|56=TARGET|34=1|52=20240101-00:00:00|11=ORD001|55=600519|54=1|38=100|44=150.00|10=123|
每个tag都有含义:35=D表示新订单,55是证券代码,54=1表示买入。最后10=123是校验和。
我建议你直接使用成熟的FIX引擎库,比如QuickFIX/C++或Python的simplefix。自己手写解析器?我曾经试过,结果被各种边缘情况折磨得够呛——比如字段顺序不固定、重复组解析、校验和计算方式...
FIX适配核心原则
- 使用标准引擎,不要自己造轮子
- 配置好
HeartBtInt心跳间隔(通常30秒) - 实现
fromApp和toApp回调处理业务消息 - 注意
ResetSeqNumFlag:每天开市前重置序列号
适配层设计模式:适配器模式
不管底层是CTP、UST还是FIX,你的策略层应该只看到统一的接口。这就是适配器模式的价值。
class TradingAdapter(ABC):
@abstractmethod
def connect(self): pass
@abstractmethod
def send_order(self, order: Order) -> str: pass
@abstractmethod
def cancel_order(self, order_id: str): pass
@abstractmethod
def on_order_update(self, callback): pass
class CTPAdapter(TradingAdapter):
# 实现CTP具体逻辑
pass
class USTAdapter(TradingAdapter):
# 实现UST具体逻辑
pass
class FIXAdapter(TradingAdapter):
# 实现FIX具体逻辑
pass
这样做的好处是:你的做市策略只需要依赖TradingAdapter这个抽象接口。换交易所?换协议?只需要换一个适配器实现,策略代码一行都不用改。
实盘迁移的避坑指南
我从模拟盘切到实盘时,踩过不少坑。分享几个印象深刻的:
- 订单ID冲突:模拟盘我用自增ID,实盘CTP要求客户端ID+请求编号唯一。后来我改成
client_id + timestamp + seq的组合。 - 资金校验:模拟盘不校验资金,实盘下单前必须先查可用资金。我加了一层
pre_trade_check。 - 交易时段:模拟盘24小时可交易,实盘有集合竞价、午休、收盘。我写了个
TradingSessionManager来管理。 - 重连风暴:网络抖动时,所有客户端同时重连,可能把交易所网关打挂。我加了指数退避+随机抖动。
曾经有一次,我忘了处理CTP的OnFrontDisconnected回调。结果交易所午间重启,我的系统一直卡在断连状态,下午开盘后整整5分钟没有报单。从那以后,我每个适配器都强制实现了on_disconnect和on_reconnect两个回调,并且加了告警。
总结
交易接口适配,说白了就是一层「翻译」:把策略层的统一指令,翻译成各个交易所协议能理解的报文。核心就三点:
- 理解协议差异(二进制 vs 文本、TCP vs UDP)
- 使用适配器模式隔离变化
- 做好异常处理(断连、超时、乱序、重传)
做到这三点,你的系统就能在模拟盘和实盘之间平滑切换。剩下的,就是交给市场去检验了。
公众号:蓝海资料掘金营,微信deep3321