4. 订单生命周期管理:订单状态机

做市商报价引擎的核心,说白了就是跟订单打交道。你想想看,一个订单从进来开始,到最终被吃掉或者被取消,中间会经历哪些状态?这就是我们今天要聊的——订单生命周期管理。

我个人习惯把订单状态机比作一个「交通灯系统」。每个订单就像一辆车,在交易所这条路上行驶,红灯停、绿灯行、黄灯警告。搞不清楚这个状态流转,你的报价引擎迟早要出大问题。

4.1 订单的五大核心状态

在大多数交易所和做市系统中,订单状态基本可以归纳为以下五种。嗯,这里要注意,不同交易所的叫法可能略有差异,但本质是一样的。

状态 含义 触发条件
New 新订单,已提交但未成交 订单通过风控检查,进入订单簿
PartiallyFilled 部分成交 订单的一部分数量被吃掉了
Filled 完全成交 订单全部数量被吃掉
Cancelled 已取消 用户主动撤单或系统自动撤单
Rejected 已拒绝 风控不通过、参数非法、资金不足等

我在项目中遇到过最坑的一次,就是忽略了 PartiallyFilled 状态的处理。当时一个做市策略下了个大单,部分成交后,剩余部分被系统当成「已完成」给清理掉了。结果呢?仓位对不上,亏了不少钱。从那以后,我对状态机的敬畏心就上来了。

4.2 状态转换图

下面这张图,是我每次给团队做培训时必画的。它清晰地展示了订单从生到死的完整路径。

核心逻辑:订单一旦进入系统,只有一条「正向路径」——从 New 到 Filled。其他路径都是「异常退出」。

New PartiallyFilled Filled Rejected Cancelled 部分成交 风控拒绝 主动取消 剩余全成交 取消剩余 继续部分成交 实线:正向成交路径 虚线:取消/拒绝路径 自环:持续部分成交

我的经验:实际生产环境中,PartiallyFilled 状态可能会反复出现。比如一个限价单挂在盘口,被多个小单慢慢吃掉。这时候状态机必须支持「多次部分成交」的循环,直到完全成交或被取消。

4.3 订单取消与修改逻辑

做市商跟普通交易者最大的区别是什么?就是我们经常要「反悔」。行情一变,挂着的单子得赶紧撤或者改。这里面的门道不少。

4.3.1 取消订单

取消订单看似简单,其实有坑。我曾经在对接某交易所时,发现取消请求发出去后,订单已经被成交了。结果呢?取消失败,但仓位已经变了。

正确的做法是:

  1. 先查状态:发送取消请求前,先确认订单当前状态
  2. 幂等处理:取消请求可能重复发送,系统要能处理重复取消
  3. 异步确认:取消请求发出后,等待交易所确认回调,不要立即认为订单已取消
// 伪代码:订单取消逻辑
bool cancelOrder(OrderId id) {
    Order* order = getOrder(id);
    if (order == nullptr) return false;
    
    // 只有 New 和 PartiallyFilled 状态才能取消
    if (order->status != NEW && order->status != PARTIALLY_FILLED) {
        logWarning("订单 %s 当前状态 %d 不可取消", id, order->status);
        return false;
    }
    
    // 发送取消请求到交易所
    bool sent = exchange->sendCancel(id);
    if (!sent) {
        logError("取消请求发送失败: %s", id);
        return false;
    }
    
    // 标记为「取消中」状态(中间态)
    order->status = CANCELLING;
    return true;
}

注意:千万不要在收到取消确认前就释放订单资源。我曾经见过一个系统,取消请求刚发出去就把订单从内存里删了,结果交易所返回「取消失败,订单已成交」,导致成交记录丢失。

4.3.2 修改订单

修改订单,说白了就是「先取消旧单,再下新单」。但这里有个性能问题——如果每次都先取消再下单,中间会有时间窗口,容易被别人抢跑。

我建议的做法是:

  • 改价不改量:很多交易所支持直接修改价格,保留订单编号和成交优先级
  • 改量不改价:减少数量通常可以直接改,增加数量需要重新排队
  • 都改:建议拆成取消+新单,但要做好时间窗口保护

4.4 订单过期处理

做市商的订单通常有时效性。比如「GTC」(Good Till Cancelled)和「IOC」(Immediate or Cancel)。我个人习惯在订单结构体里加一个 expireTime 字段。

struct Order {
    OrderId id;
    double price;
    double quantity;
    double filledQuantity;
    OrderStatus status;
    int64_t createTime;    // 创建时间戳
    int64_t expireTime;    // 过期时间戳(0表示永不过期)
    OrderType type;        // GTC, IOC, FOK 等
};

过期处理的逻辑其实不复杂:

  1. 每个订单创建时,根据订单类型设置 expireTime
  2. 系统启动一个定时任务,每隔 100ms 扫描一次
  3. 发现 当前时间 > expireTime 且订单状态为 New 或 PartiallyFilled,自动发起取消

避坑指南:我曾经在扫描过期订单时,直接遍历了整个订单簿。结果订单量一上来,扫描耗时飙升,影响了主线程的报价性能。后来改成用「最小堆」来管理过期时间,每次只检查堆顶的订单,性能提升了一个数量级。

4.5 状态机实现的几个关键点

最后,总结一下我在实现订单状态机时的一些心得:

  • 状态转换必须是单向的:比如 Filled 状态不能回到 PartiallyFilled,这是铁律
  • 引入中间态:比如 CANCELLING、PENDING_NEW,能有效防止并发问题
  • 日志要打全:每次状态转换都要记录时间、旧状态、新状态、触发原因
  • 状态机要可回放:万一系统崩溃,能从日志中恢复所有订单的最终状态

嗯,订单生命周期管理这块,说白了就是「规矩」二字。你把状态转换的规矩定好了,代码写严谨了,后面做策略开发、风控对接都会顺畅很多。我见过太多团队,一开始觉得状态机简单,随便写写,结果上线后各种灵异事件——订单丢了、重复成交了、仓位对不上了。说白了,都是状态机没搞明白。

一个小建议:刚开始做的时候,可以用一个「状态转换矩阵」来验证你的逻辑。比如行是旧状态,列是新状态,矩阵里标记哪些转换是合法的。这样一眼就能看出有没有遗漏。


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