第二章:行情数据接入层——交易所行情协议解析、多源数据聚合、行情去重与对齐、延迟优化策略

行情数据接入层,是整个做市商系统的“眼睛”。

眼睛要是瞎了,或者看东西重影、延迟,那后面的策略再牛也没用。我个人习惯把这一层叫做“数据底座”,它稳不稳,直接决定了你的做市机器人能不能赚钱。

2.1 交易所行情协议解析:从二进制到Tick

每家交易所都有自己的“方言”。

深交所用STEP协议,上交所用FAST协议,币圈交易所更是五花八门——WebSocket JSON、Protobuf、甚至自己魔改的二进制流。你想想看,一个做市商系统可能要同时对接5到10家交易所,每家协议都得单独写解析器。

核心难点在哪?

  • 粘包与半包:TCP是流式协议,你收到的可能是一截数据,也可能是好几条消息粘在一起。我刚开始做的时候,就因为这个bug,行情界面跳得跟心电图似的。
  • 字节序与对齐:有的交易所用大端,有的用小端。结构体里还有内存对齐的坑。我记得有一次,币安升级了协议,把某个字段从4字节改成了8字节,我这边没及时更新,直接导致行情解析全乱套。
  • 增量与快照:很多交易所为了省带宽,只发增量数据。你得维护一个“订单簿快照”,然后不断应用增量。这里最容易出问题的是“增量丢失”——一旦丢了一条,整个订单簿就歪了。

避坑指南:我曾经在对接某期货交易所时,发现他们的增量序列号不是连续的,而是每隔一段时间重置一次。当时没注意,结果订单簿深度差了整整10个tick。后来我加了一个“序列号连续性检测”模块,一旦发现跳跃超过阈值,立即请求全量快照重建。

下面是一个简化的STEP协议解析示例,用C++写的,核心就是处理粘包和字节序:

// 伪代码:STEP协议解析器
class StepParser {
public:
    // 每次收到数据,往缓冲区里塞
    void Feed(const char* data, size_t len) {
        buffer_.insert(buffer_.end(), data, data + len);
        TryParse();
    }

private:
    void TryParse() {
        while (buffer_.size() >= kHeaderSize) {
            // 1. 读取消息长度(大端4字节)
            uint32_t msg_len = ntohl(*(uint32_t*)buffer_.data());
            if (buffer_.size() < msg_len) break;  // 半包,等下一波

            // 2. 解析消息体
            ParseMessage(buffer_.data(), msg_len);

            // 3. 移除已处理的数据
            buffer_.erase(buffer_.begin(), buffer_.begin() + msg_len);
        }
    }

    std::vector<char> buffer_;
};

2.2 多源数据聚合:把不同交易所的“方言”翻译成普通话

每家交易所的行情格式不一样,但你的策略只需要一种统一的“内部Tick”。

所以,我们需要一个聚合层。说白了,就是把不同交易所的原始行情,转换成统一的内部数据结构。比如,币安的“asks”和“bids”是数组,而OKX的是对象,你得把它们都映射成统一的OrderBookLevel

我建议的做法是:

  • 定义一套内部行情协议(IDL),用Protobuf或者FlatBuffers。
  • 每个交易所写一个“适配器”(Adapter),负责把原始协议转成内部协议。
  • 适配器里做字段映射、单位转换(比如有的交易所价格是整数,有的是浮点数)。

个人经验:我习惯在适配器里加一个“校验层”。比如,检查价格是否为正数、数量是否合理、时间戳是否在合理范围内。曾经有一次,某交易所的行情服务器出了bug,发了一个价格为0的卖单,要不是校验层挡住了,策略就直接按0价成交了——那画面太美我不敢看。

2.3 行情去重与对齐:别让策略“看花眼”

