第四章:行情数据接入——FIX协议解析,WebSocket实时流处理,行情快照与增量更新,数据去重与对齐

做市商系统里,行情接入是命脉。你想想看,如果价格数据慢了10毫秒,可能一笔套利机会就溜走了。我个人习惯把行情模块称为“数据水龙头”——它必须拧开就有水,而且不能有气泡(脏数据)。

这一章,我们聊聊怎么搞定FIX协议、WebSocket流、快照与增量更新,以及最头疼的数据去重与对齐。嗯,这里要注意,很多新手容易把“快照”和“增量”搞混,我刚开始也踩过这个坑。

4.1 FIX协议解析:老牌金融协议,依然能打

FIX协议,说白了就是金融领域的“通用语”。它用键值对的方式传输数据,比如 35=D 表示新订单,55=600519 表示股票代码。虽然现在很多交易所开始推二进制协议,但FIX依然广泛存在于期货、期权、外汇市场。

我在项目中遇到过一个问题:某交易所的FIX网关,居然把价格字段 4444.0 混着发。解析器如果没做容错,直接崩掉。所以,我建议你写FIX解析器时,一定要做字段类型宽松校验。

核心要点: 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;
        }
    }
};
避坑指南: 我曾经因为没处理FIX消息中的“重复组”(比如多条订单信息),导致行情数据错乱。记得用递归或栈来解析嵌套结构。

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)
注意: WebSocket连接可能断开。一定要实现自动重连机制,并且重连后要重新订阅行情。我见过一个系统,断连后没重订阅,结果空跑了一整天。

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 数据去重与对齐:别让脏数据毁了策略

行情数据最怕两件事:重复和乱序。重复会导致策略重复下单,乱序会导致订单簿错乱。我建议用“时间戳+序列号”双重校验来去重。

你想想看,如果交易所同时推送了两次相同的增量,你的订单簿会多算一次数量。嗯,这里要注意,去重不能只靠时间戳,因为同一毫秒可能有多个事件。

个人经验: 我曾经用Redis的Set来去重,但延迟太高。后来改用内存中的环形缓冲区(Ring Buffer),配合位图标记,性能提升10倍。
// 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同步服务器时间,然后以“交易所时间戳”为准,不要用本地接收时间。

警告: 如果两个交易所的行情时间戳相差超过50毫秒,建议丢弃较晚的数据。否则,你的做市策略会基于错误的时间关系做出决策。

4.5 知识体系总览

下面这张图,是我自己总结的行情接入模块架构。你可以看到,数据从FIX/WebSocket进来后,经过解析、去重、对齐,最终生成干净的订单簿快照,供策略使用。

行情数据接入模块架构 FIX协议 WebSocket 协议解析(FIX/Protobuf) 去重 & 对齐(序列号+时间戳) 订单簿快照(Bid/Ask) 策略引擎 数据流方向:左→右,上→下 每个模块可独立扩展

这张图里,我特意把“去重与对齐”放在中间层。为什么?因为它是承上启下的关键。解析层出来的数据可能重复、乱序,必须经过这一层“净化”,才能交给订单簿。我见过不少团队,把去重逻辑写在策略里,结果策略代码又臭又长,还容易出bug。

好了,行情接入这块,核心就是这些。记住:FIX解析要快,WebSocket要稳,快照增量要准,去重对齐要狠。下一章,我们会聊订单管理——怎么把策略信号变成真正的订单,并跟踪它的生命周期。


专注资料整理