2. 订单簿构建:Level2快照与增量更新、订单簿重建算法
好,咱们进入第二章。这一章要聊的是订单簿的构建,说白了就是怎么把交易所发过来的原始数据,变成我们量化策略能用的那个「实时价格地图」。我个人觉得,这是所有微观结构分析的基石。你想想看,如果地基没打牢,后面那些流动性信号、订单流分析,全都会跑偏。
2.1 Level2快照:一张静态的全景图
先说说快照。快照是什么?就是交易所每隔一段时间(比如100毫秒或500毫秒)给你拍一张「当前订单簿的全家福」。这张照片里包含了所有价位的挂单量和挂单笔数。
我刚开始做高频策略的时候,以为快照就是全部了。后来发现,快照之间丢失的信息量,有时候比快照本身还大。但不管怎样,快照是重建订单簿的起点。
一个典型的Level2快照数据结构长这样:
{
"timestamp": 1630000000000,
"asks": [
[100.50, 2000, 15], // 价格, 总量, 委托笔数
[100.51, 1500, 10],
...
],
"bids": [
[100.48, 3000, 20],
[100.47, 2500, 12],
...
]
}
这里要注意,不同交易所的字段名可能不一样。有的叫asks,有的叫sell,有的把笔数放在另一个字段里。嗯,这里要提醒一句:永远不要假设数据格式是固定的。我在项目中遇到过,某次交易所升级后,快照里突然多了一个seq_num字段,差点把解析逻辑搞崩。
2.2 增量更新:捕捉每一笔变化
快照是静态的,但市场是动态的。每一笔成交、每一个撤单、每一个新挂单,都会改变订单簿。增量更新就是把这些「变化」一条一条告诉你。
增量更新的类型通常有这几种:
- 新增订单:某个价位上多了一笔挂单
- 撤销订单:某个价位上少了一笔挂单
- 成交:订单被吃掉,数量减少或完全消失
- 修改订单:价格不变但数量变了(有些交易所会拆成撤单+新单)
每条增量消息一般包含:
| 字段 | 说明 |
|---|---|
| side | 买盘还是卖盘('b' 或 'a') |
| price | 价格 |
| size | 变化后的数量(注意:不是变化量!) |
| order_id | 订单唯一标识 |
| type | 操作类型(新增/删除/修改) |
这里有个关键点:size字段是变化后的绝对值,不是差值。比如原来有100手,成交了20手,增量消息里size就是80。这个设计其实是为了方便你直接覆盖,不用做加减法。但如果你没搞清楚,直接用累加,那订单簿就乱套了。
2.3 订单簿重建算法:从零到一
好了,现在我们有快照和增量了。怎么把它们拼起来?这就是订单簿重建算法要干的事。
核心流程其实不复杂,我画了一张图帮你理解:
流程看着简单,但实现起来有几个坑。我直接上代码,咱们边看边聊:
class OrderBook:
def __init__(self):
self.bids = {} # price -> {order_id: size}
self.asks = {} # price -> {order_id: size}
self.bid_levels = [] # 排序后的买盘价位
self.ask_levels = [] # 排序后的卖盘价位
def apply_snapshot(self, snapshot):
"""应用快照,重建订单簿"""
self.bids.clear()
self.asks.clear()
for price, size, count in snapshot['bids']:
# 每个价位用一个虚拟order_id存储总量
self.bids[price] = {'snapshot_total': size}
for price, size, count in snapshot['asks']:
self.asks[price] = {'snapshot_total': size}
self._sort_levels()
def apply_increment(self, inc):
"""应用增量更新"""
side = self.bids if inc['side'] == 'b' else self.asks
price = inc['price']
order_id = inc['order_id']
size = inc['size']
if price not in side:
side[price] = {}
if size == 0:
# 订单被撤销或完全成交
if order_id in side[price]:
del side[price][order_id]
if not side[price]:
del side[price]
else:
# 新增或修改订单
side[price][order_id] = size
self._sort_levels()
def _sort_levels(self):
"""排序价位,买盘降序,卖盘升序"""
self.bid_levels = sorted(self.bids.keys(), reverse=True)
self.ask_levels = sorted(self.asks.keys())
def get_top(self, n=10):
"""获取前n档买卖盘口"""
top_bids = [(p, sum(self.bids[p].values())) for p in self.bid_levels[:n]]
top_asks = [(p, sum(self.asks[p].values())) for p in self.ask_levels[:n]]
return top_bids, top_asks
2.4 增量序列号与数据一致性
增量更新是按顺序来的。每个增量消息都有一个序列号(seq_num),用来保证顺序。如果你收到的增量序列号不连续,说明中间丢包了。
我曾经在实盘环境中遇到过这种情况:网络抖动导致丢了3条增量,结果订单簿的总量对不上了。从那以后,我养成了一个习惯——每次应用增量前,先检查序列号是否连续。
检查逻辑很简单:
def check_seq_num(self, new_seq):
if self.last_seq is not None:
if new_seq != self.last_seq + 1:
# 序列号不连续,需要重新请求快照
self.request_snapshot()
return False
self.last_seq = new_seq
return True
如果发现丢包,别犹豫,直接请求新的快照,然后重新开始。虽然会丢失几毫秒的数据,但总比用一个「坏掉的订单簿」强。
2.5 性能优化:别让订单簿拖慢你的策略
订单簿更新频率很高,尤其是在Tick级数据下。每秒几百次甚至上千次更新都很正常。如果代码写得不够高效,CPU会先扛不住。
我分享几个优化技巧:
- 用数组代替字典:如果价格档位是固定的(比如0.01一跳),可以用数组索引代替字典查找。速度能快一个数量级。
- 延迟排序:不是每次更新都需要重新排序。你可以攒够10次更新再排一次,或者只在查询盘口时排序。
- 避免内存分配:在循环里频繁创建新对象会触发GC。尽量复用已有的数据结构。
sortedcontainers库来维护有序列表,结果发现它的插入性能在数据量大时会退化。后来换成了自己维护的跳表结构,性能才稳定下来。如果你用Python做高频,建议自己实现底层数据结构。
2.6 实战:从零搭建一个订单簿引擎
最后,咱们把上面所有知识点串起来,写一个完整的订单簿引擎。这个引擎能处理快照和增量,并实时输出盘口数据。
class OrderBookEngine:
def __init__(self):
self.book = OrderBook()
self.last_seq = None
self.snapshot_received = False
def on_snapshot(self, snapshot):
self.book.apply_snapshot(snapshot)
self.snapshot_received = True
self.last_seq = snapshot['seq_num']
def on_increment(self, inc):
if not self.snapshot_received:
# 还没收到快照,忽略增量
return
if not self.check_seq_num(inc['seq_num']):
return
self.book.apply_increment(inc)
def get_market_depth(self, levels=10):
bids, asks = self.book.get_top(levels)
spread = asks[0][0] - bids[0][0] if bids and asks else None
return {
'bids': bids,
'asks': asks,
'spread': spread,
'mid_price': (bids[0][0] + asks[0][0]) / 2 if bids and asks else None
}
这个引擎虽然简单,但已经能应对大部分场景了。实际生产中,你还需要加上线程安全、日志记录、异常处理等。不过核心逻辑就是这些。
嗯,订单簿构建这块,说白了就是「快照打底,增量修补」。只要保证快照的完整性和增量的顺序性,重建出来的订单簿就不会有问题。下一章咱们聊聊怎么从订单簿里提取流动性信号,那才是真正有意思的部分。
公众号:蓝海资料掘金营,微信deep3321