交易接口适配:从模拟API切换到CTP/UST/FIX等实盘协议

模拟盘跑得再漂亮,最终还是要面对实盘。这一步,说白了就是把你代码里那些「假数据」的接口,换成交易所真正认的协议。

我见过不少团队,模拟盘回测曲线漂亮得不行,一上实盘就各种断连、超时、乱码。为什么?因为模拟API和实盘协议,根本就是两码事。

模拟API与实盘协议的核心差异

先看一张对比图,你就明白了。

模拟API vs 实盘协议 核心差异 模拟API • 本地内存/文件读写 • 无网络延迟,即时响应 • 无风控校验 • 数据格式简单(JSON/CSV) • 无断连重连机制 • 无序列号/校验和 • 单线程,无并发问题 • 无交易时段限制 实盘协议(CTP/UST/FIX) • TCP/UDP网络通信 • 毫秒级延迟,丢包重传 • 资金/持仓/限价风控 • 二进制编码(如FIX tag=value) • 心跳保活,自动重连 • 序列号防重放,CRC校验 • 多线程/异步回调 • 严格遵循交易时段

你看,模拟API就像在自家院子里练车,实盘协议是上高速。规则完全不同。

CTP接口适配要点

CTP(中国期货市场交易接口)是国内期货做市的主流协议。我个人习惯把它分成三层来理解:

  • 网络层:TCP长连接,需要处理心跳、重连、粘包
  • 协议层:二进制编码,字段定长/变长混合
  • 业务层:报单、撤单、查询、成交回报

我在项目中遇到过最坑的事:CTP的OnRtnOrderOnRtnTrade回调,在极端行情下可能乱序到达。你收到成交回报时,订单状态可能还是「未成交」。嗯,这里必须加一个序列号排序逻辑。

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秒)
  • 实现fromApptoApp回调处理业务消息
  • 注意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_disconnecton_reconnect两个回调,并且加了告警。

总结

交易接口适配,说白了就是一层「翻译」:把策略层的统一指令,翻译成各个交易所协议能理解的报文。核心就三点:

  1. 理解协议差异(二进制 vs 文本、TCP vs UDP)
  2. 使用适配器模式隔离变化
  3. 做好异常处理(断连、超时、乱序、重传)

做到这三点,你的系统就能在模拟盘和实盘之间平滑切换。剩下的,就是交给市场去检验了。


公众号:蓝海资料掘金营,微信deep3321