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 数据合并与对齐
拿到快照和增量后,怎么把它们合并成实时订单簿?这是关键一步。
基本流程是这样的:
- 收到快照后,清空本地订单簿,用快照数据初始化
- 收到增量后,按价格档位更新本地订单簿
- 如果增量中的价格已存在,更新数量;如果不存在,新增档位
- 如果增量中的数量为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 知识体系总览
下面这张图总结了本章的核心逻辑,你可以对照着梳理一下思路:
从这张图可以看得很清楚:数据从交易所流出,经过快照和增量两条路径,最终在本地合并成完整的订单簿。每一步都有坑,但每一步也都有对应的解决方案。
好了,关于订单簿快照与增量更新的内容就聊到这里。记住一句话:数据质量决定策略质量。把数据层做扎实了,后面的策略开发才能事半功倍。
公众号:蓝海资料掘金营,微信deep3321