第四章:行情数据接入——FIX协议解析,WebSocket实时流处理,行情快照与增量更新,数据去重与对齐
做市商系统里,行情接入是命脉。你想想看,如果价格数据慢了10毫秒,可能一笔套利机会就溜走了。我个人习惯把行情模块称为“数据水龙头”——它必须拧开就有水,而且不能有气泡(脏数据)。
这一章,我们聊聊怎么搞定FIX协议、WebSocket流、快照与增量更新,以及最头疼的数据去重与对齐。嗯,这里要注意,很多新手容易把“快照”和“增量”搞混,我刚开始也踩过这个坑。
4.1 FIX协议解析:老牌金融协议,依然能打
FIX协议,说白了就是金融领域的“通用语”。它用键值对的方式传输数据,比如 35=D 表示新订单,55=600519 表示股票代码。虽然现在很多交易所开始推二进制协议,但FIX依然广泛存在于期货、期权、外汇市场。
我在项目中遇到过一个问题:某交易所的FIX网关,居然把价格字段 44 和 44.0 混着发。解析器如果没做容错,直接崩掉。所以,我建议你写FIX解析器时,一定要做字段类型宽松校验。
// C++ 示例:FIX消息快速解析
class FixParser {
public:
// 预编译标签映射
std::unordered_map<int, std::string> fields;
void parse(const std::string& msg) {
// 按SOH字符分割
size_t pos = 0;
while (pos < msg.size()) {
auto sep = msg.find('\x01', pos);
if (sep == std::string::npos) break;
auto pair = msg.substr(pos, sep - pos);
auto eq = pair.find('=');
if (eq != std::string::npos) {
int tag = std::stoi(pair.substr(0, eq));
fields[tag] = pair.substr(eq + 1);
}
pos = sep + 1;
}
}
};
4.2 WebSocket实时流处理:低延迟的现代选择
WebSocket比FIX更轻量,适合高频行情推送。交易所通常用WebSocket推送逐笔成交、深度快照、增量更新。我个人习惯把WebSocket客户端设计成“事件驱动”模式——收到消息后,直接触发回调,不阻塞主线程。
为什么会这样?因为WebSocket的帧是二进制或文本格式,解析速度直接影响行情延迟。我建议用Protobuf或FlatBuffers来编码,比JSON快一个数量级。
# Python 示例:WebSocket行情客户端
import asyncio
import websockets
class MarketDataClient:
def __init__(self, url):
self.url = url
self.callbacks = []
async def connect(self):
async with websockets.connect(self.url) as ws:
while True:
msg = await ws.recv()
# 解析二进制帧
data = self.decode(msg)
for cb in self.callbacks:
cb(data)
def decode(self, raw):
# 假设使用FlatBuffers
return flatbuffers_decode(raw)
4.3 行情快照与增量更新:如何拼出完整画面
交易所为了节省带宽,通常只推送“增量”数据——比如某档位价格变了,或者某笔成交了。但做市商需要完整的订单簿快照。所以,我们需要把快照和增量拼起来。
我记得第一次做这个时,直接拿增量覆盖快照,结果价格全乱了。后来才明白:增量更新必须基于“基准快照”,而且要有序列号校验。
| 数据类型 | 说明 | 处理方式 |
|---|---|---|
| 快照(Snapshot) | 当前时刻的完整订单簿 | 直接替换本地数据 |
| 增量(Incremental) | 某档位的价格/数量变化 | 基于快照进行加减操作 |
| 清空(Clear) | 重置某个档位 | 删除对应档位数据 |
// C++ 示例:订单簿增量更新
class OrderBook {
std::map<double, double> bids; // 价格 -> 数量
std::map<double, double> asks;
public:
void apply_snapshot(const Snapshot& snap) {
bids = snap.bids;
asks = snap.asks;
}
void apply_incremental(const Incremental& inc) {
if (inc.side == 'B') {
if (inc.quantity == 0) {
bids.erase(inc.price); // 清空档位
} else {
bids[inc.price] = inc.quantity; // 更新数量
}
} else {
// 类似处理卖单
}
}
};
4.4 数据去重与对齐:别让脏数据毁了策略
行情数据最怕两件事:重复和乱序。重复会导致策略重复下单,乱序会导致订单簿错乱。我建议用“时间戳+序列号”双重校验来去重。
你想想看,如果交易所同时推送了两次相同的增量,你的订单簿会多算一次数量。嗯,这里要注意,去重不能只靠时间戳,因为同一毫秒可能有多个事件。
// Python 示例:基于序列号的去重
class Deduplicator:
def __init__(self):
self.seen = set() # 存储已处理的序列号
def is_duplicate(self, seq_no):
if seq_no in self.seen:
return True
self.seen.add(seq_no)
# 限制集合大小,防止内存泄漏
if len(self.seen) > 100000:
self.seen.clear()
return False
数据对齐,说白了就是让不同来源的行情数据在时间轴上对齐。比如,你同时接了交易所A和交易所B的行情,它们的时钟可能差几毫秒。我建议用NTP同步服务器时间,然后以“交易所时间戳”为准,不要用本地接收时间。
4.5 知识体系总览
下面这张图,是我自己总结的行情接入模块架构。你可以看到,数据从FIX/WebSocket进来后,经过解析、去重、对齐,最终生成干净的订单簿快照,供策略使用。
这张图里,我特意把“去重与对齐”放在中间层。为什么?因为它是承上启下的关键。解析层出来的数据可能重复、乱序,必须经过这一层“净化”,才能交给订单簿。我见过不少团队,把去重逻辑写在策略里,结果策略代码又臭又长,还容易出bug。
好了,行情接入这块,核心就是这些。记住:FIX解析要快,WebSocket要稳,快照增量要准,去重对齐要狠。下一章,我们会聊订单管理——怎么把策略信号变成真正的订单,并跟踪它的生命周期。