行情数据接入:美股行情协议(ITCH/OUCH)、多播数据解析、数据包重组、快照与增量处理

做高频交易,第一关就是行情数据。你策略再牛,模型再准,拿不到干净、低延迟的行情数据,一切都是白搭。今天我们就来聊聊美股行情接入的那些硬核技术。

美股行情协议:ITCH 与 OUCH

纳斯达克提供了两套核心协议:ITCH 和 OUCH。说白了,ITCH 是行情协议,负责告诉你市场发生了什么;OUCH 是订单协议,负责让你去市场里搞事情。

ITCH(直连行情协议),我习惯叫它「市场广播员」。它通过多播方式,把每一笔订单的进入、修改、撤销、成交,全部实时推送给订阅者。你想想看,这就像坐在交易所的交易大厅里,能听到每一个交易员的喊价声。

我个人经验是,ITCH 协议最核心的数据结构是 OrderBook 的增量更新。它不会每次都给你全量的买卖盘口,而是告诉你:「有人在 150.20 这个价位加了一单 500 股」或者「有人把 150.25 的 200 股撤了」。你的任务,就是把这些增量信息,拼凑出完整的订单簿。

核心要点:ITCH 是增量驱动的,你必须维护本地订单簿状态。

OUCH(直连订单协议),则是你用来下单的通道。它比 FIX 协议更轻量、更快速。我记得第一次从 FIX 切换到 OUCH 时,单笔订单的确认延迟从几百微秒降到了几十微秒。嗯,这个差距在毫秒必争的高频世界里,就是生与死的区别。

协议 方向 特点 延迟
ITCH 行情接收(只读) 多播、增量更新、高吞吐 微秒级
OUCH 订单发送(读写) 点对点、轻量、低延迟 亚微秒级

多播数据解析:从网络流到结构化数据

美股行情用的是 UDP 多播。为什么不用 TCP?因为 TCP 的重传机制在高频场景下是灾难。你想想看,行情数据每秒几百万条更新,如果因为丢包就重传,网络瞬间就炸了。

多播的解析,第一步是处理 数据包分片。ITCH 协议的消息大小不一,但 UDP 包最大只有 1500 字节左右(MTU 限制)。一条大的 ITCH 消息,可能会被拆成多个 UDP 包发送。

我曾经踩过一个坑:某个 ITCH 消息的头部在前一个包,主体在后一个包,但两个包到达的顺序颠倒了。如果直接按顺序解析,就会得到一堆乱码。解决方案是维护一个 重组缓冲区,根据消息序列号来排序。

我的建议:使用环形缓冲区(Ring Buffer)来管理重组队列,避免内存碎片和 GC 停顿。

解析代码大致长这样:

// 伪代码:ITCH 消息解析
void onUdpPacket(byte[] packet) {
    // 1. 提取消息序列号
    uint64 seqNum = readUint64(packet, 0);
    
    // 2. 检查是否有序
    if (seqNum != expectedSeqNum) {
        // 丢包或乱序,放入重组缓冲区
        reassemblyBuffer.put(seqNum, packet);
        return;
    }
    
    // 3. 解析消息体
    MessageType type = (MessageType) packet[8];
    switch (type) {
        case ADD_ORDER:
            parseAddOrder(packet);
            break;
        case ORDER_EXECUTED:
            parseOrderExecuted(packet);
            break;
        // ... 其他消息类型
    }
    
    // 4. 处理重组缓冲区中积压的消息
    processReassemblyBuffer();
}

数据包重组:应对乱序和丢包

多播的痛点就是乱序和丢包。你无法保证所有数据包按顺序到达,也无法保证每个包都能到达。

我的做法是维护一个 滑动窗口。窗口大小根据历史丢包率动态调整,一般在 64 到 256 之间。窗口内的数据包按序列号排序,窗口外的直接丢弃——因为行情数据时效性极强,过时的数据没有意义。

注意:不要试图等待所有丢包都补回来。高频交易中,延迟比完整性更重要。错过一个包,直接跳过,用下一个包来修正状态。

重组逻辑的核心是:

  1. 收到新包,检查序列号是否在窗口内
  2. 如果在,插入到正确位置
  3. 从窗口起始位置开始,按顺序取出连续的消息
  4. 如果窗口起始位置缺失,等待超时或收到后续包后,直接跳过

嗯,这里要注意:超时时间不能太长。我一般设 100 微秒,超过这个时间还没等到,就认为这个包丢了。

快照与增量处理:维护实时订单簿

这是整个行情接入中最核心的部分。交易所会在每天开盘前发送一次 全量快照,告诉你当前所有价位的挂单情况。之后,就只发送增量更新。

你的任务就是:

  • 用快照初始化本地订单簿
  • 用增量更新实时维护订单簿状态
  • 定期(比如每 10 秒)请求一次新的快照,用于校正

为什么需要定期校正?因为增量更新可能会累积误差。比如你漏掉了一个撤单消息,那么本地订单簿里就会多出一笔不存在的挂单。时间长了,误差会越来越大。

我习惯用 红黑树 来维护订单簿的买卖盘口。每个价位是一个节点,节点内部用链表存储该价位的所有订单。这样,增删改查的时间复杂度都是 O(log n)。

避坑指南:我曾经因为忘记处理「订单被部分成交」的情况,导致订单簿里出现了小数股。后来加了一个校验逻辑:每次增量更新后,检查买卖盘口的总量是否与快照一致。

快照和增量的处理流程,我画了一张图:

快照与增量处理流程 接收全量快照 初始化本地订单簿 处理增量更新 更新订单簿状态 校验一致性 循环 定期请求新快照

图中可以看到,快照初始化后,进入增量处理循环。每次增量更新后,都要做一次一致性校验。如果发现异常,立即请求新的快照进行校正。

最后说一句:行情接入是整个高频交易系统的地基。地基不稳,上层建筑再漂亮也没用。我见过太多团队在策略上花了大把时间,结果因为行情数据延迟高了几个微秒,策略完全跑不出预期效果。所以,把行情接入做扎实,比什么都重要。

我的经验:在开发阶段,一定要录制一份真实的行情数据,用于回放测试。这样既能验证解析逻辑的正确性,又能模拟各种极端情况(比如网络抖动、数据包乱序)。

公众号:蓝海数据掘金营,微信deep3321