订单簿数据结构:OrderBook设计、Level2数据模型、价格-数量映射、深度合并算法

做市商报价引擎的核心,说白了就是一本「电子账本」。

这本账本记录着市场上所有未成交的买卖意愿。谁想买、谁想卖、出什么价、要多少量,全得记清楚。我当年刚接触高频交易系统时,第一件事就是被老大扔去研究订单簿——他说,不懂OrderBook,就别谈做市。

嗯,这话一点不夸张。

一、OrderBook的核心职责

一个订单簿,至少得干三件事:

  • 存储:把买卖双方的挂单按价格层级存好
  • 更新:收到新订单、撤单、成交时,实时修改数据
  • 查询:快速给出最优买卖价、深度分布、累计量

我习惯把OrderBook比作一个「双端优先队列」。买盘是最大堆,卖盘是最小堆。但实际工程里,我们很少直接用堆结构——因为堆不支持随机删除,而撤单是家常便饭。

核心原则:订单簿的更新速度,决定了你的报价延迟。每笔行情进来,你必须在微秒级完成数据重组。

二、Level2数据模型

Level2,也叫市场深度数据。它不像Level1只给最优价,而是把每个价位的挂单量都告诉你。

一个典型的Level2快照长这样:

{
  "asks": [
    [100.50, 2000],   // 价格100.50,卖单总量2000股
    [100.55, 1500],
    [100.60, 3000]
  ],
  "bids": [
    [100.45, 2500],   // 价格100.45,买单总量2500股
    [100.40, 1800],
    [100.35, 2200]
  ],
  "timestamp": 1699000000123,
  "symbol": "BTC/USDT"
}

这里有个细节:每个价格层级上的量,是所有挂在这个价位的订单的总和。你想想看,如果100.50上有10个人挂单,每人200股,那这个价位的量就是2000。

我在项目中遇到过一个问题:有些交易所的Level2数据是「增量更新」的,只告诉你哪个价位变了。这时候你就得维护一个本地快照,每次收到增量就patch上去。嗯,这里要注意——增量更新的顺序不能乱,否则快照就废了。

三、价格-数量映射:用什么数据结构?

说白了,我们需要一个从价格到数量的映射。而且这个映射要支持:

  • 按价格排序(从小到大或从大到小)
  • 快速定位某个价格
  • 快速删除某个价格
  • 快速获取最优价

我推荐用红黑树跳表。C++里直接用std::map,Java里用TreeMap,Python里用sortedcontainers库。

但如果你追求极致性能,我建议自己实现一个价格桶数组。为什么?

因为价格精度是有限的。比如股票价格最小变动单位是0.01元,那你可以把价格乘以100转成整数,然后用数组下标直接索引。这样查询就是O(1),比红黑树快一个数量级。

我的经验:在币圈做市时,价格精度到小数点后8位,没法用桶数组。这时候我用了跳表——实现简单,而且并发性能比红黑树好。

四、深度合并算法

深度合并,就是把多个订单簿合并成一个。比如你在三个交易所都挂了单,想看看总的深度分布。

算法其实很简单:

  1. 遍历所有订单簿的买盘,按价格分组,相同价格的量相加
  2. 遍历所有订单簿的卖盘,同样处理
  3. 按价格排序输出

伪代码长这样:

function mergeDepth(books):
    merged_asks = {}
    merged_bids = {}
    
    for book in books:
        for price, qty in book.asks:
            merged_asks[price] = merged_asks.get(price, 0) + qty
        for price, qty in book.bids:
            merged_bids[price] = merged_bids.get(price, 0) + qty
    
    // 按价格排序
    sorted_asks = sorted(merged_asks.items())
    sorted_bids = sorted(merged_bids.items(), reverse=True)
    
    return (sorted_asks, sorted_bids)

但实际工程里,有个坑:不同交易所的价格精度可能不一样。比如A交易所价格是100.50,B交易所可能是100.50000001。这时候直接按价格合并就会出问题。

我曾经踩过的坑:有一次合并三个交易所的深度,发现总深度比实际少了一大截。查了半天,原来是某个交易所的价格精度是小数点后4位,另一个是8位。同样的价格,一个存成100.5000,一个存成100.50000000,在哈希表里成了两个key。后来我统一做了价格截断,才解决。

五、知识体系总览

下面这张图,是我自己总结的订单簿数据结构核心逻辑。你看一眼,基本就明白整个章节在讲什么了。

订单簿数据结构核心逻辑 OrderBook Level2数据模型 价格-数量映射 深度合并算法 快照+增量 价格层级聚合 红黑树/跳表 价格桶数组 按价格分组求和 精度对齐 核心目标:微秒级更新 + 亚微秒级查询 数据结构选择 = 性能 × 精度 × 实现复杂度

六、实战中的选择建议

说了这么多理论,到底怎么选?我给出几条实际建议:

场景 推荐数据结构 理由
股票(价格精度0.01) 价格桶数组 O(1)查询,内存连续,缓存友好
加密货币(高精度) 跳表 支持高精度,并发插入删除性能好
多交易所合并 红黑树 + 哈希表 需要快速查找和范围遍历
低延迟做市 自定义内存池 + 跳表 避免GC,控制内存分配

一个小技巧:不管用什么数据结构,记得把「最优买卖价」单独缓存起来。每次更新订单簿时,只检查最优价有没有变。这样查询最优价就是O(1),不用每次都去树里找最小/最大值。

嗯,订单簿这块内容其实挺深的。今天讲的这些,是我在实际做市系统里反复验证过的。你如果自己动手写一个OrderBook,建议先从最简单的std::map版本开始,跑通逻辑,再考虑优化性能。

毕竟,能跑起来的代码,才是有价值的代码。


公众号:蓝海资料掘金营,微信deep3321