3、数据结构设计:内存中的订单簿表示、价格-数量映射、价格队列与时间优先原则

好,咱们直接进入正题。订单簿在内存里长什么样?这是每个做量化交易系统的人,都必须亲手搭过的地基。我见过不少新手,一上来就想着用数据库表去模拟订单簿,结果延迟高得离谱。说白了,内存里的数据结构,才是真正的战场。

3.1 核心数据结构:价格-数量映射

订单簿的本质是什么?就是一张「价格 → 数量」的映射表。买盘(Bid)和卖盘(Ask)各一张。每一档价格上,挂了多少钱,一目了然。

我个人习惯用 std::map(C++)或者 TreeMap(Java)来实现。为什么?因为订单簿天然需要按价格排序——买盘从高到低,卖盘从低到高。红黑树刚好满足这个需求。

核心要点:

  • 买盘(Bid):价格降序排列,谁出价高谁排前面
  • 卖盘(Ask):价格升序排列,谁要价低谁排前面
  • 每个价格档位上,维护一个总量(total volume)

举个例子,一个简单的内存表示:

// 伪代码示意
struct PriceLevel {
    double price;
    double totalVolume;
    std::deque<Order> orderQueue;  // 时间优先队列
};

std::map<double, PriceLevel, std::greater<double>> bidBook;  // 买盘
std::map<double, PriceLevel, std::less<double>>    askBook;  // 卖盘

嗯,这里要注意:double 作为 map 的 key 其实有精度风险。我在项目中遇到过因为浮点精度导致价格档位错乱的情况。后来我改用整数表示价格,比如把 100.25 存成 10025(乘以 100 或 10000),彻底避开了这个坑。

3.2 价格队列与时间优先原则

光有价格映射还不够。同一价格上,谁先来谁先成交——这就是时间优先原则。所以每个价格档位背后,必须挂一个队列。

队列用什么结构?我建议用 std::dequestd::list。为什么不用 std::vector?因为订单簿中间插入和删除太频繁了,vector 的移位操作会让你哭的。

我的经验:

我曾经在一个高频场景里用 vector 做订单队列,结果每秒几万笔撤单操作,vector 的 erase 操作直接把 CPU 干到 100%。换成 deque 之后,延迟降了 80%。

时间优先的具体逻辑:

  • 新订单进来,追加到对应价格档位的队列尾部
  • 撮合时,从队列头部开始匹配
  • 部分成交的订单,留在队列头部,数量减少
  • 完全成交或撤单,从队列头部弹出

你想想看,这其实跟排队买奶茶一个道理。先来的先服务,后来的排后面。只不过在订单簿里,这个「排队」是按价格分组的——每个价格都是一条独立的队伍。

3.3 整体架构图

下面这张图,是我自己画的内存订单簿结构。你看一眼就能明白整体逻辑:

内存订单簿数据结构 买盘 (Bid) - 降序 价格: 100.50 总量: 1500 队列: [O1→O2→O3] 价格: 100.25 总量: 800 队列: [O4→O5] 价格: 100.00 总量: 2000 队列: [O6→O7→O8→O9] ...... 更多价格档位 ...... 卖盘 (Ask) - 升序 价格: 100.75 总量: 1200 队列: [O10→O11] 价格: 101.00 总量: 900 队列: [O12→O13→O14] 价格: 101.50 总量: 3000 队列: [O15→O16] ...... 更多价格档位 ...... 撮合 每个价格档位 = 价格 + 总量 + 订单队列(时间优先)

3.4 实际操作中的避坑指南

光知道理论还不够,我把自己踩过的坑分享给你:

我曾经踩过的坑:

  • 并发问题:订单簿是高频读写的热点。我早期用了一把大锁,结果性能惨不忍睹。后来改用读写锁(RWLock)配合无锁队列,才把吞吐量提上去。
  • 内存碎片:频繁的 new/delete 订单对象,会导致内存碎片。我建议用对象池(Object Pool)来复用订单对象。
  • 价格档位合并:当某个价格档位的数量归零时,记得及时从 map 中删除。否则你的订单簿会越来越臃肿。

3.5 性能对比表

不同数据结构的性能差异,我整理了一张表,你参考一下:

操作 std::map (红黑树) std::unordered_map (哈希表) 跳表 (Skip List)
按价格查找 O(log n) O(1) 平均 O(log n)
按价格遍历(有序) O(n) 天然有序 O(n log n) 需排序 O(n) 天然有序
插入/删除 O(log n) O(1) 平均 O(log n)
获取最优价格(最高买/最低卖) O(1) 取 begin() O(n) 需遍历 O(1) 取头节点

我个人推荐用红黑树(std::map)。虽然哈希表查找更快,但订单簿需要频繁获取最优价格和范围遍历,哈希表在这两点上完全不行。

一个小技巧:

如果你用 C++,可以试试 boost::container::flat_map。它在数据量不大时(几百个价格档位),性能比红黑树还好,因为缓存局部性更好。我在一个低延迟项目里用过,效果不错。

好了,数据结构这块就聊到这儿。记住一句话:订单簿的设计,本质是在「查找速度」和「有序遍历」之间做权衡。选对了数据结构,你的系统就成功了一半。


专注资料整理