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 撮合引擎基础逻辑:怎么把买卖双方凑到一起

撮合引擎,就是不断检查买单和卖单,看能不能对上。逻辑其实不复杂,但性能要求极高。

基本流程是这样的:

  1. 收到一个新订单(假设是买单)。
  2. 检查卖盘的最优价格(最低卖价)。如果买单价格 >= 最低卖价,就可以撮合。
  3. 从卖盘最优价格队列的头部取出一个卖单,比较数量。
  4. 如果买单数量 >= 卖单数量,卖单完全成交,买单剩余数量继续撮合。
  5. 如果买单数量 < 卖单数量,买单完全成交,卖单剩余数量放回队列头部。
  6. 重复步骤2-5,直到买单价格 < 最低卖价,或者买单完全成交。
  7. 如果买单还有剩余,就插入到买盘对应的价格队列中。

嗯,这里要注意一个坑:撮合过程中,订单簿是实时变化的。每次撮合完一个对手单,都要更新订单簿。我曾经在项目中遇到过,撮合时忘了更新订单簿,结果同一个卖单被撮合了两次,直接导致负数持仓。

避坑指南:撮合引擎一定要加锁,或者用无锁队列。多线程环境下,订单簿的并发访问是最大的风险点。我曾经因为锁粒度太粗,导致撮合性能直接腰斩。后来改成细粒度锁,每个价格队列一把锁,才把性能提上来。

3.4 核心流程图:订单生命周期与撮合

下面这张图,我画了订单从进入系统到最终结束的完整流程。你可以对照着看,会更清楚。

订单生命周期与撮合流程 新单 (New) 风控校验通过 撮合引擎匹配 检查买卖盘,按价格时间优先撮合 完全匹配 完全成交 (Filled) 部分匹配 部分成交 (Partial) 剩余继续撮合 用户撤单 撤单 (Cancel) 未匹配,挂单 挂单等待

这张图里,你可以看到订单的完整流转。新单进来,先过风控,然后进撮合引擎。撮合引擎根据价格和时间,决定是全部成交、部分成交,还是挂单等待。如果用户想撤单,随时可以取消。

3.5 实战中的几个关键点

最后,分享几个我在实战中踩过的坑:

  • 订单ID生成:一定要全局唯一,而且最好带时间戳。我习惯用雪花算法,既保证唯一性,又能按时间排序。
  • 状态变更通知:订单状态变了,要实时通知客户端。用WebSocket推送,别用轮询。轮询的延迟和带宽消耗,做市商系统根本扛不住。
  • 撤单处理:撤单请求进来,要先检查订单是否还在订单簿里。如果已经成交了,就不能撤了。这个逻辑看似简单,但并发情况下很容易出错。
  • 日志记录:每个订单的每次状态变更,都要记录日志。出了问题,全靠日志回溯。我见过有人不记日志,结果订单丢了都不知道怎么丢的。

我的建议:刚开始做订单管理模块时,别急着优化性能。先把状态机写对,把边界情况处理好。性能可以后面再优化,但状态机错了,整个系统就废了。

好了,订单生命周期和撮合引擎的基础逻辑,就聊到这儿。这些东西看着简单,但真正写起来,细节特别多。你动手写一遍,就会明白我说的那些坑到底有多坑了。


公众号:蓝海资料掘金营,微信deep3321