订单簿数据结构:核心概念与实战解析

做市商报价引擎,说白了就是跟订单簿打交道。我做了这么多年量化系统,订单簿数据结构这块踩过的坑,够写一本小册子了。今天咱们就把它掰开揉碎了讲清楚。

订单簿核心概念

订单簿是什么?简单说,就是市场上所有未成交订单的集合。它记录了谁想买、谁想卖,出什么价、要多少量。

我习惯把订单簿想象成一个双层的书架:

  • 买单(Bids):买家出的价格,从高到低排列
  • 卖单(Asks):卖家要的价格,从低到高排列

中间那个空隙,就是买卖双方的博弈地带。价差越小,流动性越好。

关键指标:

  • 买一价(Best Bid):最高的买入价格
  • 卖一价(Best Ask):最低的卖出价格
  • 价差(Spread):卖一价 - 买一价
  • 市场深度:各价位上的挂单总量

限价订单簿:L2 vs L3

这里有个容易混淆的概念——L2和L3。我在面试候选人的时候,发现很多人分不清。

L2(Level 2):聚合了每个价格档位的总挂单量。比如买一价100.00有1000股,买二价99.99有2000股。它不关心这些订单是谁挂的,只看总量。

L3(Level 3):能看到每个订单的详细信息。谁挂的、挂了多少、什么时候挂的、订单ID是什么。说白了,L3是原始数据,L2是加工后的聚合数据。

特性 L2 L3
数据粒度 按价格聚合 逐笔订单
数据量 较小(几十到几百档) 巨大(成千上万笔)
更新频率 中等 极高
典型用途 做市策略、盘口分析 订单流分析、高频策略

嗯,这里要注意:L3数据量非常大。我做过一个项目,处理全市场L3数据,单日数据量就超过100GB。如果你不是做极高频交易,L2基本够用了。

价格-时间优先级

这是订单簿的核心规则。说白了就是:

  1. 价格优先:买单出价高的优先成交,卖单要价低的优先成交
  2. 时间优先:价格相同时,先挂单的优先成交

为什么这么设计?你想想看,如果只看价格不看时间,那大家都等着最后时刻出价,市场就乱了。价格-时间优先级保证了公平性。

实战经验:我在做回测系统时,发现很多人忽略了时间戳的精度问题。交易所的时间戳通常是纳秒级的,但你的系统如果只记录到毫秒,那时间优先级的判断就会出错。曾经有个策略因为这个原因,回测和实盘表现差了20%。

订单簿快照与增量

这是订单簿数据分发的两种方式:

快照(Snapshot):某一时刻订单簿的完整状态。包含所有价格档位的挂单情况。

增量(Incremental/Diff):订单簿的变化部分。比如新增了一个订单、撤销了一个订单、成交了一部分。

为什么需要两种?我举个例子:

  • 如果你刚连上交易所,需要先拿一个快照,知道当前市场状态
  • 之后只需要接收增量更新,保持本地订单簿同步
  • 每隔一段时间(比如1分钟),再拿一次快照做校验

避坑指南:我曾经遇到过增量数据丢失的情况。交易所的网络偶尔会断,增量序列中间缺了几条,结果本地订单簿跟交易所对不上了。从那以后,我强制要求系统每30秒做一次快照校验。发现不一致就重新拉取全量快照。

订单簿数据结构设计

用Python实现一个简单的订单簿,我习惯这样设计:

class OrderBook:
    def __init__(self):
        self.bids = {}  # 价格 -> 总量
        self.asks = {}  # 价格 -> 总量
        self.orders = {}  # order_id -> Order对象
        self.last_update_id = 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.last_update_id = snapshot['update_id']
    
    def apply_update(self, update):
        side = self.bids if update['side'] == 'buy' else self.asks
        if update['type'] == 'new':
            side[update['price']] = side.get(update['price'], 0) + update['qty']
        elif update['type'] == 'cancel':
            side[update['price']] = side.get(update['price'], 0) - update['qty']
            if side[update['price']] <= 0:
                del side[update['price']]
        self.last_update_id = update['update_id']
    
    def get_best_bid(self):
        return max(self.bids.keys()) if self.bids else None
    
    def get_best_ask(self):
        return min(self.asks.keys()) if self.asks else None
    
    def get_spread(self):
        bid = self.get_best_bid()
        ask = self.get_best_ask()
        if bid and ask:
            return ask - bid
        return None

这个实现虽然简单,但核心逻辑都在了。实际生产环境中,我会用红黑树或者跳表来维护价格排序,因为Python的dict是无序的,每次取最优价都要遍历,性能不行。

订单簿核心逻辑流程图

订单簿核心逻辑流程图 交易所数据流 数据类型? 全量快照更新 增量更新 本地订单簿 策略使用

实战中的几个坑

讲几个我实际踩过的坑:

  • 浮点数精度:价格用浮点数存,比较的时候出问题。我后来统一用整数,价格乘以10000存。
  • 内存泄漏:L3订单簿如果不清除已成交订单,内存会暴涨。我设置了一个定时任务,每5分钟清理一次。
  • 并发访问:多线程同时读写订单簿,不加锁会出大问题。我用的是读写锁,读多写少的场景性能很好。

小技巧:做市商策略对订单簿的实时性要求很高。我建议把订单簿放在独立线程里,用无锁队列跟策略线程通信。这样即使订单簿更新再频繁,也不会阻塞策略逻辑。

订单簿数据结构,说白了就是这些内容。但真正做好,需要大量的实战经验。我建议你从模拟交易所开始,自己实现一个订单簿,然后跑一些历史数据看看效果。纸上得来终觉浅,绝知此事要躬行。

专注资料整理