4、订单簿构建:从原始行情数据到实时订单簿的构建流程

说实话,很多人觉得订单簿构建就是个简单的「排序+汇总」过程。但我在实际项目中踩过不少坑,才明白这里面门道有多深。今天我就把完整的构建流程拆开揉碎了讲给你听。

4.1 原始行情数据长什么样?

先看看交易所到底给我们发了什么。以最常见的沪深交易所Level-2行情为例,原始数据大概长这样:

// 逐笔成交数据示例
{
  "time": "09:30:05.123",
  "price": 10.25,
  "volume": 1000,
  "direction": "buy",   // 主动买入
  "type": "F"           // F表示成交,A表示撤单
}

// 委托快照数据示例
{
  "time": "09:30:05.000",
  "bid_prices": [10.24, 10.23, 10.22, ...],
  "bid_volumes": [500, 800, 1200, ...],
  "ask_prices": [10.25, 10.26, 10.27, ...],
  "ask_volumes": [600, 900, 1100, ...]
}

嗯,这里要注意:不同交易所的数据格式差异很大。我当年做美股的时候,纳斯达克的ITCH协议和A股的Level-2完全是两套逻辑。但核心思想是一样的——我们都需要把这些碎片化的数据拼成一张完整的「挂单地图」。

4.2 构建流程的核心三步

我个人习惯把订单簿构建拆成三个步骤。你想想看,这就像搭积木:

  1. 数据清洗:去掉脏数据、重复数据
  2. 增量更新:用逐笔数据更新快照
  3. 实时排序:维护价格-数量的映射关系

核心原则:订单簿不是每次都从头构建的,而是基于快照做增量更新。否则每秒几千笔成交,你的CPU会直接冒烟。

4.3 数据清洗——别小看这一步

我曾经在项目中遇到过一个问题:某天开盘后订单簿突然乱掉了,买一价居然比卖一价还高。查了半天,发现是交易所发了一条时间戳错乱的数据。

所以清洗阶段至少要做三件事:

  • 时间戳校验:丢弃明显异常的时间点(比如比当前时间晚5秒以上的数据)
  • 价格合理性检查:价格不能为负,不能超过涨跌停板
  • 数量非零检查:数量为0的委托其实代表撤单,要特殊处理

避坑指南:我曾经因为没处理「重复数据」导致订单簿多了一层。后来加了个简单的序列号去重,问题就解决了。记住:交易所偶尔会重发数据。

4.4 增量更新——这才是核心

说白了,订单簿维护的就是两个有序列表:买单列表(从高到低)和卖单列表(从低到高)。每次收到新数据,我们只需要做三件事:

// 伪代码示例
function updateOrderBook(snapshot, delta) {
  // 1. 处理撤单:从对应价格档位减去数量
  if (delta.type === 'cancel') {
    removeVolume(snapshot, delta.price, delta.volume)
  }
  
  // 2. 处理成交:从对应价格档位减去数量
  if (delta.type === 'trade') {
    removeVolume(snapshot, delta.price, delta.volume)
  }
  
  // 3. 处理新委托:在对应价格档位增加数量
  if (delta.type === 'new') {
    addVolume(snapshot, delta.price, delta.volume)
  }
  
  // 4. 如果某个价格档位数量归零,移除该档位
  cleanupEmptyLevels(snapshot)
}

你可能会问:为什么成交和撤单都是减数量?因为成交的本质就是「买单和卖单互相抵消」,体现在订单簿上就是两边同时减少。

4.5 实时排序——用什么数据结构?

这是很多新手会纠结的地方。我直接说结论:

数据结构 适用场景 我的评价
红黑树(std::map) 通用场景,价格档位多 稳定可靠,C++首选
跳表(Skip List) 并发读写频繁 适合高频交易场景
数组+二分查找 价格档位固定(如A股) 速度最快,但灵活性差
哈希表 仅查询,不排序 别用,订单簿必须有序

我个人习惯用红黑树。原因很简单:它保证了O(log n)的插入和删除,而且天然有序。你想想看,如果每次更新都要手动排序,那性能就完蛋了。

小技巧:对于A股这种价格档位固定的市场(最小变动单位0.01元),可以用数组+偏移量的方式。比如价格10.25元,直接映射到数组下标1025。这样查询就是O(1)的,爽得很。

4.6 完整的构建流程图

下面这张图是我自己总结的,基本上覆盖了所有场景:

订单簿构建流程图 原始行情数据 数据清洗 快照数据(基准) 增量数据(逐笔) 增量合并处理 实时订单簿 图:订单簿构建的核心流程,快照+增量=实时订单簿

4.7 实战中的几个坑

嗯,最后分享几个我亲身踩过的坑:

  • 数据延迟问题:交易所的快照和增量数据可能不是同时到达的。我建议先缓存增量数据,等快照到了再一起处理。
  • 精度问题:价格用浮点数?千万别。我见过因为浮点精度导致订单簿对不齐的惨案。用整数(比如乘以10000)存储。
  • 内存管理:如果每个价格档位都存一个对象,一天下来内存就爆了。用对象池复用,别频繁new/delete。

一句话总结:订单簿构建的本质就是「用快照做基准,用增量做修正」。把这个逻辑理清了,剩下的就是数据结构和性能优化的问题了。

好了,这一章的内容就到这里。记住我上面说的那些坑,你在实际开发中能少走不少弯路。