订单簿数据结构:订单簿的构成(Bid/Ask)、Level 2/Level 3数据、订单簿快照与增量更新
做市引擎的核心,说白了就是跟订单簿打交道。你想想看,我们每天盯着的那个买卖盘口,就是订单簿。我刚开始做量化的时候,觉得订单簿不就是一堆价格和数量嘛,有什么好研究的?后来在实盘中被狠狠教育了几次,才明白这里面门道深着呢。
订单簿的基本构成:Bid 与 Ask
订单簿由两部分组成:买单队列(Bid)和卖单队列(Ask)。Bid 是买方愿意出的最高价,Ask 是卖方愿意接受的最低价。两者之间的差距,就是大家常说的「价差」。
我个人习惯把订单簿想象成一个「价格阶梯」。每一级阶梯上,都站着一些等待成交的订单。比如这样:
卖五:100.50 200股
卖四:100.40 150股
卖三:100.30 300股
卖二:100.20 100股
卖一:100.10 500股
------------------- 当前市价
买一:100.00 400股
买二:99.90 600股
买三:99.80 200股
买四:99.70 350股
买五:99.60 100股
嗯,这里要注意:买一和卖一之间的价差,是做市商最核心的利润来源。我在项目中遇到过,有些新手做市商为了抢单,把价差压得太小,结果一次大单波动就把利润全吃回去了。
Level 2 与 Level 3 数据
这两个概念,很多人容易搞混。我简单解释一下:
| 数据层级 | 包含内容 | 典型用途 |
|---|---|---|
| Level 1 | 最优买卖价(Best Bid/Offer) | 普通行情展示 |
| Level 2 | 多档位买卖盘口(通常5-10档) | 做市策略、订单簿分析 |
| Level 3 | 全量订单簿 + 逐笔委托数据 | 高频做市、微观结构分析 |
Level 2 数据,说白了就是能看到「买一到买五、卖一到卖五」的完整挂单情况。我记得刚入行时,用的就是 Level 2 数据做策略回测,效果还不错。
Level 3 数据就厉害了。它能看到每一笔挂单的详细信息,包括订单ID、下单时间、委托类型等。我曾经用 Level 3 数据做过一个「冰山订单检测」的模块,效果出奇的好。但代价是数据量巨大,一天就能产生几个GB。
重要提醒:做市策略至少需要 Level 2 数据。如果只用 Level 1,你根本看不到盘口的深度变化,很容易被大单吃掉。
订单簿快照与增量更新
这是做市引擎中最关键的数据处理方式。为什么?因为全量推送太慢了。
订单簿快照(Snapshot):就是某一时刻订单簿的完整状态。比如交易所每隔100ms给你发一次完整的买卖盘口数据。优点是简单,缺点是带宽消耗大。
增量更新(Incremental Update):只告诉你「刚才发生了什么变化」。比如「买一价格从100.00变成了99.90,数量从400变成了600」。这样数据量小很多,但需要你自己维护一个本地的订单簿副本。
我个人习惯的做法是:
- 启动时先拉一次快照,建立初始订单簿
- 之后只接收增量更新,实时维护本地订单簿
- 每隔一段时间(比如5分钟)重新拉一次快照做校验
实战技巧:我曾经在维护订单簿时,发现本地数据和交易所快照对不上。排查了半天,原来是增量更新丢了一条消息。后来我加了一个「序列号校验」机制,每次增量更新都带一个递增的序列号,如果发现跳号就立即重新拉快照。这个坑,我踩过。
订单簿的核心数据结构
在Python中,我通常用OrderedDict或者SortedDict来维护订单簿。为什么?因为我们需要快速找到最优价格,同时还要支持插入和删除操作。
class OrderBook:
def __init__(self):
self.bids = SortedDict() # 买盘,按价格降序
self.asks = SortedDict() # 卖盘,按价格升序
self.sequence = 0 # 序列号,用于校验
def apply_snapshot(self, snapshot):
"""应用快照"""
self.bids.clear()
self.asks.clear()
for price, qty in snapshot['bids']:
self.bids[price] = qty
for price, qty in snapshot['asks']:
self.asks[price] = qty
self.sequence = snapshot['sequence']
def apply_update(self, update):
"""应用增量更新"""
# 检查序列号连续性
if update['sequence'] != self.sequence + 1:
# 序列号不连续,需要重新拉快照
return False
for price, qty in update['bids']:
if qty == 0:
# 数量为0表示删除该价位
self.bids.pop(price, None)
else:
self.bids[price] = qty
# 卖盘同理
for price, qty in update['asks']:
if qty == 0:
self.asks.pop(price, None)
else:
self.asks[price] = qty
self.sequence = update['sequence']
return True
避坑指南:我曾经在实盘环境中,因为增量更新的处理顺序搞反了,导致订单簿价格错乱。记住:先处理删除(数量为0),再处理新增或修改。否则可能出现「先插入后删除」的时序问题。
知识体系结构图
下面这张图,是我自己总结的订单簿数据结构核心逻辑。你看一眼就能明白整个脉络:
这张图把订单簿的三大核心要素串起来了:构成(Bid/Ask)、数据层级(Level 1/2/3)、更新方式(快照/增量)。三者最终汇聚到核心数据结构上,再衍生出关键操作。
嗯,最后说一句:订单簿数据结构的实现,直接决定了做市引擎的性能。我在项目中见过有人用列表硬扛,结果每秒处理几千次更新时直接崩了。选对数据结构,比什么都重要。