3. 订单簿快照与增量:全量快照与增量更新

做量化交易的朋友,肯定都跟订单簿打过交道。说白了,订单簿就是交易所里所有挂单的集合——谁想买、谁想卖、什么价格、多少量,一目了然。

但这里有个现实问题:交易所的订单变化极快,每秒可能有几千甚至上万笔撤单、新单、成交。如果你每次都去拉全量数据,带宽和计算资源根本扛不住。所以,交易所普遍采用「快照+增量」的模式。

3.1 全量快照(Snapshot)

全量快照,就是某一时刻订单簿的完整状态。它包含所有买盘和卖盘的挂单信息。

我个人习惯把快照理解为「基准点」。你想想看,如果你要画一幅不断变化的画,总得先有个底稿吧?快照就是这个底稿。

快照的核心特征:

  • 包含完整的买卖盘口深度(通常到一定层级,比如50档)
  • 带有时间戳,标识该快照的生成时刻
  • 数据量大,但频率低(通常几秒到几十秒一次)

我在对接某家二线交易所时遇到过一个问题:他们的快照居然不包含订单ID。这意味着你没法把增量更新和快照对应起来。嗯,这种坑踩过一次就记住了。

3.2 增量更新(Incremental)

增量更新,就是两次快照之间发生的所有变化。它只告诉你「什么变了」,而不是「全部是什么」。

增量更新的典型结构是这样的:

{
  "type": "incremental",
  "timestamp": 1699000000123,
  "bids": [
    ["100.50", "2.0", "add"],    // 新增买单
    ["100.40", "0", "delete"],   // 删除买单
    ["100.30", "1.5", "update"]  // 更新买单
  ],
  "asks": [
    ["101.00", "3.0", "add"]
  ]
}

看到没?每条增量消息都包含:价格、数量、操作类型(新增/删除/更新)。

为什么交易所不直接告诉你「当前订单簿长什么样」?说白了,增量更新才是实时性的保障。全量快照是「照片」,增量更新是「视频流」。

3.3 事件驱动更新机制

理解了快照和增量,接下来就是怎么把它们拼起来。这里要用到事件驱动机制。

事件驱动,说白了就是「有变化才处理」。不像轮询那样每隔一秒去问「你变了没」,而是交易所主动告诉你「我变了」。

典型的处理流程是这样的:

  1. 收到全量快照 → 初始化本地订单簿
  2. 收到增量更新 → 更新本地订单簿
  3. 收到新的全量快照 → 重置本地订单簿(防止误差累积)

注意:增量更新必须按顺序处理。如果顺序乱了,订单簿就废了。我曾经因为WebSocket重连后消息乱序,导致本地订单簿差了整整10个tick,回测结果直接崩了。

这里我画了一张流程图,帮你理清整个逻辑:

订单簿事件驱动更新流程 交易所 WebSocket 数据流 消息类型? 全量快照 增量更新 重置本地订单簿 (覆盖旧数据) 更新本地订单簿 (增/删/改) 实时订单簿状态 快照路径 增量路径

3.4 WebSocket数据流处理

WebSocket是目前最主流的实时数据通道。相比REST API,它建立一次连接后,服务器可以持续推送数据给你。

处理WebSocket数据流,我总结了几个关键点:

环节 要点 我的经验
连接管理 自动重连、心跳检测 重连后要重新请求快照
消息解析 JSON解析、二进制协议 二进制解析更快,但调试麻烦
序列控制 消息序号、去重、乱序处理 我习惯用序号缓存队列
性能优化 内存池、零拷贝、异步IO Python用asyncio就够了

这里给一段Python示例,展示基本的WebSocket处理逻辑:

import asyncio
import websockets
import json

class OrderBookManager:
    def __init__(self):
        self.bids = {}  # 价格 -> 数量
        self.asks = {}
        self.last_update_id = 0

    async def connect(self, url):
        async with websockets.connect(url) as ws:
            # 先请求快照
            await ws.send(json.dumps({"method": "snapshot"}))
            snapshot = json.loads(await ws.recv())
            self.apply_snapshot(snapshot)

            # 持续处理增量
            async for msg in ws:
                data = json.loads(msg)
                if data["type"] == "incremental":
                    self.apply_incremental(data)

    def apply_snapshot(self, data):
        self.bids = {float(p): float(q) for p, q in data["bids"]}
        self.asks = {float(p): float(q) for p, q in data["asks"]}
        self.last_update_id = data["update_id"]
        print(f"快照已加载,共 {len(self.bids)} 档买单,{len(self.asks)} 档卖单")

    def apply_incremental(self, data):
        # 检查序号连续性
        if data["update_id"] != self.last_update_id + 1:
            print(f"警告:序号不连续,期望 {self.last_update_id + 1},实际 {data['update_id']}")
            # 这里应该触发重新请求快照
            return

        for price, qty, action in data["bids"]:
            price = float(price)
            if action == "delete" or float(qty) == 0:
                self.bids.pop(price, None)
            else:
                self.bids[price] = float(qty)

        for price, qty, action in data["asks"]:
            price = float(price)
            if action == "delete" or float(qty) == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = float(qty)

        self.last_update_id = data["update_id"]

避坑指南:我曾经在增量更新中忘记处理「数量为0即删除」的情况。结果订单簿里堆了一堆0量的挂单,导致买卖价差计算全错。嗯,这种细节最容易被忽略。

另外,WebSocket重连是个大坑。断开后你收到的第一条消息可能是增量,但你的本地订单簿已经过时了。正确的做法是:重连后先请求全量快照,再开始接收增量。

说白了,订单簿的实时维护就是「快照定基准,增量做修正」。这个思路不仅在交易领域有用,任何需要实时同步状态的系统都可以借鉴。


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