订单簿数据结构:订单簿的构成(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」。这样数据量小很多,但需要你自己维护一个本地的订单簿副本。

我个人习惯的做法是:

  1. 启动时先拉一次快照,建立初始订单簿
  2. 之后只接收增量更新,实时维护本地订单簿
  3. 每隔一段时间(比如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 Level 2 Level 3 更新方式 快照 增量更新 核心数据结构:SortedDict / OrderedDict 插入/删除价位 查询最优价格 序列号校验 做市引擎的核心:维护一个准确、高效的本地订单簿副本

这张图把订单簿的三大核心要素串起来了:构成(Bid/Ask)、数据层级(Level 1/2/3)、更新方式(快照/增量)。三者最终汇聚到核心数据结构上,再衍生出关键操作。

嗯,最后说一句:订单簿数据结构的实现,直接决定了做市引擎的性能。我在项目中见过有人用列表硬扛,结果每秒处理几千次更新时直接崩了。选对数据结构,比什么都重要。