多源数据聚合之后,你可能会收到重复的行情。为什么?

  • 同一个交易所,你同时订阅了WebSocket和REST接口,两边都推送了同样的数据。
  • 交易所的网关做了冗余推送,防止丢包。
  • 你的系统内部,多个消费者订阅了同一个Topic,导致重复消费。

去重其实不难,核心就是“唯一标识”。我通常用(交易所ID + 行情序列号 + 时间戳)作为key,用一个LRU缓存来去重。缓存大小设个100万条,超过就淘汰最老的。

对齐呢? 这个更关键。

不同交易所的行情到达时间不一样。比如,币安的行情延迟10ms,OKX的延迟20ms。如果你直接拿两个不同时刻的订单簿去做套利,那算出来的价差全是错的。

我常用的对齐策略是“时间戳对齐”:

  • 每个行情都带上交易所的原始时间戳(不是本地接收时间)。
  • 对齐时,取同一毫秒(或同一微秒)内的行情做快照。
  • 如果某个交易所的行情在这一毫秒内没到,就用上一毫秒的快照“填充”。

注意:千万不要用本地接收时间做对齐!因为网络延迟是不稳定的。我曾经见过一个团队,用本地时间对齐,结果两个交易所的行情差了50ms,策略一直在做“假套利”,亏了不少钱。

2.4 延迟优化策略:每一微秒都很重要

做市商是“微秒级”的战争。行情晚到1ms,可能就抢不到单了。

延迟优化,说白了就是“压榨硬件和软件的所有潜力”。我总结了几条实战经验:

  1. 内核旁路(Kernel Bypass):用DPDK或者Solarflare的OpenOnload,跳过内核协议栈,直接从网卡拿数据。延迟能从几十微秒降到几微秒。
  2. 锁优化:行情处理路径上,尽量用无锁数据结构(比如Lock-Free Queue)。我见过一个系统,因为用了std::mutex,延迟抖动从5微秒变成了50微秒。
  3. 内存池:避免频繁的new/delete。行情数据量巨大,每次分配内存都会触发系统调用。用预分配的内存池,能省下不少时间。
  4. CPU亲和性:把行情处理线程绑定到固定的CPU核心上,避免上下文切换。我习惯把网卡中断也绑定到同一个核心,这样数据从网卡到应用层,全程不换核。
  5. 批处理:不要来一条行情就处理一条。攒一批(比如100条),一次性处理。这样能充分利用CPU的缓存和指令流水线。

我曾经踩过的坑:有一回,我为了追求极致延迟,把所有行情处理都放在一个线程里。结果行情量一上来,这个线程成了瓶颈,延迟反而飙升。后来我改成“按交易所分线程”,每个交易所一个独立线程,延迟立马降下来了。嗯,这里要注意:多线程不是越多越好,关键是避免锁竞争。

2.5 整体架构图

下面这张图,是我自己项目里用的行情接入层架构。你可以看到,数据从交易所进来,经过协议解析、聚合、去重对齐,最后统一输出给策略层。

行情数据接入层架构图 交易所A (STEP协议) 交易所B (FAST协议) 交易所C (WebSocket) 协议解析层 (粘包处理 / 字节序转换 / 增量合并) StepParser | FastParser | WsParser 多源聚合 & 去重对齐 统一内部Tick | LRU去重 | 时间戳对齐 延迟优化 (内核旁路 / 无锁队列 / 内存池 / CPU亲和性) DPDK | Lock-Free Queue | Memory Pool 输出给策略层 (统一行情流)

2.6 小结

行情数据接入层,说白了就是“快、准、稳”。

  • :延迟优化做到极致,每一微秒都要争。
  • :去重对齐不能出错,否则策略看到的行情是“假”的。
  • :协议解析要健壮,不能因为一条坏数据就崩掉整个系统。

我个人觉得,这一层是最容易被忽视的。很多人一上来就写策略,结果行情数据都接不稳,策略再牛也是空中楼阁。嗯,先把地基打牢,后面的事就好办了。

专注资料整理