3、订单簿重建与快照:从逐笔委托重建Order Book;Bid/Ask队列维护;Top of Book 与 Market Depth
好,咱们进入正题。这一章要聊的,是订单流分析里最基础、也最核心的一步——订单簿重建。
说白了,就是给你一堆逐笔委托数据,你怎么把它还原成交易所当时看到的那个“挂单墙”。我刚开始做这个的时候,觉得不就是个队列嘛,往里塞数据就行了。结果第一次回测就发现,我的订单簿跟交易所的快照对不上,差了十几个tick。嗯,后来才明白,这里面的门道比想象中多。
3.1 为什么需要重建订单簿?
交易所给我们推送的行情,通常有两种:
- 逐笔委托(Order Book Snapshots / Incremental Updates):每来一笔订单,告诉你“有人在某个价格挂了100手买单”或者“有人撤单了”。
- 快照(Snapshots):每隔一段时间(比如100ms),给你一张完整的挂单表。
如果你只依赖快照,那中间发生的所有变化你都看不到。而高频策略,恰恰就是吃这中间的“残羹剩饭”。所以,我们必须自己动手,用逐笔数据把订单簿实时重建出来。
3.2 订单簿的数据结构
我个人习惯用 价格优先队列 来维护。每个价格水平上,挂着一堆订单。具体来说:
- Bid Side(买方):价格从高到低排序。最高价是 Best Bid。
- Ask Side(卖方):价格从低到高排序。最低价是 Best Ask。
每个价格水平上,我们需要记录:
- 总挂单量(Total Volume)
- 订单数量(Order Count)
- 有时候还需要记录每个订单的ID,方便撤单时精准删除
我见过有人用 Python 的 dict 加 sortedcontainers 来做,性能也还行。但如果你用 C++ 或者 Rust,那直接上 std::map 或者 BTreeMap 就完事了。
3.3 逐笔委托的三种操作
逐笔委托数据,本质上就三种操作:
- 新增订单(Add / New):在某个价格上,增加一笔订单。
- 撤单(Cancel / Delete):删除一笔已有的订单。
- 成交(Execute / Trade):订单部分或全部成交,减少对应数量。
你想想看,交易所的订单簿就是被这三件事不断改变的。我们本地重建,就是模拟这个过程。
3.4 代码示例:一个简单的订单簿维护逻辑
下面我用 Python 写一个极简的订单簿类。别指望它能直接上生产环境,但核心逻辑都在里面了。
class OrderBook:
def __init__(self):
self.bids = {} # price -> total_volume
self.asks = {} # price -> total_volume
self.best_bid = 0
self.best_ask = float('inf')
def apply_add(self, side, price, volume):
if side == 'B':
self.bids[price] = self.bids.get(price, 0) + volume
if price > self.best_bid:
self.best_bid = price
else:
self.asks[price] = self.asks.get(price, 0) + volume
if price < self.best_ask:
self.best_ask = price
def apply_cancel(self, side, price, volume):
if side == 'B':
self.bids[price] -= volume
if self.bids[price] <= 0:
del self.bids[price]
# 如果删掉的是 best bid,需要重新计算
if price == self.best_bid:
self.best_bid = max(self.bids.keys()) if self.bids else 0
else:
self.asks[price] -= volume
if self.asks[price] <= 0:
del self.asks[price]
if price == self.best_ask:
self.best_ask = min(self.asks.keys()) if self.asks else float('inf')
def apply_trade(self, side, price, volume):
# 成交本质上就是减少挂单量
self.apply_cancel(side, price, volume)
def get_top_of_book(self):
return self.best_bid, self.best_ask
def get_depth(self, levels=5):
bid_levels = sorted(self.bids.items(), reverse=True)[:levels]
ask_levels = sorted(self.asks.items())[:levels]
return bid_levels, ask_levels
这段代码里,我故意没处理多笔订单ID的细节。实际项目中,每个价格水平上可能挂着几十笔不同ID的订单,撤单时你得精确到ID。否则,如果两笔订单在同一价格,你撤了一笔,总成交量减错了,后面全乱套。
3.5 Top of Book 与 Market Depth
订单簿重建好了,我们能拿它干什么?
- Top of Book:就是最优买卖价(Best Bid / Best Ask)。这是最直观的行情指标。价差(Spread)就是 Best Ask - Best Bid。我习惯用这个来判断当前市场的流动性紧张程度。
- Market Depth:指的是每个价格水平上的挂单量。比如“买一”挂了100手,“买二”挂了200手……这能告诉你,如果我想吃掉某个价位的所有订单,需要多少资金。
举个例子,假设当前 Top of Book 是:
| Side | Price | Volume |
|---|---|---|
| Ask | 100.05 | 500 |
| Bid | 100.00 | 300 |
价差是 0.05。如果你是个做市商,看到价差这么小,可能就不太愿意进场了,因为利润太薄。但如果你是个动量策略,看到买一有300手撑着,可能会觉得下方支撑很强。
3.6 知识体系结构图
下面这张图,是我自己总结的订单簿重建的核心流程。你可以把它当作一个思维导图来看。
这张图从左到右,从上到下,就是整个流程。输入是逐笔数据,中间经过三种操作维护两个队列,最后输出 Top of Book 和 Depth,供策略使用。
3.7 一些实战中的小建议
- 性能问题:如果你处理的是全市场数据,每秒可能有几万笔委托。Python 的 dict 操作虽然快,但如果你在每个 tick 都做排序,那肯定扛不住。我建议用
heapq或者sortedcontainers来维护价格队列。 - 数据对齐:逐笔数据的时间戳和快照的时间戳可能不同步。我习惯以快照时间为基准,把快照之前的逐笔数据全部回放一遍,然后再用快照做一次校验。
- 内存管理:如果你要回放一整天的数据,订单簿里的价格水平可能会非常多。记得定期清理那些挂单量为0的价格点,否则内存会涨得很快。
嗯,这一章的内容差不多就这些。订单簿重建听起来简单,但真正做起来,细节非常多。尤其是当你面对真实的高频数据时,各种边界情况会让你头疼。不过别担心,多踩几次坑,你就知道该怎么处理了。