3. 行情数据接入层设计:交易所行情协议解析、多数据源聚合、行情去重与对齐、延迟优化策略

做市系统里,行情接入层是真正的「前线」。

我见过太多团队,策略模型跑得飞起,结果行情一进来就卡住。说白了,行情数据如果接不好,后面全是白搭。今天咱们就聊聊这块怎么设计。

3.1 交易所行情协议解析

每家交易所都有自己的「方言」。上交所用STEP,深交所用SZV2,中金所用FAST,还有CTP、XTP……你得给每套协议配一个翻译官。

核心原则:协议解析层必须独立,不能跟业务逻辑混在一起。

我个人习惯的做法是:

  • 每个协议写一个独立的Decoder模块
  • Decoder只负责把二进制流变成统一的内存对象
  • 统一对象叫 MarketDataPacket,包含时间戳、合约代码、买卖盘口、最新价等字段
// 伪代码示例:统一行情对象
struct MarketDataPacket {
    uint64_t    timestamp_ns;   // 纳秒时间戳
    char        symbol[32];     // 合约代码
    double      last_price;     // 最新价
    double      bid_prices[10]; // 买盘价格
    double      ask_prices[10]; // 卖盘价格
    uint32_t    bid_volumes[10];// 买盘量
    uint32_t    ask_volumes[10];// 卖盘量
    uint8_t     source_id;      // 数据源标识
};

嗯,这里要注意:时间戳一定要用纳秒。我在项目中遇到过,有些交易所给的是微秒,有些是毫秒,统一转成纳秒后做对齐会省很多麻烦。

3.2 多数据源聚合

做市商一般不会只接一个数据源。你想想看,万一交易所主线路断了怎么办?所以通常会同时接两路甚至三路行情。

多数据源聚合,说白了就是「谁先到用谁,谁准用谁」。

我的经验:不要搞复杂的投票算法。简单点,主备模式就够了。主数据源正常时就用它,主数据源异常时切到备用。切换逻辑要快,毫秒级。

我曾经踩过一个坑:两个数据源同时推送,我搞了个加权平均。结果行情波动大的时候,加权出来的价格两边都不对,策略直接亏钱。后来改成「主备+心跳检测」,稳多了。

聚合层的设计要点:

  • 每个数据源独立线程接收,互不阻塞
  • 维护一个数据源状态表,记录延迟、丢包率、序列号连续性
  • 状态异常时自动切换,并发出告警

3.3 行情去重与对齐

多数据源必然带来重复数据。同一个tick,可能从A路和B路各来一次。如果不做去重,策略会以为市场变了两次,实际上只变了一次。

去重的关键是什么?

序列号 + 时间戳 + 价格快照。

我建议的做法:

  1. 每个数据源维护一个最近N条记录的环形缓冲区
  2. 新数据进来时,用(序列号, 数据源ID)做哈希,查重
  3. 如果重复,直接丢弃
  4. 如果不重复,再检查时间戳是否乱序

注意:行情对齐不是简单的时间戳相等。不同数据源的网络延迟不同,同一个tick可能差几毫秒到。对齐时要用「最近邻匹配」——找时间戳最接近的那条,而不是完全相等的那条。

我做过一个测试:用严格相等做对齐,丢包率高达3%。改用最近邻匹配后,丢包率降到0.1%以下。差距非常大。

3.4 延迟优化策略

做市对延迟极其敏感。你比别人慢1微秒,可能就抢不到单了。

延迟优化的核心思路就四个字:减少拷贝

我常用的优化手段:

优化手段 说明 效果
零拷贝技术 网卡数据直接映射到用户态,绕过内核 延迟降低40%-60%
内存池 预分配行情对象,避免频繁new/delete 延迟抖动减少80%
CPU亲和性 行情线程绑定到特定CPU核心 缓存命中率提升30%
无锁队列 用RingBuffer替代互斥锁 吞吐量提升5-10倍

举个例子,零拷贝这块。传统做法是:网卡→内核缓冲区→用户态缓冲区→应用。每次拷贝都有开销。用DPDK或者Solarflare的OpenOnload,可以直接把数据从网卡送到应用内存,中间不经过内核。嗯,这个配置起来有点麻烦,但效果是真的好。

一个小技巧:行情线程的优先级要设高,但不能最高。我曾经把行情线程设成实时优先级,结果跟系统关键线程抢CPU,直接把机器搞挂了。设成高优先级但留一点余量,比较稳妥。

3.5 整体架构图

下面这张图是我个人比较推荐的行情接入层架构。你看一眼就明白了。

行情数据接入层架构图 数据源层 上交所 STEP 深交所 SZV2 中金所 FAST 备用数据源 协议解析层 STEP Decoder SZV2 Decoder FAST Decoder 备用 Decoder 聚合与去重层 主备切换 | 序列号去重 | 时间戳对齐 延迟优化层 零拷贝 | 内存池 | CPU亲和性 | 无锁队列

从下往上看:数据源进来,经过协议解析变成统一格式,再到聚合层做去重和对齐,最后经过延迟优化层送给策略。每一层各司其职,互不干扰。

3.6 避坑指南

最后分享几个我踩过的坑:

  • 我曾经以为行情线程越多越好。结果CPU上下文切换开销比处理行情本身还大。后来发现,2-3个行情线程就够了,多了反而坏事。
  • 我曾经忽略过内存对齐。行情对象里的字段没按64字节缓存行对齐,导致多核访问时频繁缓存失效。改了对齐后,性能提升了15%。
  • 我曾经在行情处理里打了太多日志。一次行情波动,日志直接写爆了磁盘。现在我的做法是:正常行情不记日志,异常情况才记,而且用异步日志。

行情接入层看起来简单,但细节非常多。你想想看,每天几百万笔行情,每一笔都要在微秒级处理完,任何一个环节出问题都会放大。把基础打牢,后面才能跑得快。

一句话总结:行情接入层的设计目标就是「快、稳、准」。快靠零拷贝和无锁队列,稳靠主备切换和心跳检测,准靠去重和对齐。三者缺一不可。


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