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 快照与增量的协同工作
实际生产中,两者是配合使用的。我习惯用“快照+增量流”的模式:
- 初始化:先请求一次全量快照,建立本地订单簿。
- 实时更新:持续接收增量消息,逐条更新本地订单簿。
- 定期校验:每隔一段时间(比如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