第三章 订单管理系统(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 的核心流程。从订单创建到最终成交或撤销,每一步都有状态转换和路由决策。
这张图看起来简单,但每个环节都有细节。比如路由决策,不是简单的轮询,而是要考虑交易所的当前延迟、深度、手续费,甚至历史成交率。我一般会维护一个「交易所评分表」,每 10 秒更新一次,路由时直接取评分最高的。
总结一句话:OMS 的核心就三件事——管好状态、选对路、维护好订单簿。这三件事做好了,做市系统就稳了一半。