订单簿数据结构:从原始数据到交易信号的核心
做市策略的核心是什么?说白了,就是读懂订单簿。我做了这么多年量化,见过太多人把精力花在策略参数调优上,结果连订单簿的基本结构都没搞明白。嗯,今天我们就来把这个基础打扎实。
订单簿的构成要素
订单簿,你可以把它想象成一个实时更新的拍卖现场。买家和卖家都在喊价,只不过这个现场是数字化的。
一个标准的订单簿包含以下几个核心要素:
- 价格档位(Price Level):每个价格就是一个档位,比如3.250、3.251这样
- 委托量(Volume):该价格上挂着的总订单数量
- 订单数量(Order Count):该价格上有多少个独立的订单
- 买卖方向(Side):买单(Bid)还是卖单(Ask)
- 层级深度(Depth Level):从最优价格开始算,第几档
关键点:最优买卖价(Best Bid & Best Ask)之间的价差,就是我们常说的Spread。这是做市商吃饭的本钱。
我在项目中遇到过一个问题:有些交易所会把同价格的不同订单合并显示,有些则分开。这个细节会直接影响你计算订单簿深度的方式。千万别想当然。
Level2行情数据解析
Level2行情,说白了就是比普通行情更细的数据。普通行情只给你看最优的5档或10档,Level2能给你看全量数据。
常见的Level2数据格式有两种:
| 数据格式 | 特点 | 常见交易所 |
|---|---|---|
| 增量快照(Snapshot + Incremental) | 先发全量快照,后续只发变化 | Binance、OKX |
| 全量推送(Full Order Book) | 每次推送都是完整订单簿 | 部分传统交易所 |
我个人习惯用增量快照模式。为什么?因为数据量小,延迟低。你想想看,如果每秒推送100次全量数据,光网络带宽就够你受的。
解析Level2数据时,有几个坑要注意:
- 时间戳对齐:交易所的时间戳和你的本地时间可能有偏差
- 序列号校验:确保你没有漏掉任何一条增量消息
- 数据校验:我见过有些交易所偶尔会推送错误数据,需要做合法性检查
避坑指南:我曾经因为没做序列号校验,导致订单簿数据错乱了整整3分钟。那3分钟里,策略一直在用错误的数据做决策,亏了不少钱。从那以后,序列号校验成了我的标配。
订单簿的更新与维护
拿到数据后,怎么维护一个实时准确的订单簿?这里我分享一个我常用的数据结构:
class OrderBook:
def __init__(self):
self.bids = {} # 价格 -> 委托量
self.asks = {} # 价格 -> 委托量
self.best_bid = None
self.best_ask = None
self.sequence = 0
def update(self, data):
# 增量更新逻辑
for change in data['changes']:
price = change['price']
volume = change['volume']
side = change['side']
if volume == 0:
# 删除该价格档位
if side == 'bid':
self.bids.pop(price, None)
else:
self.asks.pop(price, None)
else:
# 更新或新增
if side == 'bid':
self.bids[price] = volume
else:
self.asks[price] = volume
# 更新最优价格
self.best_bid = max(self.bids.keys()) if self.bids else None
self.best_ask = min(self.asks.keys()) if self.asks else None
这个代码看起来简单,但实际生产环境中要考虑的东西多着呢。比如:
- 内存管理:全量订单簿可能有几千个价格档位,用字典存储要注意内存
- 排序效率:每次更新后都要重新找最优价格,用堆(Heap)结构会更高效
- 并发安全:如果你的策略是多线程的,记得加锁
小技巧:我建议用SortedDict或者跳表(Skip List)来维护订单簿。这样查找最优价格的时间复杂度能从O(n)降到O(log n)。对于高频场景,这个优化很关键。
订单簿的深度聚合
深度聚合,就是把原始订单簿数据加工成更有用的信息。比如:
- 累计深度:从最优价格开始,累计到第N档的总委托量
- 加权价格:考虑委托量后的平均价格
- 买卖压力比:买单总深度 vs 卖单总深度
- 价格冲击成本:吃掉一定量订单需要滑多少点
为什么要做深度聚合?你想想看,原始订单簿里几千个价格档位,你不可能全用上。我们需要提炼出关键特征,才能生成交易信号。
下面是我常用的一个深度聚合函数:
def aggregate_depth(order_book, levels=10):
"""
聚合订单簿深度
levels: 聚合的档位数
"""
bid_depth = []
ask_depth = []
# 按价格排序
sorted_bids = sorted(order_book.bids.items(), reverse=True)
sorted_asks = sorted(order_book.asks.items())
cum_bid_vol = 0
cum_ask_vol = 0
for i in range(min(levels, len(sorted_bids))):
price, vol = sorted_bids[i]
cum_bid_vol += vol
bid_depth.append({
'price': price,
'volume': vol,
'cum_volume': cum_bid_vol
})
for i in range(min(levels, len(sorted_asks))):
price, vol = sorted_asks[i]
cum_ask_vol += vol
ask_depth.append({
'price': price,
'volume': vol,
'cum_volume': cum_ask_vol
})
return bid_depth, ask_depth
这个函数返回的数据,可以直接用来计算各种指标。比如买卖压力比:
def pressure_ratio(bid_depth, ask_depth):
total_bid = bid_depth[-1]['cum_volume']
total_ask = ask_depth[-1]['cum_volume']
return total_bid / total_ask if total_ask > 0 else float('inf')
实战经验:我在做市策略中,最喜欢用的一个指标是「深度斜率」。它反映了订单簿在不同价格区间的流动性分布。斜率陡峭的地方,说明流动性集中,做市风险小;斜率平缓的地方,说明流动性分散,做市要小心。
最后,我画了一张图来总结订单簿数据结构的核心逻辑:
这张图把整个流程串起来了。从交易所拿到原始数据,经过解析、维护、聚合,最后生成交易信号。每一步都有坑,每一步都需要优化。
好了,订单簿数据结构这块就讲到这里。记住一句话:订单簿是你的眼睛,数据质量决定了你能不能看清市场。下一章我们会聊如何基于订单簿数据生成具体的做市信号。