3. 核心数据结构设计:价格队列模型、价格-订单映射、订单生命周期管理
做量化交易系统,订单簿是绕不开的核心。说白了,它就是交易所用来记录所有挂单的账本。你想想看,每秒成千上万的订单涌进来,怎么存、怎么查、怎么改,直接决定了你的系统能不能跑得快。
我个人习惯把订单簿拆成三个核心模块:价格队列模型、价格-订单映射、订单生命周期管理。这三个东西搞明白了,订单簿的骨架就搭好了。
3.1 价格队列模型:按价格分桶
先问个问题:为什么不用一个巨大的数组存所有订单?
因为价格是离散的。每个价格点上的订单,其实是一堆人在排队。比如买一价 100.50 元,可能有 10 个人挂单,每人 100 股。这些订单之间没有优先级差异(按时间优先),所以用队列最合适。
我见过不少新手直接拿 std::vector 往里塞,结果撤单时遍历一遍,性能直接崩了。正确的做法是:每个价格对应一个队列。
代码示例(C++ 风格):
// 价格队列:每个价格点一个队列
struct PriceLevel {
double price;
std::deque<Order> orders; // 先进先出
};
// 买盘:价格从高到低排序
std::map<double, PriceLevel, std::greater<double>> bidLevels;
// 卖盘:价格从低到高排序
std::map<double, PriceLevel, std::less<double>> askLevels;
这里用 std::map 而不是 unordered_map,是因为我们需要按价格顺序遍历。买盘从高到低,卖盘从低到高,这样才能快速找到最优价格。
TreeMap;Python 的话,sortedcontainers 库的 SortedDict 是个不错的选择。
3.2 价格-订单映射:快速定位
光有价格队列还不够。你想想,用户要撤单怎么办?他只知道订单 ID,不知道价格。如果遍历所有价格队列去找,那复杂度就是 O(N),N 是价格层数乘以每层订单数。这在高频场景下根本扛不住。
所以我们需要一个反向索引:订单 ID → 价格。
我在项目中遇到过一个问题:撤单请求来了,我得先找到这个订单在哪个价格队列里,然后从队列里删掉它。如果队列是 std::deque,删除中间元素是 O(n) 的。后来我换成了 std::list,并且把迭代器存下来,删除就变成 O(1) 了。
代码示例:
// 订单结构体
struct Order {
int64_t id;
double price;
int64_t quantity;
int64_t timestamp;
// 指向队列中位置的迭代器
std::list<Order>::iterator it;
};
// 价格-订单映射
std::unordered_map<int64_t, Order> orderMap;
这样设计的好处是:
- 新增订单:插入到价格队列尾部,同时记录迭代器到
orderMap - 撤单:从
orderMap拿到迭代器,直接 O(1) 删除 - 查单:通过
orderMap直接定位,不用遍历
std::list 是安全的,但 std::deque 在中间插入会导致迭代器失效。我建议用 std::list 或自己实现一个 intrusive list。
3.3 订单生命周期管理:从生到死
一个订单从创建到成交或撤单,会经历几个状态。我习惯用状态机来管理:
| 状态 | 说明 | 触发动作 |
|---|---|---|
| NEW | 新订单,刚进入系统 | 收到下单请求 |
| PARTIAL | 部分成交 | 与对手单匹配,未完全成交 |
| FILLED | 完全成交 | 剩余数量为 0 |
| CANCELLED | 已撤单 | 用户主动撤单 |
| REJECTED | 被拒绝 | 风控检查不通过 |
状态流转图:
我曾经踩过一个坑:订单部分成交后,剩余数量更新了,但忘记更新价格队列里的数量统计。结果盘口显示的量是错的,被交易所警告了。所以每次状态变化,都要同步更新三个地方:
- 订单本身:剩余数量、状态
- 价格队列:该价格的总量、订单数
- 映射表:如果价格变了(比如改单),要重新插入队列
onOrderNew、onOrderFill。这样风控、统计、日志都可以挂上去,代码解耦得很干净。
3.4 三者如何协作
这三个模块不是孤立的。我画个图帮你理解:
举个例子你就明白了:
- 一个新订单进来,先通过生命周期管理创建状态为 NEW
- 然后根据价格,插入到价格队列模型中对应的队列尾部
- 同时把订单 ID 和迭代器存入价格-订单映射
- 如果后续有撤单请求,通过映射找到迭代器,直接从队列删除,状态改为 CANCELLED
嗯,这套设计我用了好几年,在多个项目里验证过。吞吐量做到每秒 10 万笔订单没问题,前提是内存要够,别频繁 GC。
boost::lockfree;Java 的话 ConcurrentLinkedDeque 是个选择。
好了,核心数据结构就这些。说白了就是三个东西:按价格分桶存订单、用映射表快速定位、用状态机管理生命周期。搞懂了它们,订单簿的骨架就有了。