第1章:行情数据接入层设计
行情数据接入层,说白了就是量化交易系统的「耳朵」和「眼睛」。
我做了这么多年量化系统,见过太多团队在行情接入上栽跟头。有的数据延迟高,有的丢包严重,还有的因为协议解析不对直接崩了。嗯,今天我们就来聊聊这块到底该怎么设计。
1.1 交易所协议解析
每家交易所都有自己的「方言」。上交所用STEP协议,深交所用SZSE MDDP,期货交易所用CTP。你想想看,如果系统要同时接入多个市场,协议解析这块就是个硬骨头。
我个人习惯的做法是:抽象一层统一的行情数据模型。不管底层是什么协议,解析完后都转成内部标准格式。
核心思路:协议解析层只做一件事——把二进制流变成结构体。
// 内部统一行情结构体示例
struct MarketData {
uint64_t timestamp; // 纳秒级时间戳
char symbol[32]; // 合约代码
double last_price; // 最新价
uint64_t volume; // 成交量
double bid_price[5]; // 买一到买五
double ask_price[5]; // 卖一到卖五
uint32_t bid_vol[5];
uint32_t ask_vol[5];
};
我在项目中遇到过一个问题:某家交易所的行情快照和逐笔成交是分开推送的。如果直接按原始协议处理,很容易出现价格和成交量对不上的情况。后来我们加了个「时间窗口对齐」的逻辑,才彻底解决。
避坑指南:我曾经因为没处理好协议中的「心跳包」,导致系统误判交易所断连,触发了自动切换。后来加了个心跳超时计数器,连续3次无响应才切换。
1.2 行情网关架构
行情网关,你可以把它理解成一个「翻译官+快递员」。它负责从交易所拉数据,翻译成内部格式,再分发给各个业务模块。
我推荐用事件驱动+异步IO的架构。为什么?因为行情数据是流式的,用同步阻塞的方式处理,延迟会高得离谱。
上图展示的就是行情网关的经典三层架构。每一层各司其职,互不干扰。
注意:网关层一定要做「熔断保护」。如果某个交易所的行情突然异常(比如数据量暴增),要能自动切断连接,防止拖垮整个系统。
1.3 多数据源聚合与去重
做量化交易的朋友都知道,同一个合约可能从多个数据源收到行情。比如CTP和飞马都推送螺纹钢的数据。这时候就面临两个问题:
- 去重:同一笔成交,不能重复计算
- 聚合:不同数据源的数据,要合并成一条完整记录
我常用的去重策略是「时间戳+交易所序列号」双字段校验。为什么不用单一字段?因为不同交易所的时间戳精度不一样,有的到毫秒,有的到微秒。加上序列号就能唯一确定一笔数据。
// 去重逻辑伪代码
bool is_duplicate(MarketData& data) {
string key = data.symbol + "_" +
to_string(data.timestamp) + "_" +
to_string(data.exchange_seq);
if (seen_keys.find(key) != seen_keys.end()) {
return true; // 已处理过
}
seen_keys.insert(key);
return false;
}
经验之谈:我曾经遇到过一个坑——某家交易所的序列号会定期重置。后来我们加了个「时间窗口」机制,只保留最近5秒的序列号缓存,完美解决。
聚合的逻辑相对简单:以最快的数据源为主,用其他数据源做补充。比如A源推送了最新价,B源推送了成交量,那就把两者合并。
1.4 低延迟数据分发
数据解析完了,去重聚合也做了,接下来就是分发给各个业务模块。这里有个关键问题:怎么保证延迟最低?
我推荐用共享内存+无锁队列的方案。为什么不用TCP?因为TCP有协议栈开销,延迟至少多几十微秒。在量化交易里,这几十微秒可能就是几百万的盈亏。
| 分发方式 | 延迟(微秒) | 适用场景 |
|---|---|---|
| 共享内存+无锁队列 | < 1 | 同机进程间通信 |
| Unix Domain Socket | 2-5 | 同机进程间通信 |
| TCP Socket | 10-50 | 跨机通信 |
| RDMA | < 1 | 跨机通信(需硬件支持) |
你看这个表格就明白了。同机通信,共享内存是首选。跨机通信,如果预算够,上RDMA;预算有限,用TCP也凑合。
核心原则:数据分发路径上,每多一次拷贝,就多一份延迟。尽量用「零拷贝」技术。
嗯,说到零拷贝,我记得有一次优化行情分发,把数据从内核态拷贝到用户态这一步就花了3微秒。后来用mmap映射共享内存,直接省掉了拷贝,延迟降到了0.5微秒以下。
最后提一句:分发要支持「订阅模式」。不是所有模块都需要全部行情。比如套利模块只需要价差合约,CTA模块只需要主力合约。按需订阅,能大幅降低系统负载。