第二章:行情数据接入层——交易所行情协议解析、多源数据聚合、行情去重与对齐、延迟优化策略
行情数据接入层,是整个做市商系统的“眼睛”。
眼睛要是瞎了,或者看东西重影、延迟,那后面的策略再牛也没用。我个人习惯把这一层叫做“数据底座”,它稳不稳,直接决定了你的做市机器人能不能赚钱。
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,可能就抢不到单了。
延迟优化,说白了就是“压榨硬件和软件的所有潜力”。我总结了几条实战经验:
- 内核旁路(Kernel Bypass):用DPDK或者Solarflare的OpenOnload,跳过内核协议栈,直接从网卡拿数据。延迟能从几十微秒降到几微秒。
- 锁优化:行情处理路径上,尽量用无锁数据结构(比如Lock-Free Queue)。我见过一个系统,因为用了
std::mutex,延迟抖动从5微秒变成了50微秒。 - 内存池:避免频繁的
new/delete。行情数据量巨大,每次分配内存都会触发系统调用。用预分配的内存池,能省下不少时间。 - CPU亲和性:把行情处理线程绑定到固定的CPU核心上,避免上下文切换。我习惯把网卡中断也绑定到同一个核心,这样数据从网卡到应用层,全程不换核。
- 批处理:不要来一条行情就处理一条。攒一批(比如100条),一次性处理。这样能充分利用CPU的缓存和指令流水线。
我曾经踩过的坑:有一回,我为了追求极致延迟,把所有行情处理都放在一个线程里。结果行情量一上来,这个线程成了瓶颈,延迟反而飙升。后来我改成“按交易所分线程”,每个交易所一个独立线程,延迟立马降下来了。嗯,这里要注意:多线程不是越多越好,关键是避免锁竞争。
2.5 整体架构图
下面这张图,是我自己项目里用的行情接入层架构。你可以看到,数据从交易所进来,经过协议解析、聚合、去重对齐,最后统一输出给策略层。
2.6 小结
行情数据接入层,说白了就是“快、准、稳”。
- 快:延迟优化做到极致,每一微秒都要争。
- 准:去重对齐不能出错,否则策略看到的行情是“假”的。
- 稳:协议解析要健壮,不能因为一条坏数据就崩掉整个系统。
我个人觉得,这一层是最容易被忽视的。很多人一上来就写策略,结果行情数据都接不稳,策略再牛也是空中楼阁。嗯,先把地基打牢,后面的事就好办了。