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::deque 或 std::list。为什么不用 std::vector?因为订单簿中间插入和删除太频繁了,vector 的移位操作会让你哭的。
我的经验:
我曾经在一个高频场景里用 vector 做订单队列,结果每秒几万笔撤单操作,vector 的 erase 操作直接把 CPU 干到 100%。换成 deque 之后,延迟降了 80%。
时间优先的具体逻辑:
- 新订单进来,追加到对应价格档位的队列尾部
- 撮合时,从队列头部开始匹配
- 部分成交的订单,留在队列头部,数量减少
- 完全成交或撤单,从队列头部弹出
你想想看,这其实跟排队买奶茶一个道理。先来的先服务,后来的排后面。只不过在订单簿里,这个「排队」是按价格分组的——每个价格都是一条独立的队伍。
3.3 整体架构图
下面这张图,是我自己画的内存订单簿结构。你看一眼就能明白整体逻辑:
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。它在数据量不大时(几百个价格档位),性能比红黑树还好,因为缓存局部性更好。我在一个低延迟项目里用过,效果不错。
好了,数据结构这块就聊到这儿。记住一句话:订单簿的设计,本质是在「查找速度」和「有序遍历」之间做权衡。选对了数据结构,你的系统就成功了一半。