订单管理与执行算法

订单管理与执行算法的工程实践:OMS 的职责边界、订单生命周期状态机、A 股与期货的订单类型差异、TWAP 与 VWAP 与 POV 拆单算法、Almgren-Chriss 最优执行、冰山单与隐藏单、FIX 协议报文、撤改单时序竞态,以及执行质量评估与成本归因。

OMS(Order Management System)是把「目标仓位」变成「实际成交」的那一层。它看起来简单——收到指令、发单、收回报——但生产环境中的大部分疑难杂症都出在这里:订单状态机漏了一个状态、撤单和成交的竞态、部分成交后的残单处理、断线重连后的订单恢复。

真正难的地方在于OMS 必须处理一个不可靠的外部世界:交易所会拒单、会延迟回报、会部分成交、会连接中断。策略层假设的「下单即成交」在 OMS 层必须被还原成一套完整的状态机,任何一个状态的遗漏都会导致持仓对不上。

本文按「职责 → 状态机 → 订单类型 → 执行算法 → 最优执行 → 高级订单 → 协议 → 时序 → 评估」的顺序展开。组合层面的权重生成在 组合优化与风险模型 中讨论,本文从「收到目标仓位」开始。

目录

  1. OMS 的职责与边界
  2. 订单生命周期状态机
  3. 订单类型与交易所规则
  4. 拆单算法:TWAP、VWAP、POV
  5. Almgren-Chriss 与最优执行
  6. 冰山单与隐藏单
  7. FIX 协议与报文结构
  8. 撤改单的时序竞态
  9. 执行质量评估

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含义说明
35MsgTypeD=新单, F=撤单, 8=成交回报
11ClOrdID客户端订单号,幂等键
41OrigClOrdID被撤订单的原始 ID
150ExecType0=新, 1=部分成交, 2=成交, 4=撤销
39OrdStatus订单状态
32LastQty本次成交量
31LastPx本次成交价

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 算法。

权衡取舍

维度TWAPVWAPPOV激进限价
冲击成本低低中高
时间风险高中低极低
依赖预测无成交量分布无无
适合场景alpha 衰减慢跟踪基准流动性好alpha 衰减快

算法选择的核心是alpha 的衰减速度:如果信号在 10 分钟内衰减一半,用 TWAP 拆 1 小时等于把 alpha 全浪费了;如果信号能维持一天,快速执行反而推高成本。

订单状态机的设计上也有取舍:严格状态机(非法转移直接抛异常)能尽早发现 bug,但会在行情异常时误伤;宽松状态机(接受所有转移)容错性好,但会把 bug 藏起来。生产系统通常采用严格模式 + 完善的告警。

常见坑清单

  1. 撤单即标记已撤销:忽略 PENDING_CANCEL,导致撤单前成交的回报被丢弃。
  2. 忽略部分成交:只处理全成交,部分成交的残单无人管理。
  3. 改单不等待撤单确认:撤旧单和发新单之间可能重复成交。
  4. 用市价单假设无限成交:A 股最优五档外的量无法成交。
  5. ClOrdID 不唯一:重发时用新 ID,交易所无法去重,产生重复订单。
  6. 不校验 CumQty:丢回报时累计成交量错误,持仓对不上。
  7. 收盘前不留缓冲:14:57 后无法撤单,残单被迫留到次日。
  8. 涨跌停仍持续发单:产生大量废单,占用通道并被交易所警告。
  9. 不记录决策价:无法计算实现差价,执行质量无从评估。
  10. 重连后不查询订单状态:断线期间的成交回报丢失,订单状态永久错误。

小结

OMS 的核心是把不可靠的外部世界抽象成可靠的状态机。订单的每一个状态、每一次转移、每一次回报都必须被完整处理,任何一个遗漏都会在持仓统计上体现出来。工程上最值得投入的是状态机的完备性和幂等性,而不是算法的高深。

判断一个 OMS 是否健壮,可以问三个问题:断线重连后能否恢复所有订单状态?撤单和成交竞态是否有明确的处理?每一笔成交是否有唯一可追溯的标识? 三个都是「是」,才具备上实盘的基础。

下一步建议阅读 交易风控与实时限额 ,看 OMS 发出的每一笔订单如何被实时校验;如果关心订单在交易所侧如何被撮合,可以看 撮合引擎设计 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「量化交易」更多文章

  1. 风险模型与因子归因
  2. 回测偏差与过拟合防范
  3. 市场微结构与流动性