一、Tick数据源接入:交易所行情网关协议解析、FIX/FAST协议解码、UDP组播与TCP回放链路设计

做高频交易系统,第一道坎就是怎么把行情数据稳稳地接进来。我见过不少团队,策略模型写得漂漂亮亮,结果一到实盘,数据源就断了,或者延迟高得离谱。说白了,数据源接入是整个系统的地基,地基不稳,上面盖多高的楼都没用。

今天我们就来聊聊Tick数据源接入的几个核心环节。嗯,这里我主要讲三块:行情网关协议解析、FIX/FAST协议解码,以及UDP组播与TCP回放链路的设计。

1.1 行情网关协议解析

交易所的行情网关,说白了就是一道门。各家交易所都有自己的门禁系统——也就是协议。比如上交所的STEP协议,深交所的SZV5协议,中金所的CTP协议。每个协议都有自己的报文格式、心跳机制、登录流程。

我个人习惯的做法是:先把协议文档吃透,然后抽象出一个统一的网关接口层。为什么?因为你想想看,策略层不希望关心今天连的是上交所还是深交所,它只想要一个统一的Tick数据流。

核心要点:行情网关接入的关键在于协议适配层的设计。每个交易所的协议差异很大,但通过适配器模式,我们可以把差异封装在网关内部,对外暴露统一的接口。

我在项目中遇到过最头疼的问题是什么?是交易所的协议升级。有一次上交所的STEP协议突然改了心跳间隔,我们的网关没及时更新,结果被交易所踢下线了。从那以后,我要求团队必须把协议版本号作为配置项,并且要有自动检测机制。

1.2 FIX/FAST协议解码

FIX协议,全称是Financial Information eXchange,金融信息交换协议。它是金融行业的标准协议之一,但有个问题——它太啰嗦了。每个字段都是明文标签,一条行情消息可能几百个字节。对于高频交易来说,这延迟太大了。

于是就有了FAST协议。FAST是FIX Adapted for Streaming的缩写,说白了就是给FIX协议减肥。它通过模板和编码规则,把消息压缩到原来的十分之一甚至更小。

FAST解码的核心思想是什么?我总结为三点:

  • 模板驱动:预先定义好消息模板,解码时按模板解析,省去了字段标签的传输
  • 增量编码:只传输变化的部分,不变的部分用前值填充
  • 字节对齐优化:用位操作代替字节操作,减少数据量

来看一个简单的FAST解码示例:

// FAST解码核心逻辑(伪代码)
class FastDecoder {
    // 模板定义
    struct Template {
        int fieldId;
        FieldType type;  // int32, decimal, string等
        bool isIncremental;  // 是否增量编码
        int defaultValue;    // 默认值
    };
    
    // 解码一条消息
    Message decode(byte[] data, Template template) {
        Message msg = new Message();
        int bitPos = 0;
        
        for (Field field : template.fields) {
            if (field.isIncremental) {
                // 检查presence bit
                bool hasValue = (data[bitPos/8] & (1 << (bitPos%8))) != 0;
                bitPos++;
                
                if (hasValue) {
                    // 读取增量值
                    int delta = readInt(data, bitPos);
                    msg.setField(field.fieldId, field.lastValue + delta);
                    field.lastValue = msg.getField(field.fieldId);
                } else {
                    // 使用前值
                    msg.setField(field.fieldId, field.lastValue);
                }
            } else {
                // 非增量字段,直接读取
                msg.setField(field.fieldId, readValue(data, bitPos, field.type));
            }
        }
        
        return msg;
    }
}

避坑指南:我曾经在FAST解码上栽过一个大跟头。当时没注意模板版本号,结果生产环境用了旧模板解析新格式的数据,所有价格字段都解析错了。幸好是模拟盘,不然亏大了。所以,模板版本管理一定要做好,最好用哈希校验模板文件。

1.3 UDP组播与TCP回放链路设计

行情数据的传输,通常有两种方式:UDP组播和TCP回放。

UDP组播是主力链路。为什么?因为快。UDP没有三次握手,没有重传机制,延迟极低。交易所通常会用组播把行情数据推送给所有订阅者。

