4. 订单簿快照与增量:全量快照与增量更新的机制

做量化交易的朋友都知道,订单簿是市场的“心跳”。但怎么把这个心跳实时、准确地传到你的策略里,是个技术活。

说白了,有两种思路:全量快照增量更新。一个简单粗暴,一个精打细算。今天我就把这两个家伙的底裤扒干净。

4.1 全量快照:简单但“重”

全量快照,就是交易所每隔一段时间,把整个订单簿的买盘和卖盘,一股脑全发给你。

优点很明显:

  • 简单可靠:你不需要维护任何状态。收到快照,直接覆盖本地数据就行。
  • 不怕丢数据:哪怕中间断线了,重新连上,拿一份新快照,一切恢复如初。

缺点也很致命:

  • 数据量大:一个深度5档的快照可能只有几KB,但如果是100档,甚至全量深度,一次快照可能几百KB甚至几MB。
  • 延迟高:高频场景下,每秒发10次快照,带宽和CPU都扛不住。

核心要点:全量快照适合低频场景,或者作为“基线”数据。我见过不少新手直接用全量快照做高频策略,结果网络延迟直接拉满。

4.2 增量更新:轻量但“精”

增量更新,只告诉你“变了什么”。比如:“卖一价从100.5变成100.6,数量从100变成80”。

你本地维护一个订单簿,收到增量消息后,逐条应用。这样,每次传输的数据量极小,延迟也低。

增量更新的核心数据结构:

// 增量消息示例(JSON格式)
{
    "type": "incremental",
    "timestamp": 1678901234567,
    "bids": [
        [100.5, 0],   // 价格100.5,数量0表示删除这个价位
        [100.3, 200]  // 价格100.3,数量200表示新增或更新
    ],
    "asks": [
        [101.0, 150],
        [101.2, 0]    // 删除卖二价位
    ]
}

你想想看,如果每秒有1000笔成交,用全量快照你得传1000次完整订单簿。但用增量更新,每次只传几个字节的变动数据。效率差距是数量级的。

我的经验:我在做币安行情接入时,发现增量更新有个坑——序列号。每个增量消息必须带一个递增的序列号。如果收到序列号不连续,说明中间丢包了,必须立刻请求一次全量快照来同步。

4.3 快照与增量的协同工作

实际生产中,两者是配合使用的。我习惯用“快照+增量流”的模式:

  1. 初始化:先请求一次全量快照,建立本地订单簿。
  2. 实时更新:持续接收增量消息,逐条更新本地订单簿。
  3. 定期校验:每隔一段时间(比如5分钟),或者发现增量序列号不连续时,重新请求一次全量快照,覆盖本地数据。

这样做的好处是:平时用增量保证低延迟,偶尔用快照保证数据一致性。

注意:千万不要在收到增量消息后,直接丢弃本地数据。我曾经犯过这个错——以为增量消息是“完整状态”,结果本地订单簿越跑越偏,最后策略亏了一笔大的。

4.4 核心逻辑流程图

下面这张图,是我自己画的一个简化流程。你看一眼就明白了:

订单簿快照与增量协同流程 开始连接 请求全量快照 建立本地订单簿 接收增量消息 序列号连续? 更新本地订单簿 重新请求快照

4.5 实战中的避坑指南

嗯,这里我要多说几句。我在对接多个交易所时,发现几个共性问题:

  • 快照和增量的时间戳要对齐:有些交易所的快照时间戳和增量时间戳不是同一个时钟源。你收到快照后,如果增量消息的时间戳比快照还早,直接丢弃。
  • 增量消息的“删除”操作要小心:有些交易所用数量为0表示删除价位,有些用负数量,有些用单独的“delete”字段。一定要看文档,别想当然。
  • 内存管理:本地订单簿如果维护全量深度,内存占用可能很大。我建议用红黑树或跳表来存储,查找和更新都是O(log n)的复杂度。

一句话总结:全量快照是“基线”,增量更新是“补丁”。两者结合,才能既快又准。

4.6 代码示例:增量更新核心逻辑

最后,给一个简单的Python示例。这不是完整代码,但核心逻辑都在里面了:

class OrderBook:
    def __init__(self):
        self.bids = {}  # 价格 -> 数量
        self.asks = {}
        self.last_seq = 0
    
    def apply_snapshot(self, snapshot):
        """应用全量快照"""
        self.bids = {price: qty for price, qty in snapshot['bids']}
        self.asks = {price: qty for price, qty in snapshot['asks']}
        self.last_seq = snapshot['seq']
    
    def apply_incremental(self, inc):
        """应用增量更新"""
        # 检查序列号
        if inc['seq'] != self.last_seq + 1:
            # 丢包了!需要重新请求快照
            raise Exception(f"序列号不连续: 期望{self.last_seq+1}, 收到{inc['seq']}")
        
        # 更新买盘
        for price, qty in inc['bids']:
            if qty == 0:
                self.bids.pop(price, None)  # 删除价位
            else:
                self.bids[price] = qty      # 新增或更新
        
        # 更新卖盘
        for price, qty in inc['asks']:
            if qty == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = qty
        
        self.last_seq = inc['seq']

你看,核心逻辑就这么几行。但实际生产环境里,还要处理网络重连、数据校验、性能优化等等。嗯,那些就是后面章节的内容了。

小技巧:如果你用C++做低延迟交易,建议用内存池来管理订单簿节点,避免频繁malloc/free。我在一个项目中,光这个优化就降低了30%的延迟。


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