3. 订单簿快照与增量更新:Snapshot vs. Delta 机制
做市商系统里,订单簿的更新方式是个绕不开的话题。说白了,就是你怎么把交易所那一堆不断变化的挂单数据,高效地同步到你的本地系统里。
我个人习惯把这个问题拆成两个极端来看:全量快照(Snapshot)和增量更新(Delta)。嗯,这俩就像“重新下载整张地图”和“只告诉你哪里修路了”的区别。
3.1 全量快照:简单粗暴,但代价高
全量快照,就是交易所每隔一段时间(比如每100毫秒),把当前订单簿上所有的买单和卖单,一股脑全发给你。
- 优点:实现简单,你拿到数据直接覆盖本地订单簿就行,不用担心数据对不上。
- 缺点:数据量大得吓人。一个深度稍好的币对,订单簿可能有几千甚至上万档价格。每次快照都传一遍,带宽和CPU都扛不住。
我在早期做回测系统时,就偷懒用过全量快照。结果呢?回测跑一次要等半天,光解析JSON就占了大半时间。后来我学乖了——全量快照只适合做“基准校准”,不适合做高频实时更新。
3.2 增量更新:高效,但容易“对不上账”
增量更新就聪明多了。交易所只告诉你:
“刚才有人撤单了,价格100.5的卖单数量从10变成5。”
“有人新挂了一个单,价格101.0,数量2。”
你只需要在本地订单簿上做对应的加减操作就行。
但这里有个坑——增量更新依赖一个正确的“初始状态”。如果你本地订单簿因为网络丢包、程序重启等原因,跟交易所对不上了,那后续所有增量更新都会“失之毫厘,谬以千里”。
⚠️ 我曾经踩过的坑:
有一次生产环境里,网络抖动导致我漏掉了一条增量消息。结果本地订单簿的卖一价跟交易所差了0.5个tick。我的做市策略以为价格要跌,疯狂挂低价卖单,差点被套利者吃掉。从那以后,我强制要求:每收到1000条增量,必须拉一次全量快照做校验。
有一次生产环境里,网络抖动导致我漏掉了一条增量消息。结果本地订单簿的卖一价跟交易所差了0.5个tick。我的做市策略以为价格要跌,疯狂挂低价卖单,差点被套利者吃掉。从那以后,我强制要求:每收到1000条增量,必须拉一次全量快照做校验。
3.3 混合策略:Snapshot + Delta 的黄金组合
实际生产中,没人只用纯增量或纯快照。主流做法是:
- 启动时:先拉一次全量快照,建立基准。
- 运行中:只接收增量更新,维持本地订单簿。
- 定期校验:比如每5分钟或每1000条增量后,再拉一次快照,跟本地数据做比对。如果发现偏差,就丢弃本地数据,用新快照重建。
你想想看,这就像你开车时,导航每隔几分钟告诉你“前方路况有变”,但每半小时会重新规划一次全路径。既保证了实时性,又防止了累积误差。
3.4 代码实现:一个简单的订单簿类
下面是我自己写的一个简化版订单簿,支持快照和增量更新。代码不长,但核心逻辑都在里面了。
class OrderBook:
def __init__(self):
self.bids = {} # 买单:价格 -> 数量
self.asks = {} # 卖单:价格 -> 数量
self.sequence = 0 # 用于校验增量顺序
def apply_snapshot(self, snapshot):
"""全量快照:直接覆盖"""
self.bids = {item[0]: item[1] for item in snapshot['bids']}
self.asks = {item[0]: item[1] for item in snapshot['asks']}
self.sequence = snapshot['sequence']
print(f"快照应用完成,买一:{self.get_best_bid()},卖一:{self.get_best_ask()}")
def apply_delta(self, delta):
"""增量更新:逐条处理"""
# 检查增量顺序,防止乱序
if delta['sequence'] <= self.sequence:
print(f"警告:收到过期增量,跳过 (seq {delta['sequence']} <= {self.sequence})")
return
# 处理买单增量
for price, size in delta['bids']:
if size == 0:
# 数量为0表示删除该档位
self.bids.pop(price, None)
else:
self.bids[price] = size
# 处理卖单增量
for price, size in delta['asks']:
if size == 0:
self.asks.pop(price, None)
else:
self.asks[price] = size
self.sequence = delta['sequence']
def get_best_bid(self):
"""获取最优买价"""
if not self.bids:
return None
return max(self.bids.keys())
def get_best_ask(self):
"""获取最优卖价"""
if not self.asks:
return None
return min(self.asks.keys())
def check_consistency(self, snapshot):
"""校验本地数据与快照是否一致"""
local_bids = set(self.bids.items())
snapshot_bids = set((item[0], item[1]) for item in snapshot['bids'])
if local_bids != snapshot_bids:
print("⚠️ 买单数据不一致!需要重建")
return False
return True
💡 个人经验:
代码里那个
代码里那个
sequence 字段很重要。我在对接某二线交易所时,他们的增量消息偶尔会乱序。如果不做序列号校验,本地订单簿会越跑越偏。后来我加了个“延迟队列”,把乱序的增量先缓存起来,等前面的序号补齐了再处理。这招帮我省了不少麻烦。
3.5 性能对比:什么时候该用哪种?
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 系统启动/重启 | 全量快照 | 快速建立基准,避免增量累积 |
| 高频交易(毫秒级) | 增量更新 | 带宽和CPU开销小,延迟低 |
| 网络不稳定 | 混合模式 | 定期快照校验,防止数据漂移 |
| 回测/历史数据分析 | 全量快照 | 简化逻辑,避免增量重放带来的复杂度 |
3.6 知识体系图:Snapshot vs. Delta 的核心逻辑
下面这张图,是我自己梳理的决策流程。你一看就明白什么时候该用快照,什么时候该用增量。
这张图的核心逻辑就是:首次用快照,后续用增量,定期做校验。一旦发现数据对不上,别犹豫,直接拉快照重建。宁可花几毫秒重建,也别让错误数据跑一整天。
📌 核心要点:
- 全量快照是“基准”,增量更新是“日常”。
- 序列号校验是增量更新的生命线,别省。
- 定期校验能防止“千里之堤,溃于蚁穴”。
- 回测时用全量快照更省心,别跟自己过不去。
公众号:蓝海资料掘金营,微信deep3321