3. 订单管理核心:订单生命周期与撮合引擎
做市商系统里,订单管理是心脏。我见过不少团队,策略模型跑得飞起,结果订单管理一塌糊涂,最后亏得莫名其妙。说白了,订单管不好,策略再牛也是白搭。
今天咱们就聊聊订单的生命周期、订单簿的数据结构,还有撮合引擎的基础逻辑。这些都是基本功,但也是最容易出坑的地方。
3.1 订单生命周期:从生到死的四个阶段
一个订单从创建到结束,无非就四种状态:新单、部分成交、完全成交、撤单。嗯,听起来简单,但实际处理起来,细节多得吓人。
| 状态 | 含义 | 触发条件 |
|---|---|---|
| 新单 (New) | 订单刚进入系统,还没参与撮合 | 客户端提交订单,风控校验通过 |
| 部分成交 (Partially Filled) | 订单的一部分数量被撮合了,还剩一些挂着 | 撮合引擎匹配到对手单,但未完全吃掉 |
| 完全成交 (Filled) | 订单全部数量都成交了 | 撮合引擎匹配到足够数量的对手单 |
| 撤单 (Cancelled) | 订单被用户或系统主动取消 | 用户发起撤单,或系统风控触发 |
核心要点:订单状态流转是单向的,不能回退。比如部分成交后,不可能再变回新单。我曾经在项目中遇到过有人把状态机写成了双向的,结果撮合逻辑全乱套了。
这里有个小细节:部分成交的订单,剩余数量还在订单簿里挂着,继续等待撮合。直到剩余数量也被吃掉,或者用户主动撤单,这个订单才算真正结束。
3.2 订单簿数据结构:价格队列与时间优先
订单簿,说白了就是所有挂单的集合。买盘叫Bid,卖盘叫Ask。每个价格上,都排着一堆订单。
那怎么排呢?两个原则:
- 价格优先:买单出价高的排前面,卖单出价低的排前面。
- 时间优先:价格相同的情况下,先来的订单排前面。
你想想看,如果价格优先和时间优先搞反了,会怎样?我见过一个交易所,时间优先没处理好,结果大户总是插队,小散户根本吃不到单子,最后全跑了。
数据结构上,我习惯用价格队列的方式。每个价格对应一个FIFO队列,队列里按时间顺序排着订单。这样撮合的时候,直接从最优价格队列的头部取单就行,效率很高。
// 伪代码示例:订单簿数据结构
struct OrderBook {
// 买盘:价格从高到低排序
map<double, queue<Order>, greater<double>> bids;
// 卖盘:价格从低到高排序
map<double, queue<Order>, less<double>> asks;
};
struct Order {
string order_id;
double price;
int quantity;
int timestamp; // 时间戳,用于时间优先
};
我的习惯:用红黑树(C++里的std::map)来维护价格队列。插入和删除都是O(log n),性能足够。如果追求极致性能,可以考虑跳表(Skip List),但实现复杂度会高不少。
3.3 撮合引擎基础逻辑:怎么把买卖双方凑到一起
撮合引擎,就是不断检查买单和卖单,看能不能对上。逻辑其实不复杂,但性能要求极高。
基本流程是这样的:
- 收到一个新订单(假设是买单)。
- 检查卖盘的最优价格(最低卖价)。如果买单价格 >= 最低卖价,就可以撮合。
- 从卖盘最优价格队列的头部取出一个卖单,比较数量。
- 如果买单数量 >= 卖单数量,卖单完全成交,买单剩余数量继续撮合。
- 如果买单数量 < 卖单数量,买单完全成交,卖单剩余数量放回队列头部。
- 重复步骤2-5,直到买单价格 < 最低卖价,或者买单完全成交。
- 如果买单还有剩余,就插入到买盘对应的价格队列中。
嗯,这里要注意一个坑:撮合过程中,订单簿是实时变化的。每次撮合完一个对手单,都要更新订单簿。我曾经在项目中遇到过,撮合时忘了更新订单簿,结果同一个卖单被撮合了两次,直接导致负数持仓。
避坑指南:撮合引擎一定要加锁,或者用无锁队列。多线程环境下,订单簿的并发访问是最大的风险点。我曾经因为锁粒度太粗,导致撮合性能直接腰斩。后来改成细粒度锁,每个价格队列一把锁,才把性能提上来。
3.4 核心流程图:订单生命周期与撮合
下面这张图,我画了订单从进入系统到最终结束的完整流程。你可以对照着看,会更清楚。
这张图里,你可以看到订单的完整流转。新单进来,先过风控,然后进撮合引擎。撮合引擎根据价格和时间,决定是全部成交、部分成交,还是挂单等待。如果用户想撤单,随时可以取消。
3.5 实战中的几个关键点
最后,分享几个我在实战中踩过的坑:
- 订单ID生成:一定要全局唯一,而且最好带时间戳。我习惯用雪花算法,既保证唯一性,又能按时间排序。
- 状态变更通知:订单状态变了,要实时通知客户端。用WebSocket推送,别用轮询。轮询的延迟和带宽消耗,做市商系统根本扛不住。
- 撤单处理:撤单请求进来,要先检查订单是否还在订单簿里。如果已经成交了,就不能撤了。这个逻辑看似简单,但并发情况下很容易出错。
- 日志记录:每个订单的每次状态变更,都要记录日志。出了问题,全靠日志回溯。我见过有人不记日志,结果订单丢了都不知道怎么丢的。
我的建议:刚开始做订单管理模块时,别急着优化性能。先把状态机写对,把边界情况处理好。性能可以后面再优化,但状态机错了,整个系统就废了。
好了,订单生命周期和撮合引擎的基础逻辑,就聊到这儿。这些东西看着简单,但真正写起来,细节特别多。你动手写一遍,就会明白我说的那些坑到底有多坑了。
公众号:蓝海资料掘金营,微信deep3321