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 事件驱动更新机制
理解了快照和增量,接下来就是怎么把它们拼起来。这里要用到事件驱动机制。
事件驱动,说白了就是「有变化才处理」。不像轮询那样每隔一秒去问「你变了没」,而是交易所主动告诉你「我变了」。
典型的处理流程是这样的:
- 收到全量快照 → 初始化本地订单簿
- 收到增量更新 → 更新本地订单簿
- 收到新的全量快照 → 重置本地订单簿(防止误差累积)
注意:增量更新必须按顺序处理。如果顺序乱了,订单簿就废了。我曾经因为WebSocket重连后消息乱序,导致本地订单簿差了整整10个tick,回测结果直接崩了。
这里我画了一张流程图,帮你理清整个逻辑:
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