第三章 订单管理系统(OMS):订单生命周期管理、订单状态机设计、订单路由策略、撤单与改单逻辑、订单簿维护

做市商系统里,OMS 是心脏。订单从哪来、到哪去、怎么改、怎么撤,全得靠它管。我见过不少团队,策略写得漂亮,结果 OMS 一塌糊涂,实盘跑起来各种漏单、重复下单。说白了,OMS 稳,系统才稳。

3.1 订单生命周期管理

一个订单从诞生到消亡,会经历几个固定阶段。我个人习惯把它分成五个核心状态:

  • 创建(Created):策略发出指令,OMS 收到后生成订单对象。此时还没发往交易所。
  • 已发送(Sent):订单通过网络发到了交易所网关,但还没收到确认。
  • 已确认(Acknowledged):交易所返回了订单 ID,表示正式受理。
  • 部分成交(Partially Filled):订单被吃掉了部分数量,剩余部分还在挂单。
  • 完全成交(Filled):订单全部成交,生命周期结束。
  • 已撤销(Cancelled):主动撤单或被动失效。
  • 已拒绝(Rejected):交易所认为订单不合法,比如价格超出涨跌停。

嗯,这里要注意:部分成交状态很容易被忽略。很多新手只盯着「成交」和「未成交」,结果做市策略里挂单被吃掉一半,剩余部分没管,导致仓位对不上。我在项目中遇到过,一个做市商因为没处理部分成交,半小时内仓位偏差了 20 万美金。

核心原则:每个订单必须记录完整的状态变迁日志。出问题时,这是唯一的排查依据。

3.2 订单状态机设计

状态机是 OMS 的骨架。我建议用有限状态机(FSM)来建模,每个状态只允许特定的转换。举个例子:

// 伪代码示例:订单状态机定义
enum OrderState {
    CREATED,
    SENT,
    ACKNOWLEDGED,
    PARTIALLY_FILLED,
    FILLED,
    CANCELLED,
    REJECTED
}

// 允许的转换规则
Map<OrderState, List<OrderState>> transitions = {
    CREATED: [SENT, REJECTED],
    SENT: [ACKNOWLEDGED, REJECTED],
    ACKNOWLEDGED: [PARTIALLY_FILLED, FILLED, CANCELLED],
    PARTIALLY_FILLED: [PARTIALLY_FILLED, FILLED, CANCELLED],
    FILLED: [],  // 终态
    CANCELLED: [], // 终态
    REJECTED: []  // 终态
};

为什么会这样设计?因为订单一旦进入终态(FILLED、CANCELLED、REJECTED),就不允许再变。我曾经见过一个系统,因为状态机没锁死,撤单后还能收到成交回报,结果资金计算全乱套了。

避坑指南:我曾经在状态机里漏掉了「SENT -> REJECTED」这条路径。结果网络超时后,订单既没确认也没拒绝,卡在 SENT 状态整整一天。后来我加了个超时机制:SENT 状态超过 5 秒没收到确认,自动标记为 TIMEOUT,然后走重试或撤单流程。

3.3 订单路由策略

做市商通常对接多个交易所。订单往哪送,是个技术活。我常用的路由策略有三种:

策略 适用场景 优缺点
价格优先 追求最优成交价 成交质量高,但可能延迟大
流动性优先 需要快速成交 速度快,但可能牺牲价格
成本优先 控制手续费 省钱,但可能错过机会

你想想看,如果做市策略同时在 Binance 和 OKX 挂单,哪个先成交?其实没法预测。所以我一般用「智能路由」:先查两个交易所的盘口深度和手续费,然后动态分配订单比例。比如 Binance 深度好但手续费高,那就少送点;OKX 深度差但手续费低,那就多送点。

注意:路由策略不能只看价格。我记得有一次,某个交易所的 API 延迟突然飙到 500ms,结果订单全堵在那了。后来我加了个「健康检查」:路由前先测交易所的响应时间,超过阈值就自动切换。

3.4 撤单与改单逻辑

撤单和改单,是 OMS 里最容易出 bug 的地方。先说撤单:

  • 主动撤单:策略判断需要撤单,OMS 发出 Cancel 指令。
  • 被动撤单:比如交易所重启、网络断开,订单自动失效。
  • 条件撤单:比如订单超过 10 秒没成交,自动撤掉。

改单更复杂。很多交易所不支持直接改单,只能先撤再下。我建议的做法是:

// 改单流程:先撤后下
async function modifyOrder(orderId, newPrice, newQuantity) {
    // 1. 发送撤单请求
    await cancelOrder(orderId);
    
    // 2. 等待撤单确认(关键!)
    //    这里要轮询或监听回调,确认原订单已撤销
    await waitForCancellation(orderId);
    
    // 3. 下新订单
    const newOrder = await placeOrder(newPrice, newQuantity);
    
    return newOrder;
}

嗯,这里有个坑:撤单确认没到就下新单。我曾经遇到过,撤单指令发出后 200ms 就下了新单,结果原订单和新订单同时存在,导致重复挂单。后来我强制要求:必须收到交易所的 CancelAck 消息,才能下新单。

经验之谈:撤单和改单的并发控制,建议用「订单锁」。每个订单 ID 对应一个锁,同一时间只允许一个操作。这样可以避免竞态条件。

3.5 订单簿维护

订单簿(Order Book)是 OMS 的实时数据核心。做市商需要维护本地的订单簿副本,用来做策略决策。我一般用红黑树或跳表来存储,因为需要快速查询最优买卖价。

订单簿的维护逻辑:

  • 增量更新:交易所推送的每个成交或挂单变化,都更新本地订单簿。
  • 全量快照:每隔一段时间(比如 1 分钟),拉一次全量数据做校验。
  • 数据对齐:如果本地和交易所的订单簿不一致,以交易所为准。

为什么会不一致?说白了,网络丢包或延迟会导致本地漏掉一些更新。我建议的做法是:每次收到增量更新时,都带上一个序列号(Sequence Number)。如果发现序列号不连续,说明丢了数据,立即请求全量快照。

小技巧:订单簿的深度信息,不要只存价格和数量。我习惯额外存一个「累计深度」字段,这样策略在计算「吃掉多少量才能把价格推到目标位」时,不用每次都遍历。

3.6 核心逻辑流程图

下面这张图,概括了 OMS 的核心流程。从订单创建到最终成交或撤销,每一步都有状态转换和路由决策。

OMS 订单生命周期与路由流程 创建订单 路由决策 交易所 A 交易所 B 状态机管理 订单簿维护 终态(成交/撤销) 说明: 1. 订单创建后进入路由决策 2. 路由决策选择目标交易所 3. 状态机全程跟踪订单状态 4. 订单簿实时更新 5. 最终进入终态

这张图看起来简单,但每个环节都有细节。比如路由决策,不是简单的轮询,而是要考虑交易所的当前延迟、深度、手续费,甚至历史成交率。我一般会维护一个「交易所评分表」,每 10 秒更新一次,路由时直接取评分最高的。

总结一句话:OMS 的核心就三件事——管好状态、选对路、维护好订单簿。这三件事做好了,做市系统就稳了一半。

专注资料整理