4. 订单管理系统(OMS):订单生命周期管理、订单状态机设计、订单路由策略、订单簿数据结构设计

订单管理系统,简称OMS,是整个暗池交易系统的核心中枢。说白了,它就是负责处理订单从生到死的全过程。我参与过好几个暗池项目,每次重构OMS都是最头疼的环节。为什么?因为订单的状态流转一旦出错,轻则丢单,重则引发交易纠纷。今天我们就来拆解OMS的四个核心模块。

4.1 订单生命周期管理

一个订单在暗池里会经历哪些阶段?我习惯把它分成三个阶段:接收前、处理中、完成/取消。每个阶段都有严格的约束。

核心原则:订单一旦进入系统,状态变更必须通过状态机控制,不允许直接修改状态字段。

订单的生命周期大致如下:

  • 新建(New):客户端提交订单,系统校验通过后进入此状态。
  • 待成交(Pending):订单已进入订单簿,等待匹配。这是暗池中最常见的状态。
  • 部分成交(Partially Filled):匹配引擎撮合了一部分数量,剩余部分继续等待。
  • 完全成交(Filled):订单全部成交,生命周期结束。
  • 已取消(Cancelled):用户主动撤销,或系统因超时等原因强制撤销。
  • 已拒绝(Rejected):校验失败,比如价格超出限制、数量不合法等。

嗯,这里要注意:暗池里订单的生命周期比交易所要简单一些。因为暗池没有公开的深度行情,所以订单不会出现「部分成交后修改价格」这种复杂操作。我见过有些团队把订单状态设计得过于复杂,结果维护成本极高。

4.2 订单状态机设计

状态机是OMS的骨架。我个人习惯用有限状态机(FSM)来建模。为什么?因为状态和事件是有限的,而且每个状态转移都有明确的触发条件。

下面是我常用的订单状态机设计:

当前状态 触发事件 下一状态 备注
New 校验通过 Pending 进入订单簿
New 校验失败 Rejected 返回错误信息
Pending 匹配到对手单 Partially Filled 部分成交
Pending 用户取消 Cancelled 主动撤销
Partially Filled 继续匹配 Partially Filled 剩余部分继续等待
Partially Filled 剩余全部匹配 Filled 完全成交
Partially Filled 用户取消剩余 Cancelled 剩余部分撤销

你想想看,如果不用状态机,直接用if-else判断状态流转,代码会变成什么样?我曾经接手过一个项目,订单状态用int表示,到处是switch-case,结果新增一个「冻结」状态时,改了十几个地方才搞定。用状态机的话,只需要在状态定义里加一条转移规则就行。

避坑指南:状态机一定要支持「回滚」操作。比如订单在Pending状态时,如果系统检测到重复提交,应该能回滚到New状态重新校验。我曾经因为没做回滚,导致一笔大单被重复处理了两次,差点引发资金问题。

4.3 订单路由策略

暗池的订单路由和交易所不太一样。交易所通常只有一个订单簿,而暗池可能有多个流动性池子。比如,一个暗池可能同时对接了多个券商、多个算法交易平台。订单该往哪送?这就是路由策略要解决的问题。

我常用的路由策略有三种:

  1. 最优价格路由:把订单送到当前价格最优的池子。适合市价单。
  2. 流动性均衡路由:根据各池子的深度和成交量,动态分配订单。适合大单,避免冲击市场。
  3. 固定优先级路由:按预设的优先级顺序尝试,直到找到可成交的池子。适合对特定池子有偏好的用户。

举个例子,假设我们有三个流动性池子:A(深度大)、B(深度中等)、C(深度小)。一个1000股的买单进来,最优价格路由会直接送到A,因为A的价格最优。但流动性均衡路由可能会把订单拆成三份:500股给A,300股给B,200股给C。这样能避免A的价格被瞬间拉高。

注意:路由策略必须考虑「延迟」。暗池对延迟极其敏感,如果路由逻辑太复杂,反而会拖慢成交速度。我建议把路由策略做成可配置的,上线后根据实际表现调整。

4.4 订单簿数据结构设计

订单簿是OMS的心脏。暗池的订单簿和交易所的订单簿有本质区别:交易所的订单簿是公开的,而暗池的订单簿是隐藏的。也就是说,用户看不到深度,只能看到自己的订单状态。

数据结构上,我推荐使用跳表(Skip List)红黑树。为什么不用数组?因为订单簿需要频繁的插入和删除操作,数组的O(n)复杂度扛不住高频场景。

下面是一个简化的订单簿数据结构示例:

// 订单簿节点
struct OrderBookNode {
    double price;           // 价格
    int64_t quantity;       // 数量
    uint64_t order_id;      // 订单ID
    OrderType type;         // 买单/卖单
    uint64_t timestamp;     // 时间戳
    OrderBookNode* next;    // 链表指针
};

// 订单簿
class OrderBook {
private:
    OrderBookNode* buy_head;   // 买单链表头(价格降序)
    OrderBookNode* sell_head;  // 卖单链表头(价格升序)
    std::unordered_map<uint64_t, OrderBookNode*> order_map; // 订单ID到节点的映射
public:
    void add_order(Order order);
    void cancel_order(uint64_t order_id);
    void match_order();  // 匹配引擎调用
};

嗯,这里要注意:暗池的订单簿不需要维护「深度快照」,因为用户看不到。但内部必须维护一个价格-数量映射,用于匹配引擎快速查找对手单。我习惯用std::map来维护价格层级,因为它是红黑树实现,插入和查找都是O(log n)。

下面这张图展示了订单簿的核心结构:

暗池订单簿数据结构 买单链表(价格降序) 价格: 100.50 | 数量: 500 订单ID: 1001 | 时间戳: T1 价格: 100.30 | 数量: 300 订单ID: 1002 | 时间戳: T2 价格: 100.10 | 数量: 200 订单ID: 1003 | 时间戳: T3 卖单链表(价格升序) 价格: 100.60 | 数量: 400 订单ID: 2001 | 时间戳: T4 价格: 100.80 | 数量: 250 订单ID: 2002 | 时间戳: T5 价格: 101.00 | 数量: 150 订单ID: 2003 | 时间戳: T6 匹配引擎 价格交叉检查 数量匹配 图:暗池订单簿采用双向链表结构,买单按价格降序排列,卖单按价格升序排列 匹配引擎在中间区域检查价格交叉,当买单价格 ≥ 卖单价格时触发成交

订单簿的核心操作有三个:插入、删除、查询。插入时,按价格找到合适的位置;删除时,通过订单ID的哈希映射快速定位节点;查询时,从链表头开始遍历,找到价格最优的对手单。

个人经验:订单簿的并发控制一定要做好。暗池里多个线程可能同时操作订单簿,比如匹配线程和取消线程。我建议用读写锁:读操作(查询)可以并发,写操作(插入/删除)必须互斥。千万别用全局锁,否则性能会崩。

好了,订单管理系统的核心内容就这些。状态机保证订单状态不乱,路由策略保证订单送到正确的地方,订单簿数据结构保证匹配引擎能高效工作。这三者配合好了,OMS才能稳定运行。

蓝海数据掘金营,专注资料整理