OMS(Order Management System)是把「目标仓位」变成「实际成交」的那一层。它看起来简单——收到指令、发单、收回报——但生产环境中的大部分疑难杂症都出在这里:订单状态机漏了一个状态、撤单和成交的竞态、部分成交后的残单处理、断线重连后的订单恢复。
真正难的地方在于OMS 必须处理一个不可靠的外部世界:交易所会拒单、会延迟回报、会部分成交、会连接中断。策略层假设的「下单即成交」在 OMS 层必须被还原成一套完整的状态机,任何一个状态的遗漏都会导致持仓对不上。
本文按「职责 → 状态机 → 订单类型 → 执行算法 → 最优执行 → 高级订单 → 协议 → 时序 → 评估」的顺序展开。组合层面的权重生成在 组合优化与风险模型 中讨论,本文从「收到目标仓位」开始。
目录
- OMS 的职责与边界
- 订单生命周期状态机
- 订单类型与交易所规则
- 拆单算法:TWAP、VWAP、POV
- Almgren-Chriss 与最优执行
- 冰山单与隐藏单
- FIX 协议与报文结构
- 撤改单的时序竞态
- 执行质量评估
1. OMS 的职责与边界
OMS 的职责边界需要划清楚,否则会与策略层和风控层产生重复或遗漏:
| 职责 | 属于 | 说明 |
|---|---|---|
| 目标仓位生成 | 策略层 | OMS 不关心为什么买 |
| 事前风控校验 | 风控层 | OMS 调用但不实现规则 |
| 拆单与择时 | OMS | 决定怎么买、买多少笔 |
| 订单状态维护 | OMS | 唯一事实来源 |
| 持仓与资金核算 | 清结算层 | OMS 只上报成交 |
| 通道管理与重连 | OMS | 与柜台/交易所的会话 |
策略层 ──TargetPosition──► OMS ──Order──► 风控 ──Order──► 柜台 ──► 交易所
▲ │
└────────── ExecutionReport ◄───────┘
OMS 应该是无状态可重建的:任何时候重启,都能从柜台查询订单状态并恢复。做不到这一点,一次进程崩溃就意味着持仓对不上。
2. 订单生命周期状态机
订单状态机是 OMS 的核心。交易所回报的每一种状态都必须被映射,缺失任何一条都会导致统计错误:
PENDING_NEW ──► NEW ──► PARTIALLY_FILLED ──► FILLED
│ │ │
│ │ └──► CANCELLED(撤掉剩余)
│ └──► PENDING_CANCEL ──► CANCELLED
└──► REJECTED
from enum import Enum
class OrderState(Enum):
PENDING_NEW = 1 # 已发送,未收到确认
NEW = 2 # 交易所已接受
PARTIALLY_FILLED = 3 # 部分成交
FILLED = 4 # 全部成交
PENDING_CANCEL = 5 # 已发撤单,未确认
CANCELLED = 6 # 已撤销
REJECTED = 7 # 被拒绝
EXPIRED = 8 # 过期(如当日未成交)
TRANSITIONS = {
OrderState.PENDING_NEW: {OrderState.NEW, OrderState.REJECTED},
OrderState.NEW: {OrderState.PARTIALLY_FILLED, OrderState.FILLED,
OrderState.PENDING_CANCEL, OrderState.EXPIRED},
OrderState.PARTIALLY_FILLED: {OrderState.FILLED, OrderState.PENDING_CANCEL},
OrderState.PENDING_CANCEL: {OrderState.CANCELLED, OrderState.FILLED},
}
最容易漏掉的是 PENDING_CANCEL 状态。发出撤单请求后,订单既可能被成功撤销,也可能在撤单到达前刚好全部成交。如果 OMS 在发出撤单请求时就把订单标记为 CANCELLED,那么后来的成交回报就会被丢弃,导致持仓少算。
订单状态的持久化是可靠性的关键:每一笔订单的每一次状态变更都要落盘,且要能幂等地重放,相关设计参考 分布式幂等与可靠性 。
3. 订单类型与交易所规则
不同市场支持的订单类型差异很大,OMS 必须做能力映射:
| 订单类型 | 上交所/深交所 | 中金所 | 说明 |
|---|---|---|---|
| 限价单 | 支持 | 支持 | 基础类型 |
| 市价单 | 最优五档即时成交剩余撤销 | 支持 | A 股无真市价单 |
| IOC | 部分支持 | 支持 | 立即成交剩余撤销 |
| FOK | 部分支持 | 支持 | 全部成交否则撤销 |
| 止损单 | 不支持 | 部分支持 | 需在 OMS 侧模拟 |
| 冰山单 | 不支持 | 不支持 | 需在 OMS 侧模拟 |
A 股的一个关键限制:没有真正的市价单。所谓「市价委托」实际是「最优五档即时成交剩余撤销」,五档之外的量无法成交。OMS 如果要保证成交,必须自己实现「追价」逻辑:
def aggressive_limit_price(order_book, side, ticks=3): # 对手价加若干档模拟市价
if side == 'BUY':
levels = sorted(order_book.asks)[:ticks]
return max(levels) if levels else None
else:
levels = sorted(order_book.bids, reverse=True)[:ticks]
return min(levels) if levels else None
涨停板时对手价不存在(没有卖单),此时任何买单都无法成交,OMS 必须识别并停止发单,否则会不断产生废单。
4. 拆单算法:TWAP、VWAP、POV
大单直接发出会造成巨大冲击成本,必须拆成小单分散执行。三种基础算法:
| 算法 | 拆分依据 | 优点 | 缺点 |
|---|---|---|---|
| TWAP | 时间均匀 | 简单、可预测 | 忽略成交量分布 |
| VWAP | 历史成交量分布 | 贴近市场均价 | 依赖成交量预测 |
| POV | 实时成交量比例 | 自适应 | 成交量小时执行慢 |
TWAP 最简单:把总量按时间等分。
def twap_schedule(total_qty, start_ts, end_ts, n_slices):
step = (end_ts - start_ts) / n_slices
qty_per_slice = total_qty // n_slices
schedule = []
for i in range(n_slices):
ts = start_ts + int(i * step)
schedule.append((ts, qty_per_slice))
return schedule
VWAP 需要预测成交量分布。A 股的日内成交量呈 U 形(开盘和收盘活跃),可以用历史平均分布作为权重:
U_SHAPE = [0.18, 0.12, 0.09, 0.08, 0.08, 0.10, 0.12, 0.23] # 日内 30 分钟切片权重
def vwap_schedule(total_qty, u_shape=U_SHAPE):
return [int(total_qty * w) for w in u_shape]
POV(Percentage of Volume)按「市场成交量的固定比例」下单,是最自适应的:
class POVExecutor:
def __init__(self, target_qty, pov_rate=0.1):
self.remaining = target_qty
self.pov = pov_rate # 目标参与率 10%
self.market_volume = 0
def on_market_trade(self, qty):
self.market_volume += qty
should_sent = int(self.market_volume * self.pov)
to_send = min(should_sent - self.sent, self.remaining)
if to_send > 0:
self.place_order(to_send)
POV 的参与率通常设 5%~15%。超过 20% 会显著推高价格。
5. Almgren-Chriss 与最优执行
Almgren-Chriss 模型把执行问题形式化为「冲击成本 vs 时间风险」的权衡:
总成本 = 冲击成本(随执行速度增加)+ 时间风险(随执行时间增加)
冲击成本 ≈ η · (v)² # v 为执行速率
时间风险 ≈ λ · σ · x # x 为剩余持仓
最优执行轨迹是指数衰减:
x(t) = X · sinh(κ(T−t)) / sinh(κT)
κ:与风险厌恶 λ、波动率 σ、流动性 η 相关的常数
import numpy as np
def ac_trajectory(X, T, kappa, n_steps=10): # 返回每个时点应剩余持仓
t = np.linspace(0, T, n_steps + 1)
remaining = X * np.sinh(kappa * (T - t)) / np.sinh(kappa * T)
return remaining
traj_fast = ac_trajectory(1e6, 1.0, kappa=5.0) # 高风险厌恶,快速执行
traj_slow = ac_trajectory(1e6, 1.0, kappa=0.5) # 低风险厌恶,缓慢执行
实用要点:当 alpha 衰减很快时,κ 取大值(快速执行);当 alpha 缓慢衰减时,κ 取小值(缓慢执行)。这个直觉比公式本身更重要。
6. 冰山单与隐藏单
冰山单把大单拆成小单,每次只显示一小部分,成交后自动补充:
class IcebergOrder:
def __init__(self, total, display_qty, limit_price):
self.total = total
self.display = display_qty # 每次显示的量
self.price = limit_price
self.executed = 0
def on_fill(self, qty):
self.executed += qty
if self.executed < self.total:
self.replenish(min(self.display, self.total - self.executed)) # 补充会失去时间优先级
关键权衡:每次补充都会失去时间优先级(因为重新挂单要排到队尾)。因此冰山单在流动性差的标的上效果不好,因为频繁补充会导致成交价不断变差。
国内交易所不原生支持冰山单,必须在 OMS 侧模拟,这意味着你的「隐藏」意图对交易所是可见的(交易所能看到你的真实挂单),只是对其他市场参与者不可见。
7. FIX 协议与报文结构
FIX(Financial Information eXchange)是国际通用的交易协议,理解它的报文结构对排查问题很重要:
8=FIX.4.4|9=148|35=D|49=SENDER|56=TARGET|34=2|
52=20261007-09:30:00.000|11=ORD001|21=1|38=1000|
40=2|44=10.50|54=1|55=600519|60=20261007-09:30:00|
10=128|
关键字段:
| Tag | 含义 | 说明 |
|---|---|---|
| 35 | MsgType | D=新单, F=撤单, 8=成交回报 |
| 11 | ClOrdID | 客户端订单号,幂等键 |
| 41 | OrigClOrdID | 被撤订单的原始 ID |
| 150 | ExecType | 0=新, 1=部分成交, 2=成交, 4=撤销 |
| 39 | OrdStatus | 订单状态 |
| 32 | LastQty | 本次成交量 |
| 31 | LastPx | 本次成交价 |
Tag 11(ClOrdID)是幂等性的关键:重发订单时必须用同一个 ClOrdID,交易所才能识别为重复并去重。OMS 的订单号生成必须全局唯一且可追溯。
成交回报的解析要特别注意 LastQty(本次成交量)与 CumQty(累计成交量)的区别:
def on_exec_report(msg):
order_id = msg.get(11)
last_qty = msg.get(32, 0) # 本次成交量
cum_qty = msg.get(14, 0) # 累计成交量
last_px = msg.get(31, 0)
order = orders[order_id] # 用 cum_qty 校验,防止丢回报导致累计错误
if cum_qty != order.filled + last_qty:
raise InconsistentFill(order_id, order.filled, cum_qty)
order.filled = cum_qty
8. 撤改单的时序竞态
撤单和成交之间的竞态是 OMS 最经典的 bug 来源:
时刻 T1:OMS 发出撤单请求
时刻 T2:交易所撮合,订单成交
时刻 T3:撤单到达交易所,但订单已成交,撤单失败
时刻 T4:OMS 收到「撤单拒绝」回报
时刻 T5:OMS 收到「成交」回报
如果 OMS 在 T1 就把订单标记为已撤销,那么 T5 的成交回报会被忽略,持仓少算。正确做法是:
def on_cancel_request(order):
if order.state == OrderState.NEW:
order.state = OrderState.PENDING_CANCEL # 不是 CANCELLED
elif order.state == OrderState.PARTIALLY_FILLED:
order.state = OrderState.PENDING_CANCEL
def on_cancel_reject(order, reason): # 撤单失败通常是已成交,需恢复原状态
order.state = OrderState.FILLED if order.remaining == 0 else OrderState.PARTIALLY_FILLED
另一个常见竞态是改单:A 股不支持原生改单,改单实际是「撤旧单 + 发新单」,两步之间订单可能已经成交,导致重复下单。OMS 必须等待撤单确认后才能发新单。
9. 执行质量评估
执行算法好不好,要用数据说话。四个核心指标:
| 指标 | 定义 | 目标 |
|---|---|---|
| 实现差价 | 成交均价 − 决策时价格 | 越小越好 |
| 相对 VWAP | 成交均价 vs 市场 VWAP | 优于 VWAP |
| 参与率 | 成交量 / 市场成交量 | 符合目标 |
| 冲击成本 | 执行前后价格变化 | 越小越好 |
def implementation_shortfall(fills, decision_price, side):
vwap = sum(f.price * f.qty for f in fills) / sum(f.qty for f in fills)
sign = 1 if side == 'BUY' else -1 # 买入成交价高于决策价为负贡献
return sign * (vwap - decision_price) / decision_price * 10000 # 单位 bp
实现差价(Implementation Shortfall) 是最全面的指标,因为它以「决策时刻」为基准,包含了延迟、冲击、择时所有成本。VWAP 对比则适合评估被动型算法。
执行质量的数据应该回流到 OMS 的参数调优:如果某个标的的 TWAP 执行总是劣于 VWAP,说明该标的的成交量分布偏斜,应该改用 VWAP 算法。
权衡取舍
| 维度 | TWAP | VWAP | POV | 激进限价 |
|---|---|---|---|---|
| 冲击成本 | 低 | 低 | 中 | 高 |
| 时间风险 | 高 | 中 | 低 | 极低 |
| 依赖预测 | 无 | 成交量分布 | 无 | 无 |
| 适合场景 | alpha 衰减慢 | 跟踪基准 | 流动性好 | alpha 衰减快 |
算法选择的核心是alpha 的衰减速度:如果信号在 10 分钟内衰减一半,用 TWAP 拆 1 小时等于把 alpha 全浪费了;如果信号能维持一天,快速执行反而推高成本。
订单状态机的设计上也有取舍:严格状态机(非法转移直接抛异常)能尽早发现 bug,但会在行情异常时误伤;宽松状态机(接受所有转移)容错性好,但会把 bug 藏起来。生产系统通常采用严格模式 + 完善的告警。
常见坑清单
- 撤单即标记已撤销:忽略
PENDING_CANCEL,导致撤单前成交的回报被丢弃。 - 忽略部分成交:只处理全成交,部分成交的残单无人管理。
- 改单不等待撤单确认:撤旧单和发新单之间可能重复成交。
- 用市价单假设无限成交:A 股最优五档外的量无法成交。
- ClOrdID 不唯一:重发时用新 ID,交易所无法去重,产生重复订单。
- 不校验 CumQty:丢回报时累计成交量错误,持仓对不上。
- 收盘前不留缓冲:14:57 后无法撤单,残单被迫留到次日。
- 涨跌停仍持续发单:产生大量废单,占用通道并被交易所警告。
- 不记录决策价:无法计算实现差价,执行质量无从评估。
- 重连后不查询订单状态:断线期间的成交回报丢失,订单状态永久错误。
小结
OMS 的核心是把不可靠的外部世界抽象成可靠的状态机。订单的每一个状态、每一次转移、每一次回报都必须被完整处理,任何一个遗漏都会在持仓统计上体现出来。工程上最值得投入的是状态机的完备性和幂等性,而不是算法的高深。
判断一个 OMS 是否健壮,可以问三个问题:断线重连后能否恢复所有订单状态?撤单和成交竞态是否有明确的处理?每一笔成交是否有唯一可追溯的标识? 三个都是「是」,才具备上实盘的基础。
下一步建议阅读 交易风控与实时限额 ,看 OMS 发出的每一笔订单如何被实时校验;如果关心订单在交易所侧如何被撮合,可以看 撮合引擎设计 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。