2. 行情接入层设计:多交易所行情协议适配
行情接入层,说白了就是系统的「眼睛」。眼睛要是瞎了,后面策略写得再漂亮也是白搭。我做了这么多年量化,见过太多团队在行情接入上栽跟头——有的数据延迟高,有的断连了没人知道,最惨的是接了脏数据导致策略反向开仓。
今天咱们就聊聊,怎么把这层设计得既稳又准。
2.1 多交易所协议适配:FIX / WebSocket / REST
不同交易所用的协议不一样,你得有个统一的接入框架。我个人习惯用「协议适配器模式」——每个交易所一个适配器,对外暴露统一接口。
FIX 协议接入
FIX 协议在传统金融圈用得最多,期货、外汇、股票都认它。但说实话,FIX 的报文格式挺啰嗦的,一个行情消息能拆成几十个字段。
我在项目中遇到过一个问题:某交易所的 FIX 网关每隔 30 秒会发一个 TestRequest,如果你没在规定时间内回 Heartbeat,它就直接断开连接。嗯,这个坑我踩过,后来在适配器里加了个定时器,专门处理心跳。
// FIX 适配器核心逻辑(伪代码)
class FixAdapter : public MarketAdapter {
void onMessage(const string& rawMsg) {
FixMessage msg = FixParser::parse(rawMsg);
if (msg.type == MARKET_DATA_SNAPSHOT) {
// 转成统一行情结构
MarketData md = convert(msg);
dispatch(md);
} else if (msg.type == HEARTBEAT) {
sendHeartbeat(); // 必须及时回复
}
}
};
WebSocket 接入
WebSocket 现在是数字货币交易所的主流选择。它比 FIX 轻量,但有个问题——断连重连你得自己处理。
你想想看,行情数据每秒几十上百条,一旦断连,中间的数据就丢了。我建议的做法是:
- 维护一个心跳计数器,超过 N 秒没收到数据就主动重连
- 重连后请求一次快照,把缺失的增量数据补回来
- 用指数退避策略,别一断就连,连不上又断,把服务器搞崩了
REST 接口接入
REST 一般用来拿历史数据或者做备用通道。实时行情用 REST 轮询?说实话,延迟太高了,不推荐。
但 REST 有个好处——它是最简单的兜底方案。如果 WebSocket 挂了,你可以临时切到 REST 顶一阵子。我曾经在某个交易所的 WebSocket 连续断连 3 次后,自动切到 REST 模式,虽然延迟多了 200ms,但至少没断数据。
2.2 行情数据标准化
不同交易所的行情字段名千奇百怪。有的叫 lastPrice,有的叫 close,还有的叫 tradePrice。你总不能每个策略都去适配一遍吧?
所以,标准化是必须的。我一般定义一套统一的数据结构:
struct MarketData {
string symbol; // 统一品种代码,如 "BTCUSDT"
double bidPrice; // 买一价
double askPrice; // 卖一价
double lastPrice; // 最新成交价
double volume; // 成交量
int64_t timestamp; // 统一用毫秒时间戳
// ... 其他字段
};
每个适配器在收到原始数据后,先转成这个结构再往上抛。这样上层策略、风控、回测模块都不用关心底层是哪个交易所。
2.3 行情去重与缓存策略
行情数据有个特点——同一笔成交可能被推送多次。比如交易所的撮合引擎先发了个增量更新,紧接着又发了个快照,里面包含了同样的数据。如果你不做去重,策略可能会重复计算,导致信号错乱。
去重怎么做?
我常用的方法是「序列号 + 时间戳」双重校验:
- 每个行情消息带一个递增的序列号,重复的序列号直接丢弃
- 如果序列号不连续,说明中间有丢包,需要触发补数据流程
- 时间戳作为辅助校验,防止序列号回绕
bool isDuplicate(const MarketData& md) {
static int64_t lastSeq = 0;
if (md.seqNum <= lastSeq) {
return true; // 重复数据
}
if (md.seqNum > lastSeq + 1) {
// 丢包了!触发补数据
requestSnapshot(md.symbol);
}
lastSeq = md.seqNum;
return false;
}
缓存策略
行情缓存的目的有两个:一是给策略提供「当前最新行情」的快速查询,二是给断连重连后补数据用。
我一般用两级缓存:
| 缓存层级 | 存储内容 | 更新频率 | 用途 |
|---|---|---|---|
| L1 内存缓存 | 最新一笔行情 | 每次推送更新 | 策略实时查询 |
| L2 环形缓冲区 | 最近 N 笔行情 | 滚动覆盖 | 断连补数据、回放 |
L1 缓存用 std::unordered_map<string, MarketData> 就行,key 是品种代码。L2 缓存我建议用环形缓冲区,每个品种维护一个固定大小的数组,比如存最近 1000 笔。为什么用环形?因为行情数据量太大,用链表或者 vector 频繁分配内存,GC 会让你哭的。
我曾经在一个高频项目里,L2 缓存用了 std::deque,结果跑了一天内存涨了 2 个 G。后来换成环形缓冲区,内存稳稳的。
2.4 整体架构图
下面这张图展示了行情接入层的核心流程。从左边多个交易所进来,经过协议适配、去重、标准化,最后统一输出给上层使用。
从图上你能看到,数据从左到右流经三个核心模块。每个模块各司其职,互不干扰。这也是我设计系统的一贯原则——高内聚、低耦合。