2. 订单簿快照与增量更新:Level 1/Level 2/Level 3数据、快照与增量机制、数据合并与对齐

做高频交易的朋友都知道,订单簿就是市场的「心跳」。你盯盘时看到的那些跳动的价格和挂单量,背后其实有一套非常精巧的数据机制在支撑。今天咱们就来聊聊这个核心话题——订单簿的快照与增量更新。

2.1 数据层级:Level 1 / Level 2 / Level 3

先说说数据层级。我刚开始接触这个领域时,也被这些Level搞得有点晕。其实说白了,它们就是不同深度的市场数据。

层级 包含内容 典型用途
Level 1 最优买卖价(Best Bid/Offer) 散户看盘、简单策略
Level 2 前N档买卖盘口(通常5-10档) 量化交易、盘口分析
Level 3 全量订单簿(所有挂单) 高频做市、套利策略

Level 1 是最基础的。它只告诉你当前市场上最好的买价和卖价。比如BTC/USDT,买一价30000,卖一价30001。嗯,就这么简单。我见过不少新手以为这就是全部,其实差远了。

Level 2 就丰富多了。它会展示前几档的买卖挂单。比如买一到买五,卖一到卖五,每档的价格和数量都清清楚楚。我个人习惯用Level 2数据做盘口压力分析,效果还不错。

Level 3 是最高权限的数据。它能看到整个订单簿上所有的挂单,包括那些离当前价格很远的单子。说实话,能拿到Level 3数据的交易所不多,而且费用不菲。我在做高频做市策略时,曾经花了大价钱接入过某交易所的Level 3数据流,那体验确实不一样。

核心要点:Level越高,数据越全,延迟越低,但成本也越高。选哪个层级,取决于你的策略需求和预算。

2.2 快照与增量机制

好,现在聊聊数据是怎么传输的。你想想看,如果每次订单簿变化都把整个订单簿发一遍,那网络带宽得有多大?所以交易所采用了「快照+增量」的模式。

快照(Snapshot):就是某一时刻订单簿的完整状态。它像一张照片,定格了那个瞬间的所有挂单。

增量(Incremental Update):就是后续的变化。比如有人下了一笔买单,或者撤了一笔卖单,交易所只发送这个变化信息。

我举个例子你就明白了:

// 初始快照
{
  "type": "snapshot",
  "timestamp": 1628000000000,
  "bids": [[30000, 1.5], [29990, 2.0]],
  "asks": [[30010, 1.2], [30020, 0.8]]
}

// 后续增量更新
{
  "type": "update",
  "timestamp": 1628000001000,
  "changes": [
    {"side": "bid", "price": 30000, "size": 0},  // 撤单
    {"side": "ask", "price": 30005, "size": 0.5} // 新挂单
  ]
}

这里要注意,增量更新中的 size: 0 表示删除该价格档位。这是交易所的通用约定,我曾经踩过这个坑——一开始以为size为0是没变化,结果订单簿越积越乱。

避坑指南:我曾经在对接某二线交易所时,发现它的增量更新里偶尔会漏掉一些变化。后来我加了一个校验机制:每收到1000条增量后,主动请求一次快照做对比。这个习惯我一直保留到现在。

2.3 数据合并与对齐

拿到快照和增量后,怎么把它们合并成实时订单簿?这是关键一步。

基本流程是这样的:

  1. 收到快照后,清空本地订单簿,用快照数据初始化
  2. 收到增量后,按价格档位更新本地订单簿
  3. 如果增量中的价格已存在,更新数量;如果不存在,新增档位
  4. 如果增量中的数量为0,删除该档位

代码实现其实不复杂:

class OrderBook:
    def __init__(self):
        self.bids = {}  # 价格 -> 数量
        self.asks = {}
    
    def apply_snapshot(self, snapshot):
        self.bids.clear()
        self.asks.clear()
        for price, size in snapshot['bids']:
            self.bids[price] = size
        for price, size in snapshot['asks']:
            self.asks[price] = size
    
    def apply_update(self, update):
        for change in update['changes']:
            side = change['side']
            price = change['price']
            size = change['size']
            book = self.bids if side == 'bid' else self.asks
            if size == 0:
                book.pop(price, None)  # 删除
            else:
                book[price] = size      # 新增或更新

嗯,看起来很简单对吧?但实际生产中,有几个坑要注意:

  • 时序问题:增量更新的顺序必须严格按时间排列。如果乱序,订单簿就乱了。我建议在本地维护一个缓冲区,按时间戳排序后再处理。
  • 快照过期:快照本身也有时效性。如果你收到快照后很久才收到增量,中间的变化可能已经丢失了。一般交易所会要求你在收到快照后立即开始接收增量。
  • 数据对齐:有时候交易所会同时推送多个数据源(比如WebSocket和REST API),你需要确保它们的时间戳对齐。我习惯用交易所的sequence ID来做对齐,比时间戳更可靠。

个人经验:我建议在本地维护一个「校验和」。每处理100条增量后,计算当前订单簿的哈希值,与交易所定期推送的校验和做对比。如果对不上,立即重新请求快照。这个机制帮我避免过好几次数据污染。

2.4 知识体系总览

下面这张图总结了本章的核心逻辑,你可以对照着梳理一下思路:

订单簿数据体系 Level 1 最优买卖价 Level 2 前N档盘口 Level 3 全量订单簿 数据传输机制:快照(Snapshot)+ 增量(Incremental Update) 核心操作:数据合并与对齐 时序校验 快照初始化 增量更新

从这张图可以看得很清楚:数据从交易所流出,经过快照和增量两条路径,最终在本地合并成完整的订单簿。每一步都有坑,但每一步也都有对应的解决方案。

好了,关于订单簿快照与增量更新的内容就聊到这里。记住一句话:数据质量决定策略质量。把数据层做扎实了,后面的策略开发才能事半功倍。


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