但UDP有个致命问题——丢包。网络抖动、交换机缓存溢出,都可能导致丢包。对于高频交易来说,丢一个Tick可能就错过了一个交易机会。

所以就有了TCP回放链路。TCP回放链路的作用是:当UDP链路出现丢包时,通过TCP链路请求重传丢失的数据包。

我设计这套链路时,遵循的原则是:

  • UDP做主链路:实时性要求高的数据走UDP
  • TCP做辅链路:用于补包和回放历史数据
  • 自动切换机制:检测到UDP丢包时,自动通过TCP请求重传

来看一个典型的双链路架构:

// UDP组播接收 + TCP回放补包
class TickDataLink {
    // UDP组播接收
    MulticastSocket udpSocket;
    // TCP回放连接
    Socket tcpSocket;
    // 序列号管理器
    SequenceManager seqMgr;
    
    void onUdpPacket(byte[] data) {
        Packet packet = decode(data);
        long seqNo = packet.getSeqNo();
        
        // 检查序列号是否连续
        if (seqMgr.isContinuous(seqNo)) {
            // 正常处理
            processTick(packet);
        } else {
            // 发现丢包,通过TCP请求重传
            long lostStart = seqMgr.getLastSeqNo() + 1;
            long lostEnd = seqNo - 1;
            requestRetransmit(lostStart, lostEnd, tcpSocket);
            
            // 缓存当前包,等待补包完成
            buffer.put(seqNo, packet);
        }
    }
    
    void onTcpRetransmit(byte[] data) {
        Packet packet = decode(data);
        // 将补包插入到正确位置
        buffer.put(packet.getSeqNo(), packet);
        
        // 检查是否所有缺失包都已补齐
        if (seqMgr.allContinuous()) {
            // 按序列号顺序处理所有缓存包
            flushBuffer();
        }
    }
}

这里有个关键点:序列号管理。每个Tick数据包都必须有唯一的、递增的序列号。这样我们才能判断是否丢包,以及丢了多少包。

注意:UDP组播的接收缓冲区一定要设置得足够大。我记得有一次生产环境,因为缓冲区太小,导致大量数据包被内核丢弃。调大缓冲区后,丢包率从5%降到了0.1%。具体多大合适?一般建议设置为最大消息大小的10倍以上。

另外,TCP回放链路还有一个重要用途——初始化同步。当系统启动时,UDP链路可能已经错过了很多数据。这时候需要通过TCP回放链路,拉取最近一段时间的历史数据,把状态恢复到最新。

1.4 整体架构图

下面我用一张SVG图来展示整个Tick数据源接入的核心逻辑:

Tick数据源接入核心架构 交易所行情网关 上交所 STEP协议 深交所 SZV5协议 中金所 CTP协议 其他交易所 协议解码层 FIX协议解码 FAST协议解码 私有协议解码 传输链路层 UDP组播(主链路) TCP回放(辅链路) 自动补包机制 统一Tick数据流输出 交易所层 协议解码层 传输链路层 数据输出层

这张图展示了我设计的四层架构。从交易所行情网关,到协议解码,再到传输链路,最后输出统一的Tick数据流。每一层都有明确的职责,层与层之间通过接口解耦。

嗯,这里要特别强调一点:每一层都要有监控和告警。我在生产环境中吃过亏,UDP链路断了半小时才发现,因为监控只做了应用层,没做网络层。现在我的做法是:每层都埋点,延迟、丢包率、吞吐量全部上报,任何异常都能秒级发现。

个人经验:刚开始做这套系统时,我总想着把所有功能都塞到一个进程里。后来发现,这样做调试和运维都特别痛苦。现在我的习惯是:每个核心模块独立部署,通过共享内存或消息队列通信。这样即使某个模块挂了,也不会影响其他模块。

最后总结一下。Tick数据源接入,说白了就是三件事:读懂交易所的协议、高效解码、保证数据传输的可靠性。这三件事做好了,后面的策略开发才能安心。我见过太多团队在这上面栽跟头,所以希望你能重视起来。


专注资料整理