母单(parent order)下来之后,从「现在我要买 100 万股招商银行」到「成交簿里 100 万股全部落地」之间发生的事情,是策略 PM 在卖方研报里读不到、在自家研究服务器上跑不动、在通用回测引擎里看不见的。它不是 alpha,不是组合优化,也不是风控;它是一套独立的工程子系统:把一个无法瞬间吃完的母单,按某种节奏切成一串子单(child order),再按某种规则把每个子单送进市场,过程中持续接收回报、对账、动态调整剩余切片,最终给出一份可以核对到每股每分钱的执行报告。
这套子系统的核心是执行算法(execution algorithm)。机构经纪台、买方执行台、自营做市台都会写自己的版本;卖方电子交易部门把它打包成「DMA + algo suite」卖给客户;交易所与第三方供应商也提供经过认证的算法套件(FlexTrade、QuantHouse、Tradeweb 等)。市场上能看到的「执行算法」少说有几十个名字——TWAP、VWAP、POV、IS、Liquidity Seeking、Iceberg、Sniper、Guerrilla、Dark Aggregator、Close、Open、Pegged、With Volume、Inline——但万变不离其宗,工程上要实现的算法主干只有四个:TWAP(Time-Weighted Average Price)、VWAP(Volume-Weighted Average Price)、POV(Percentage of Volume)、IS(Implementation Shortfall)。其余算法本质都是这四个主干算法在某些维度上的特化、融合或风控加套。
本文把这四个主干算法从「策略概念」拆到工程能落地的颗粒度:每个算法的数学定义、典型场景、可被操纵的攻击面、在线纠偏的工程做法、与上游母单管理系统(OMS, Order Management System)和下游订单路由(SOR, Smart Order Router)的接口约定,以及它们在 A 股、美股、加密三个市场口径下的差异。最后给出一份可以直接抄走的 Python 切片器骨架与基于历史 bar 的成本对比实验。前一篇《绩效指标》给出了「赚到的钱怎么计量」,本篇给出「下达的单子怎么落地」;下一篇《智能订单路由》进一步把每一个子单切到具体的交易场所与价格档位上。
风险提示与适用范围:本文不构成任何投资建议。所有算法、参数、代码示例均用于说明工程结构,未经过完整生产审计,参数取值仅用于教学示例,不可直接当作生产环境配置。真实算法参数应使用本机构成交回报数据按本文方法重新拟合并经过完整压力测试。执行算法在不同司法辖区受不同最佳执行(best execution)合规义务约束(MiFID II RTS 27/28、FINRA Rule 5310、SEC Rule 605/606、中国证监会《证券公司客户资产管理业务管理办法》、香港 SFC Code of Conduct §3.2 等),本文不替代任何监管文档。算法切片若被认定为操纵市场(如开收盘集合竞价的 marking the close、连续竞价的 layering / spoofing),会涉及行政处罚乃至刑事责任,相关边界由各市场监管机构界定,本文示例代码仅做学术演示,禁止直接用于实盘。
一、执行算法的位置(信号 → 子单 → 撮合)
把整个交易系统铺开看,执行算法处于一个非常具体的位置:上游是组合管理系统(PMS, Portfolio Management System)生成的目标持仓与对应的母单流;下游是订单路由器与交易所/经纪商网关。理解这个位置之前,必须先把「单子的层级结构」讲清楚。
机构交易里一笔决策从生成到落地,至少要经历四级单子。第零级是目标头寸(target position):组合优化器在某个时点告诉系统「招商银行的目标权重 0.85%」,按当前组合 NAV 折算成股数,是一个绝对值(target shares)。第一级是母单(parent order):把当前持仓与目标头寸做差,得出今天需要执行的净买卖股数与方向,附带一组执行约束(urgency、benchmark、cap、close-out time、limit)。第二级是子单(child order):执行算法按某种节奏,把母单切成一串小单,每条小单都有自己的下单时刻、下单数量、限价、TIF(time-in-force)、路由偏好。第三级是路由后的市场单(market order at venue):智能订单路由把每条子单进一步切到具体的交易场所(交易所主板、暗池、ECN)与具体的价格档位上,这一级直接和交易所撮合引擎对话。第四级是成交回报(execution report,FIX 协议里的 ExecutionReport, MsgType=8):交易所对每一次撮合返回一条回报,按子单 → 母单 → 目标头寸逐级汇总,对账后写入持仓与现金账户。
执行算法位于第一级到第二级的转换。它的输入是「这一笔母单:品种 600036.SH,方向买入,数量 1,000,000 股,benchmark VWAP,紧迫度中等,区间 09:30–14:55,限价 ≤ 当日开盘 +3%」;它的输出是「当前应该发的子单:09:32:00 发 4,800 股,限价 35.21,TIF=IOC,路由偏好上交所主板优先」。这个输出是动态的:每次行情更新(每个新 tick、每个新 bar)、每次自家成交回报到达、每个新的市场广播事件,都可能让算法重新计算下一个子单。
把执行算法作为一个独立子系统看,它必须暴露三组接口:
对上的接口(向 OMS / PMS)。接收母单与执行约束,返回母单状态(受理 / 进行中 / 完成 / 取消 / 撤销)、当前已成交数量与均价、当前未成交数量、预估完成时间、预估冲击成本。机构系统里这一组接口通常用 FIX 协议(NewOrderSingle MsgType=D 进、ExecutionReport MsgType=8 出),加上一组自定义 tag 描述算法参数(如 Tag 847 TargetStrategy 取自 FIX 标准的算法 ID 表)。
对下的接口(向 SOR / 交易网关)。下达子单(NewOrderSingle)、修改子单(OrderCancelReplaceRequest, MsgType=G)、撤销子单(OrderCancelRequest, MsgType=F),接收成交回报。算法不直接和交易所通信,它和 SOR 通信,SOR 负责场所选择与下游网关协议适配。
对内的接口(与执行参考数据 / 行情)。读取实时行情(订单簿、成交流、参考价)、历史成交量曲线、历史波动率曲线、当前的市场冲击模型参数。这一组数据通常由独立的市场数据子系统提供,算法只读不写。
四级单子的层级关系决定了单子状态机的层级也是四级:每一级单子都有自己的状态,且必须保证「子级状态变化 → 父级状态聚合更新」的一致性。任何一级状态机做错,都会导致同一笔决策被重复下达或漏下达。第八节会展开状态机的工程实现。
执行算法不是一段「定时器 + 切片公式」就能糊弄过去的脚本。它必须处理:
- 市场关闭与暂停(开盘集合竞价、午盘休市、临停、熔断、节假日提前收盘、单一品种停牌);
- 行情中断(行情线断开、行情滞后、行情数据异常值);
- 回报丢失与重复(FIX session 重连之后回报序号回放、网关重发导致的重复回报);
- 撤单成功与撤单失败(撤单请求和成交回报赛跑导致的「撤了又成交」);
- 限价不足(限价单挂在档外没成交、市场跑掉之后限价过期);
- 风控否决(OMS 风控层临时禁止该品种交易、单笔限额触发);
- 算法被替换(PM 中途把母单算法从 VWAP 改成 IS);
- 算法被中止(PM 直接 cancel 整个母单、风控强制平仓)。
每一种情况都对应一段需要测试的代码路径。生产系统里执行算法的代码量,业务逻辑(切片公式)部分往往不到 20%,剩下 80% 全是这些边界情形的处理。
理解了这个位置之后,再来看四个主干算法各自的定义与差异。
二、TWAP:均匀切片与抗操纵
TWAP(Time-Weighted Average Price,时间加权均价)是四个主干算法里最简单的一个——简单到几乎所有人都觉得「不就是除一下吗」。这种轻视恰恰是 TWAP 在生产里反复出问题的根源。
二·一 TWAP 的定义
设母单总量为 \(X\)(股或合约数),执行区间 \([t_0, t_T]\),TWAP 的「理想切片」是把 \(X\) 沿时间轴均匀分布:在每个等长子区间内成交 \(X / N\)(\(N\) 是子区间数),不考虑这段时间市场实际成交了多少量。
TWAP 作为参考价与 TWAP 作为执行算法是两件不同的事情。前者是 \(\bar P_\text{TWAP} = \frac{1}{T} \int_{t_0}^{t_T} P(s)\, ds\),按时间均匀对 mid 价取平均,是一个评估指标。后者是「以匹配 TWAP 参考价为目标的切片算法」。算法的输出(子单序列)应当让自己的成交均价尽可能接近这一区间的 TWAP 参考价。
理论上,如果价格遵循无漂移随机游走,且子单本身没有冲击,那么按时间均匀切片得到的成交均价的期望就等于该区间 TWAP 参考价。这是 TWAP 算法的合理性来源。
二·二 典型应用场景
TWAP 在三类场景下是默认选择:
第一是流动性极差的品种。日均成交不到目标量 5% 的品种,VWAP 基准本身极易被几笔大单偏倚(甚至被自己偏倚),TWAP 用「时间」做基准比用「成交量」更稳定。债券、期权、ETF 申赎对应的一篮子股票、加密小币种常见这种处理。
第二是目标基准明确为时间的合规要求。中国证监会《证券基金经营机构债券投资交易业务内控指引》要求对部分债券交易使用时间均价作为公允性核查基准;某些做市商协议、跨经纪商分仓协议明确写入「按 TWAP 结算」。
第三是对到达时间敏感、对成交量曲线无所谓的场景。例如「在 14:00 之前必须建仓 60%」这种强制时间节奏的策略,本质要求一个时间均匀执行器,TWAP 是直接答案。
二·三 可被操纵的攻击面
TWAP 算法最容易被对手方探测和反向操纵——这是它的工程价值远低于教科书定义的核心原因。原始 TWAP 切片有以下可观测特征:
第一,等时间间隔下单。每 30 秒、60 秒、5
分钟一个子单,时序极规律。HFT
通过自身订单簿镜像与时延测量,能在两三个子单之后识别出某条买盘是
TWAP
算法在跑,并对剩余执行区间做简单线性外推(剩余量 = (T-t)/T * X),按
TWAP 节奏在自家盘口前面挂单(即一种 layering
风格的预先吃单),等 TWAP 子单到达时反向出货赚价差。
第二,等量切片。每个子单数量都是 \(X / N\),便于探测整数模式。
第三,只在关键时刻附近活跃。如果 TWAP 区间是 09:30–14:55,那么 14:55 附近的最后一片切片几乎必然存在,对手方有充分时间在收盘前抢跑。
工程实现里,任何一个稍有规模的机构都会在原始 TWAP 上叠加随机化:
- 时间抖动(time
jitter):每个子单的发出时刻在「等间隔 ±
一个随机偏移」内浮动。常见做法是
t_i = t_0 + i*Δt + U(-α*Δt, +α*Δt),\(\alpha\) 取 0.2–0.4。 - 数量抖动(size
jitter):每个子单的数量在
X/N * (1 + U(-β, +β))之间浮动,\(\beta\) 取 0.1–0.3,最后一片做余量调整。 - 限价抖动(limit
jitter):在被动挂单模式下,限价相对 mid
价的偏移随机化(如取
mid - U(0, 1) * half_spread)以避免可预测的挂单档位。 - 路由抖动(venue jitter):在多场所市场里,子单在不同交易所间随机轮转。
抖动不能太大,否则会偏离 TWAP 基准;也不能太小,否则失去抗探测意义。机构生产里的取值是「在 95% 概率下不让对手方在 5 个子单内识别出 TWAP 模式」反推得到的。
二·四 被动 vs 主动模式
TWAP 算法在每个时刻有一个待发数量,但具体怎么把这个数量送进市场,有两种模式。
主动 TWAP(Aggressive TWAP)。每个子单用市价单或对手价限价单(IOC)直接吃对手方挂单。优点是确定性高,每片切完就完;缺点是显性消耗 spread,平均每股要付 half-spread 的成本,对窄价差品种影响小、对宽价差品种影响大。
被动 TWAP(Passive TWAP)。每个子单挂在自己一侧的最优价(或更靠内一档),等对手方主动来吃。优点是赚 half-spread 而不是付;缺点是「挂得太被动会挂不上」,价格走掉后剩余量得追,反而付出更大滑点。
实际生产 TWAP 用自适应混合模式:默认被动挂,挂一段时间没成交就转主动;或者按「时间进度 vs 成交进度」动态决定。具体逻辑:
if 成交进度 / 时间进度 > 0.95: # 进度领先,可以慢一点
被动挂在 best bid 内一档
elif 成交进度 / 时间进度 > 0.85: # 略落后
挂在 best bid
elif 成交进度 / 时间进度 > 0.7: # 明显落后
挂在 best ask 内一档(aggressive cross),或 IOC
else: # 严重落后
市价 IOC,能吃多少吃多少
这就引出了 TWAP 与 POV 的边界——一旦 TWAP 加上「按当前进度调节激进程度」,它已经在向 POV 靠拢。第四节会展开这个连续光谱。
三、VWAP:基于成交量曲线的切片
VWAP 是机构经纪与买方默认的基准,绝大多数券商电子交易部门的算法主力是 VWAP。它的核心思想是:把母单按一条「日内成交量分布曲线」切片,使自己的成交节奏与全市场一致。如果切得准,自己的成交均价应当与当日 VWAP 基准一致或略好。
三·一 VWAP 的定义
记 \(V(t)\) 为时刻 \(t\) 全市场已累计成交量占当日总量的比例,\(V(t_0) = 0\),\(V(t_T) = 1\)。VWAP 算法的目标切片轨迹是 \(x(t) = X \cdot V(t)\),即「市场成交了多少比例,自己就完成多少比例」。
每个子区间 \([t_i, t_{i+1}]\) 内应当成交 \(X \cdot (V(t_{i+1}) - V(t_i))\)。
问题在于 \(V(t)\) 是未来值——下单时刻还不知道。工程上必须用历史数据估计 \(V(t)\) 的形状,并在交易过程中用实时数据纠偏。
三·二 历史量曲线的拟合
A 股、美股的日内成交量分布有强烈的 U 型形态:开盘后 30 分钟与收盘前 30 分钟成交量明显高于午盘。下图是一个理想化的 U 型量分布与对应的 VWAP 累计目标曲线:
工程上拟合 \(V(t)\) 形状的常见做法是按 5 分钟或 15 分钟 bin 把过去 N 个交易日的日内成交量归一化(每天总量为 1),然后取截尾均值(trimmed mean)或中位数:
\[ \hat v_i = \mathrm{TrimmedMean}_{0.1, 0.9} \left\{ \frac{V_{d, i}}{\sum_j V_{d, j}} \;\middle|\; d \in \text{过去 } N \text{ 天} \right\} \]
\(N\) 通常取 20 或 60 个交易日。截尾的目的是消除单日异常事件(如停牌恢复、定增公告、纳指调样)对量分布的污染。
进一步可以按品种做分组建模:流动性高的大盘股 U 型程度低、流动性差的小盘股 U 型程度高、ETF 在临近开收盘的量集中度更高。生产系统会维护一张品种 → 量曲线的查找表,每周或每月重拟合。
历史量曲线的两个常见陷阱:
第一,事件日污染。期权到期日、指数调样日、季末月末、节前最后一日的量分布与平时差异显著。如果不剔除,会让平时的 VWAP 切片在错误的时间段过度集中。生产做法是维护一份事件日历,按事件类型分别建模或剔除。
第二,收盘集合竞价占比。A 股 14:57–15:00 集合竞价占当日量约 5–10%(个股差异极大),美股收盘 cross 占当日量约 6–8%。VWAP 切片在 14:57 之前是否「赶完所有量」,还是把这部分留到集合竞价里走,是一个明确的算法选择,不能让模型自己默认。
三·三 在线纠偏
历史量曲线只是先验,真实的当日量分布可能和先验偏离。开盘 10 分钟之内行业突发新闻、宏观数据公布、相关个股异动都会让某一段量大幅放大或缩小。VWAP 算法必须用实时已观测到的市场量来更新对剩余区间的量预测,这一步称为动态 VWAP(dynamic VWAP)或自适应 VWAP。
最简单的纠偏:用观测到的「截至当前时刻全市场量 / 历史量曲线在当前时刻的累计预测」当作日内放大系数 \(\alpha\),对剩余区间的预测量乘 \(\alpha\)。这一步隐含「今天比平时活跃多少倍」的判断。
更稳健的做法是贝叶斯量曲线更新:把历史曲线作为先验,把当日已观测的早盘量分布作为似然,求后验分布下的剩余量分布。Bialkowski 等人(2008, Journal of Banking & Finance)提出的因子分解模型把日内量分解为「全天因子 × 日内形状因子 × 残差」,是机构 VWAP 引擎里普遍使用的结构。
工程实现要点:
- 更新频率:每 5 分钟重估一次,过频会引入噪声、过疏会跟不上事件。
- 冷启动:开盘前 15 分钟内,已观测样本太少,不要做激进调整,沿用历史曲线。
- 截断:剩余执行节奏不能调到太快或太慢,加
[0.5x, 2.0x]的硬截断,避免极端纠偏导致最后几分钟暴力执行。
三·四 VWAP 的「自我成就」与考核陷阱
VWAP 作为考核基准时存在一个尴尬:自己的成交本身被算进 VWAP 里。母单越大,自己的成交对当日 VWAP 的贡献越高,「跑赢 VWAP」就越容易(因为 VWAP 被自己拉到自己的成交均价附近)。机构里的对策包括:
- 从基准里剔除自己的成交(self-removal VWAP);
- 约定 participation cap(如自己占当日成交不超过 20%,超过的部分单独考核);
- 使用同业 VWAP(peer VWAP)作为基准,但口径很难统一。
合规一侧也警惕这个自我成就效应:故意在收盘前最后几分钟把单子集中下出去去推 VWAP,可能构成 marking the close。SEC、FCA、香港 SFC、中国证监会都有对应的执法案例(如 SEC 2018 年对某做市商在收盘 cross 中的执行做出 USD 4.5M 罚款,案号 SEC v. 案略,参考 Release No. 34-83399)。
二·五 TWAP 区间选择与日历约束
TWAP 的「时间区间 \([t_0, t_T]\)」不是越长越好。区间越长,隔夜 / 跨段风险越大;区间越短,每片切得越大、冲击越高。生产里区间选择有几条经验法则:
第一,避免横跨集合竞价。如果母单是 09:30 下达、TWAP 区间到 14:55,那么 14:57 的集合竞价之外的连续竞价段只到 14:57:00,算法必须把所有切片限制在连续竞价段内,否则最后一片可能被打到 14:57 之后无法成交。区间结束时刻一定要早于集合竞价开始(A 股 14:56:30、美股 16:00、CME 不同合约不同)。
第二,避免横跨午盘休市。A 股 11:30–13:00 全市场休市 90 分钟。如果 TWAP 区间是 11:00–13:30,简单等距切片会把切片落在没有交易的 12:00 上,这是 bug。算法必须把午盘休市段从切片基轴里剔除,按「上午分钟数 + 下午分钟数」做拼接。
第三,避免横跨过宽时段。区间超过 4 小时的 TWAP 在风控上几乎等同「全天 TWAP」,与全日 VWAP 在统计意义上等价,应当直接用 VWAP 而不是手工写 4 小时 TWAP。
第四,事件日历过滤。重大宏观数据公布前后(如美国 CPI、非农就业、FOMC 决议)通常被剔出 TWAP 区间,由 PM 手工决定那段时间是「停止执行」还是「换成更激进的 IS 算法」。
三·五 VWAP 的事件日处理
事件日的量分布与平时差异显著,VWAP 算法在这些日子要切换到不同的曲线:
- 指数调样日(rebalance day):A 股沪深 300、上证 50 调样日的标的成交集中在 14:50–15:00 的集合竞价中,常规日内曲线不适用。MSCI、FTSE Russell、S&P 系列调样日同理。
- 期权到期日(OpEx):美股每月第三个星期五,量分布在收盘前 30 分钟显著放大;季度三巫日(quad witching)放大更多。
- 季末月末:机构调仓集中,日间量分布有偏移,特别是 ETF 申赎对应的成分股。
- 节前最后一日:A 股春节、国庆前最后一日量普遍小于平时,且午后量集中度更高。
- 新股上市日:纳入指数后第一日的量分布异常,需要单独处理。
工程实现上维护一份事件日历(CSV / 数据库表),每天开盘前查询当日是否命中事件,命中则切换到对应的曲线版本。
POV(Percentage of Volume,又称 Participation 算法)是 VWAP 的一种近亲,但视角不同:VWAP 把「执行节奏」绑在历史量曲线上,POV 把「执行节奏」绑在实时市场量上。
四·一 POV 的定义
设目标参与率(participation rate)\(\rho \in (0, 1)\),POV 算法在每个时间窗内的目标成交量为「该时间窗市场总成交量的 \(\rho\) 倍」。例如 \(\rho = 0.10\) 意味着「市场每成交 100 万股,我就成交 10 万股」。
形式化:在 \([t, t+\Delta]\) 内市场成交 \(V_\Delta\),则该窗自己应成交 \(\rho V_\Delta\)(受限于母单剩余量)。子单序列由「窗内目标量 - 窗内已自成交量」得出。
POV 不预设一个完成时间(除非加上 cap),它的总执行时长取决于市场量本身。如果市场量大,今天就能跑完;如果市场量小,今天跑不完留到明天,或在 close-out 时刻强制清仓。
四·二 与 VWAP 的差异
VWAP 与 POV 在 \(\rho\) 不变、当日量分布与历史量曲线一致的理想情况下,给出几乎相同的子单序列。差异在不一致时显现:
- 市场量异常放大时:VWAP 沿用历史曲线,按比例切;POV 实时跟随,会按 \(\rho\) 把放大的量同比例吃下,导致总执行量超出母单上限——所以 POV 必须配 cap。
- 市场量异常萎缩时:VWAP 仍按历史曲线切,可能在低流动性时段强行下单,付出高冲击;POV 自动减速,但可能在 close-out 之前完不成。
POV 的工程优势在于冲击成本可控:参与率固定意味着自己占市场比例固定,平方根律下冲击成本相对稳定。VWAP 在量异常时冲击可能爆掉。
四·三 目标参与率的选取
参与率不是越小越好——越小越慢,机会成本越高。机构里 \(\rho\) 通常按以下规则选:
| 紧迫度 | 典型 \(\rho\) | 备注 |
|---|---|---|
| 极慢(多日完成) | 2–5% | 减仓时不引起市场注意 |
| 慢 | 5–10% | 默认建仓节奏 |
| 中 | 10–20% | 当日完成主流策略 |
| 快 | 20–30% | 信号衰减快、必须当日完成 |
| 紧急 | 30–50% | 接近 IS 算法的领域 |
30% 是一个「工程红线」,绝大多数交易所有意控制单一账户单一时刻的参与占比上限,参与率太高会被场所风控告警,且冲击成本接近平方律陡升区间。
四·四 POV 的「跟单」陷阱
POV 算法被对手方探测后会陷入一种危险循环:对手方用小量诱导单子(pinging)让 POV 跟单。具体场景:HFT 在某个被 POV 大单跟踪的时刻,挂出大量「假性流动性」(在多档位轮换挂撤),人为放大成交量;POV 的实时市场量监测器误以为市场真的活跃,按 \(\rho\) 加快自己的节奏;HFT 看到 POV 出量后反手吃 POV 的单。
工程对策有几条:
- 量来源过滤:把「闪烁单」(在 100ms 内挂撤的订单簿挂单)、「自成交」对手方的成交不算入跟随量。
- 成交规模阈值:单笔小于一定股数(如 100 股)的成交不计入跟随量。
- 价区过滤:远离当前 mid 的成交(如比 mid 偏离超过 50 bps)不计入。
- 时间平滑:用 EMA 而不是瞬时量,平滑系数取 5–15 秒。
五、IS(Implementation Shortfall)算法:基于 Almgren-Chriss 求最优轨迹
IS 算法是这四个主干算法里最「数学」的一个,也是高紧迫度场景的默认选择。它不靠匹配某条历史曲线或市场量,它直接最小化预期执行成本与执行风险的加权和。
五·一 IS 框架
记 \(X\) 为母单总量,\(x(t)\) 为剩余持仓,\(x(0) = X\), \(x(T) = 0\)。在小区间 \([t, t+\Delta]\) 内成交 \(\Delta x = x(t) - x(t+\Delta) \geq 0\) 股,平均成交价为:
\[ \tilde P(t) = P(t) - h(\dot x(t)) \]
其中 \(P(t)\) 是中价(受到永久冲击 \(g(\dot x)\) 的影响逐步偏移),\(h(\dot x)\) 是临时冲击(temporary impact,仅作用于当前子单),\(\dot x = \Delta x / \Delta\) 是瞬时成交速率。
总实施成本(implementation shortfall)相对到达价的偏差为:
\[ \mathrm{IS} = \int_0^T h(\dot x) \dot x \, dt + \int_0^T g(\dot x) x(t) \, dt + \int_0^T \sigma \, dB(t) \cdot x(t) \]
第一项是临时冲击成本(被自己单子推高再吃自己),第二项是永久冲击成本(对剩余持仓的隐性损失),第三项是等待风险(剩余持仓在波动中漂移)。Almgren & Chriss(2000, Journal of Risk, “Optimal execution of portfolio transactions”)提出在线性冲击假设下:
\[ g(v) = \gamma v, \quad h(v) = \epsilon \, \mathrm{sgn}(v) + \eta v \]
其中 \(\gamma\) 是永久冲击系数,\(\eta\) 是临时冲击系数,\(\epsilon\) 是固定成本(half-spread)。
五·二 Almgren-Chriss 解析解
在线性冲击 + 算术布朗运动的假设下,最小化「期望成本 + λ × 成本方差」(\(\lambda\) 是风险厌恶系数)的最优轨迹是:
\[ x^*(t) = \frac{\sinh(\kappa(T-t))}{\sinh(\kappa T)} \cdot X \]
其中 \(\kappa = \sqrt{\lambda \sigma^2 / \tilde\eta}\),\(\tilde\eta = \eta - \gamma/2\)。
最优成交速率:
\[ v^*(t) = -\dot x^*(t) = \frac{\kappa X \cosh(\kappa(T-t))}{\sinh(\kappa T)} \]
参数解读:
- \(\lambda \to 0\)(风险中性):\(\kappa \to 0\),\(x^*(t) \to X(1 - t/T)\),退化为 TWAP(线性下降)。
- \(\lambda \to \infty\)(极度厌恶等待风险):\(\kappa \to \infty\),\(x^*(t)\) 在 \(t = 0\) 附近迅速下降,几乎全部前置执行。
- \(\lambda\) 中等:得到经典 sinh 形状,前重后轻——为减少持仓暴露在波动里的时间,越早执行越多。
下图给出几条不同 \(\lambda\) 下的最优轨迹:
VWAP/POV 都没有「风险厌恶」这一维参数;IS 算法的本质就是把风险厌恶显式化,在「冲击 vs 风险」之间做最优权衡。
五·三 风险厌恶的标定
\(\lambda\) 不是算法的隐参数,而是业务参数。它代表 PM/交易员对「执行不确定性」的容忍度,单位是「单位风险(方差)= 多少 bps 的额外成本」。机构里常见的标定路径:
- PM 给出「我能容忍最终成交均价偏离到达价的标准差不超过 X bps」。
- 用历史品种数据估计 \(\sigma\), \(\eta\), \(\gamma\)。
- 从 X 反推出对应的 \(\lambda\),检查 \(\kappa T\) 是否落在合理区间(一般 0.5–3)。
\(\kappa T\) 是无量纲数,刻画轨迹「前置度」。\(\kappa T < 0.5\) 几乎是 TWAP;\(\kappa T > 3\) 几乎是「立即吃完」。生产 IS 算法的 \(\kappa T\) 通常落在 1–2。
五·四 IS 算法的实战修正
教科书 AC 解析解在生产里直接用会出问题,必须做几项工程修正:
修正一:离散化与最小下单单位。\(x^*(t)\) 是连续值,要按交易所最小下单单位取整(A 股 100 股、美股 1 股、币安按 lot size),且每个时间格 \(\Delta\)(通常 1–5 分钟)一次重整。
修正二:实时冲击系数更新。\(\eta, \gamma\) 不是常数,随当日波动率、买卖压力、订单簿厚度变化。生产里用滚动窗口对历史成交回报做平方根律拟合(参见前一篇《交易成本模型》),每天开盘前重估一次,盘中再做小幅在线调整。
修正三:限价与流动性约束。AC 模型默认每一时刻有足够流动性接住 \(v^*(t)\) 的速率。真实订单簿厚度有限,必须加 cap:\(v(t) \leq c \cdot V_\text{market}(t)\),\(c\) 取 0.2–0.3,超出部分推到下一时间格。
修正四:闭式解作为锚,实时优化做调整。生产 IS 引擎通常每个时间格基于当前剩余持仓与剩余时间,重解一次「单步最优」问题(不是从头解),用 AC 解析公式作为锚,再叠加流动性、订单簿即时状态、风控约束的微调。
五·五 IS 与 VWAP 的对比与场景分工
IS 算法与 VWAP 算法常常被并列讨论,工程上它们的定位完全不同:
| 维度 | VWAP | IS |
|---|---|---|
| 基准 | 当日市场 VWAP | 到达价 |
| 紧迫度假设 | 低(按市场节奏) | 中到高 |
| 风险厌恶 | 隐含为零 | 显式参数 |
| 抗冲击 | 强(与市场同节奏) | 弱(前置增加冲击) |
| 抗时间风险 | 弱(暴露全天波动) | 强(早完成早出清) |
| 可考核性 | 高(与公开 VWAP 对比) | 高(与到达价对比) |
| 对冲信号衰减的鲁棒性 | 弱 | 强 |
PM 在选择算法时,本质在回答一个问题:「我对剩余持仓的风险厌恶有多强?」
- 信号衰减时间常数远大于一天 → 用 VWAP,按市场节奏走;
- 信号衰减时间常数小于半天 → 用 IS,早完成早释放风险;
- 不确定 → 用 POV 中等参与率(10–15%),介于两者之间。
机构里的「算法选择器」通常按信号特征 + 母单大小自动给出建议。某券商内部规则示例(节选自其客户文档):母单 < 日均量 5% 且信号半衰期 > 3 天 → 默认 VWAP;母单 5–15% 且半衰期 1–3 天 → POV(10%);母单 > 15% 或半衰期 < 1 天 → IS;母单 > 30% → 必须分多日执行。
五·六 IS 算法的失败模式
IS 算法在三种场景下表现显著劣于 VWAP:
第一是短期信号被市场反向跑掉。IS 假设价格是无漂移随机游走,但买入信号本身意味着对短期上涨的判断。如果信号正确,IS 前置执行获益(早建仓早吃涨);如果信号错误(实际下跌),IS 前置执行让自己在更高价位建满,比 VWAP 损失更大。这就是为什么 IS 必须配信号置信度门槛,置信度低时退化为 VWAP。
第二是冲击参数错估。\(\eta, \gamma\) 估计偏低 → 算法过度激进 → 实际冲击远超预期;估计偏高 → 算法过度保守 → 退化为 VWAP 但平白多承担了风险溢价。生产里 \(\eta\) 的标定误差通常在 30% 以内,IS 算法的成本对此敏感。
第三是异常市场状态。市场断流、单边连板、闪崩等场景下,AC 模型假设的连续轨迹不再成立。生产 IS 引擎必须有「市场状态检测器」,异常状态自动暂停 IS 切片,转人工或转 TWAP 应急模式。
四个主干算法都有一个共同假设:冲击模型是事先标定的、参数稳定。这在工程现实里往往不成立。冲击系数随事件、随时段、随订单簿状态显著变化,把它假设成常数等于在某些时段系统性低估冲击、某些时段系统性高估冲击。这就是自适应执行(adaptive execution)的工程动机。
自适应执行不是一个新的主干算法,而是在主干算法上叠加一层「实时模型预测」用于动态参数调整。它的几种典型形态:
形态一:在线冲击模型。每一笔自家成交都给出一对 \((\dot x, P_\text{exec} - P_\text{mid})\) 观测,用滑动窗口拟合 \(\eta_t\) 与 \(\gamma_t\)。当前一段冲击突然变大,立刻调小 \(\rho\)(POV)或 \(\kappa\)(IS)。卡尔曼滤波是常见的实现手段。
形态二:订单簿状态预测。订单簿失衡比(order book imbalance, OBI)= (best bid size - best ask size) / 总挂单量是一个对短期价格走向的有力预测因子。Cont 等人(2014, Operations Research, “The price impact of order book events”)系统性证明了 OBI 与下一个 mid 价变动之间的正相关。执行算法可以利用 OBI 调整子单的激进程度:买入时若 OBI > 0(买盘更厚,价格更可能上行),抢先吃;OBI < 0,再等等。
形态三:基于强化学习的执行策略。把「执行算法」建模成一个强化学习问题:状态包含剩余持仓、剩余时间、订单簿快照、近期成交方向;动作是「下一个子单的数量与限价」;奖励是「减小负 IS」。Nevmyvaka, Feng, Kearns(2006, ICML, “Reinforcement learning for optimized trade execution”)是早期工作;近年 DeepMind、Citadel、Two Sigma 等机构都有相关论文与专利。
形态四:transformer / sequence model 做量曲线预测。把日内量分布拟合从「截尾均值的统计模型」升级为序列模型,用最近 N 个交易日的分钟 bar 序列做 attention,输出当日剩余区间的量分布预测。生产里这一类模型对量分布的均方误差比经典统计模型有 5–15% 的改善(机构内部数字,未公开发表)。
ML-driven 执行要回答两个工程问题:
第一,模型 vs 解析解的边际收益。AC 解析解本身已经给出了一个非常合理的轨迹形状,ML 模型要超过它的边际收益,必须能稳定带来比解析解更小的执行成本。机构里实测的边际收益普遍在 5–20% 之间(相对解析解),不是数量级提升。这意味着 ML 模型必须按实盘 P&L 数据持续验证,不能只靠回测。
第二,模型失效的兜底。任何预测模型都会失效(行情突变、训练分布之外、模型推理服务挂了)。生产里 ML 执行算法必须在置信度低时回退到主干算法(TWAP/VWAP/POV/IS)。回退逻辑、回退触发器、回退之后的报警链路都要单独设计。
ML 执行不替代主干算法,它是主干算法上的一层「条件自适应」。这是这一领域近十年最重要的工程认识。
六·五 执行成本归因与 ML 训练标签
ML 执行模型的训练标签是个工程难题。直接用「成交均价 - 到达价」作标签会让模型学到「猜价格走势」,这不是执行算法该做的事——价格预测属于 alpha 模型,执行算法只负责「在给定信号下最小化成本」。正确的标签应当是「相对最优解析解的边际改善」:
\[ \text{label} = \mathrm{IS}_\text{baseline} - \mathrm{IS}_\text{actual} \]
其中 baseline 是 AC 解析解或 VWAP 在同一段历史上的反事实成本。这种「反事实」标签构造本身需要一套独立工程(counterfactual simulation engine),且要考虑反事实路径的冲击反馈。Two Sigma、Citadel 这类机构内部都有这种反事实仿真系统,但开源界很少见到完整实现。
ML 执行的另一个工程难点是部署反馈循环。模型上线后会改变自己的训练数据分布——自己执行得越好,未来收集到的训练数据里「baseline 与 actual 的差距」就越小,模型的提升空间逐步被压缩,且会逐渐学不到原始训练分布之外的场景。这种「分布漂移」要靠定期回灌 baseline 路径数据来缓解,工程上要保留一定比例的母单按基线算法跑(A/B test 的执行版本),让模型持续看到「真实 baseline 表现」。
六·六 ML 执行的工程边界
不是所有母单都适合 ML 执行。机构里通常按下面几条决定是否启用 ML 模型:
- 样本量足够:标的过去 6 个月有至少 200 笔同算法母单的执行数据,否则统计上不显著。
- 市场状态正常:不在事件日(FOMC、CPI、季报集中日)的高波动时段。
- 母单规模处于训练分布内:训练集里 90% 分位以下的规模才用 ML,超出的退到主干算法。
- 置信度阈值:模型对当前状态的预测置信度高于阈值才用 ML 输出,低于阈值退到主干算法。
这套「条件启用 + 安全回退」的工程结构让 ML 执行在生产里相对稳定。绝对依赖 ML 模型的执行系统在 2018–2022 年的市场极端事件(疫情爆发、加息周期)里普遍出过较大损失,业内已经形成「ML 永远是辅助」的共识。
四个主干算法的数学定义市场无关,但工程实现里每个市场都有不可忽视的口径差异。生产系统不能写一个「全市场通用」的执行引擎然后换品种参数就上,必须按市场分版本实现。
七·一 A 股
T+1 规则:当日买入的股票当日不能卖出。这意味着 A 股 IS 算法的「机会成本」概念与海外不同——如果信号反转,IS 算法切完的股票当天无法对冲掉。建仓 IS 算法必须在风控里加「T+1 反向暴露」约束。
涨跌停板(10% / 5% / 20%):执行算法要时刻关注当前价距涨跌停的距离。碰板的一刻所有买单或卖单全部排队,VWAP/POV 子单可能完全无法成交,IS 算法要被迫加大限价偏离或转向流动性更好的相关品种(暂停母单等待)。
集合竞价(09:15–09:25, 14:57–15:00):开盘集合竞价占当日量约 5%、收盘集合竞价占 5–10%。VWAP 算法需要明确决定是否参与集合竞价。参与的话,算法的最后一片要在 14:57:00 之前发出,且要按集合竞价规则下达(限价单,单一笔次,撤单权限受限)。
日内交易禁停(部分品种):科创板 / 创业板有「价格涨跌幅 30% / 60% 临停」机制,触发时所有未成交订单冻结,算法状态机要正确处理「临停 → 复牌」的过渡。
手续费结构:印花税 0.05% 单边卖出(2023.08 减半后),过户费 0.001% 双边,券商佣金机构客户 0.5–3 bps。算法在评估「主动 vs 被动」时要把这些成本带入。
最小下单单位 100 股:所有子单必须是 100 股的倍数,最后一片做「向下取整 + 余量并入倒数第二片」处理。
深圳与上海差异:深市的 ST 股票涨跌停 5%,上市新股首日上海 44%、深圳无限制(科创板、创业板特殊规则),算法的限价范围必须按品种 + 当日规则查表。
七·二 美股
Reg NMS(Regulation National Market System):Order Protection Rule 要求最佳报价(NBBO, National Best Bid and Offer)必须被尊重,跨档交易要给出价格改善证明。这意味着执行算法的子单必须经过 SOR 检查 NBBO,否则会被交易所或经纪商拒绝。
多场所:美股有 16 个 lit exchanges + 数十个暗池 + ATS。VWAP 子单不只是「在 NYSE 上挂」,要在多个场所之间分配。POV 算法要分别看每个场所的成交量,按场所权重发单。
碎股(odd lots)与圆股(round lots):100 股是 round lot,<100 股是 odd lot。SIP(Securities Information Processor)的 NBBO 在 2020 年之前不包含 odd lot 报价,部分场所对 odd lot 的处理与 round lot 不同。
收盘 cross:NYSE 与 NASDAQ 的收盘 cross 占当日量 6–10%,是 VWAP 算法和 IS 算法的关键节点。许多机构的「market on close」(MOC)订单在 15:50 前必须提交。
short sale rule(SEC Rule 201 / SHO):当一只股票从前收下跌 10% 触发 circuit breaker 后,做空只能挂在 best ask 之上。卖出 IS 算法在熔断后必须把子单类型自动改成 ASK+1 限价。
Section 31 fee:卖方按 USD 27.80 / USD 1M 名义额(2024 费率,按年调整)付给 SEC。算法不直接承担这一费用,但会影响净 P&L。
最小下单单位 1 股(与 A 股截然不同),算法不需要做整 100 股取整,但要处理 odd lot 报价的特殊性。
七·三 加密
24×7 交易、无开收盘:没有「集合竞价」「收盘 cross」的概念,意味着 VWAP 量曲线呈现的是周内 + 日内的双周期叠加。亚洲时段(UTC 0–8)、欧洲时段(UTC 8–16)、美洲时段(UTC 16–24)量分布显著不同,且不同币种的主导时段不同(BTC、ETH 美洲时段量大;某些山寨币亚洲时段量大)。
永续合约的资金费率(funding rate):每 8 小时结算一次,做多与做空互付。IS 算法在持仓期会承担资金费率成本,必须在「持仓时间」一项里折现进去。
多交易所:流动性碎片化:BTC/USDT 现货在 Binance、Coinbase、OKX、Kraken 等数十个场所同步交易,价差通常 0.05–0.5%。机构级执行算法必须做跨交易所 SOR,且要处理跨交易所的资金调拨延迟(链上转账 5–60 分钟)。
杠杆与清算:永续合约的清算价机制让 IS 算法在大单时必须考虑「自己单子是否会触发其他持仓的清算反馈」。
手续费结构:Maker 0.02–0.075%,Taker 0.05–0.10%。Maker 负费率(如 Binance 高级账户 -0.005%)让 VWAP/TWAP 的被动模式有「直接收钱」的特殊价值。
API 限流:交易所对每秒下单数有限制(Binance 现货 100 orders/10s 普通账户),算法必须做 token bucket 限流,否则触发限速被封 IP。
没有统一 NBBO:跨交易所没有标准化的最佳报价,算法要自己合成「合成 NBBO」。
七·四 跨市场口径汇总表
把三个市场的关键差异归纳为一张工程参考表:
| 维度 | A 股 | 美股 | 加密(永续主流) |
|---|---|---|---|
| 交易时段 | 09:30–11:30, 13:00–15:00 | 09:30–16:00 ET | 24×7 |
| 集合竞价 | 09:15–25 / 14:57–15:00 | 09:30 open / 16:00 close cross | 无 |
| 最小下单单位 | 100 股(基金 100 份) | 1 股 | lot size 因币而异 |
| 涨跌停 | ±10% / ±5%(ST) / ±20%(科创/创业) | 无(个股熔断 5/10/20%) | 无(部分所有限价带宽) |
| T+N | T+1 股票 / T+0 ETF | T+0 净额 / T+2 结算 | T+0 即时 |
| 做空 | 融券(券源紧张) | 直接做空 + Rule 201 | 永续合约多空对称 |
| 印花税 | 卖方 0.05% | 无(SEC §31 0.0028 bps 卖方) | 无 |
| 暗池 / 多场所 | 无 | 多场所 + 暗池 | 多交易所碎片化 |
| 监管基准 | 同业 VWAP | NBBO + Reg NMS | 无统一 NBBO |
| 资金费率 | N/A | N/A | 永续每 8 小时 |
| API 限速 | 券商柜台限速 | OUCH/FIX 网关限速 | REST/WS rate limit |
任何「跨市场通用执行引擎」必须在算法核心之外维护一份按市场分版本的「市场规则适配层」,把上面这些差异隔离在算法主干之外。生产里这一层通常占到代码量的 30–40%。
七·五 跨市场对照下的算法选型
同一个母单在三个市场的最佳算法选择可能完全不同:
- A 股 100 万股大盘股建仓:默认 VWAP 全天,留集合竞价做收尾。母单大于日均量 5% 时拆多日 IS。
- 美股 100 万股大盘股建仓:VWAP + close cross,最后 6–10% 量留给收盘 cross;信号衰减快时改用 IS。
- 加密 1000 个 BTC 永续建仓:跨交易所 POV(5–10%),同时考虑资金费率结算时点(避开费率结算前 30 分钟)。三个市场的「同一笔 1000 万美元买入」在时序与节奏上几乎没有可移植性。
把前面六节的算法语义落到生产代码上,需要解决三个工程问题:任务调度、子单状态机、回报对账。这一节给出工程结构与关键代码骨架。
八·一 任务调度
执行算法本质是一个事件驱动循环:在「时间事件」(每秒/每 N 秒触发)、「行情事件」(新 tick)、「回报事件」(成交回报)三类事件下分别决定要不要发新子单、要不要撤旧子单、要不要更新状态。
事件循环的核心结构:
class ExecutionEngine:
def __init__(self, mother_order, market_data, order_router):
self.mother = mother_order
self.md = market_data
self.router = order_router
self.algo = self._make_algo(mother_order)
self.state = MotherOrderState.PENDING
def on_timer(self, t):
if not self._is_active(t):
return
actions = self.algo.on_tick(t, self._snapshot())
for a in actions:
self._execute_action(a)
def on_market_data(self, md_event):
self.md.update(md_event)
def on_execution_report(self, exec_report):
self._reconcile(exec_report)
self.algo.on_fill(exec_report)
self._update_mother_state()algo 是 TWAP / VWAP / POV / IS
的具体实现,遵循统一接口:
class AlgoBase:
def on_tick(self, t, snapshot) -> list[Action]: ...
def on_fill(self, report) -> None: ...
def is_done(self) -> bool: ...Action 是「下单 / 撤单 /
改单」的指令对象。
八·二 子单状态机
每条子单独立维护状态机:
PENDING_NEW → ACK → PARTIALLY_FILLED → FILLED
↓ ↓
REJECTED ↓
↓ ↓
CANCELLED ← PENDING_CANCEL ←┘
关键的边界处理:
PENDING_NEW → REJECTED:交易所拒绝时(限价无效、风控否决、品种暂停),要把数量回滚到母单未发数量,不可漏。
PENDING_CANCEL → FILLED:撤单请求与最后一笔成交赛跑。CME、Binance、A 股都有「撤单失败因为已经成交」的回报,必须按「成交先于撤单」的最终一致性处理:以成交回报为准、忽略撤单失败回报。
PARTIALLY_FILLED → CANCELLED:部分成交后撤单,剩余数量回到母单未发数量。算法要把这一部分加进剩余执行计划里。
ACK 丢失:下单后没收到 ACK 在 N 秒内,是「网关超时」还是「ACK 丢包」未知。生产里通过下单时附带 ClOrdID(FIX Tag 11),用 OrderStatusRequest(MsgType=H)按 ClOrdID 查询当前状态来恢复。任何「猜测」的处理(再发一笔、放弃这笔)都是 bug 来源。
母单状态由所有子单状态聚合:
def aggregate_mother_state(children: list[ChildOrder]) -> MotherOrderState:
total_filled = sum(c.cumulative_filled for c in children)
total_pending = sum(c.leaves_qty for c in children if c.state in ACTIVE_STATES)
target = mother.target_qty
if total_filled == target:
return MotherOrderState.FILLED
if total_filled + total_pending < target and t > mother.deadline:
return MotherOrderState.PARTIAL_DEADLINE
if all(c.state in TERMINAL_STATES for c in children) and total_filled < target:
return MotherOrderState.PARTIAL_DONE
return MotherOrderState.WORKING八·三 回报对账
实时对账:每条 ExecutionReport 到达时,查 ClOrdID → 子单 → 母单,校验 cumulative quantity 单调不减、leaves quantity 单调不增、avgPrice 落在合理区间。任何不一致立刻报警并进入手工处理。
T+0 对账:当日收盘后,把交易系统的 fill 记录与经纪商的 confirmation file 逐笔对齐,差异在 T+1 早盘前必须澄清。常见差异来源:
- 重复回报:网关重连后回放,按 ExecID 去重。
- 回报延迟:T 日尾盘的回报到 T+1 早晨才到,按时间戳归到 T 日。
- 价格修订:交易所事后修订(trade bust),按交易所通知调整对账。
T+1 资金 / 头寸对账:与托管行、清算所的清算文件(DTCC、ChinaClear、CME Clearing)逐笔对齐,差异进入清算异常处理流程。
八·四 Python 实现:四个主干算法的切片器
下面给出可直接运行的 Python 骨架,实现 TWAP / VWAP / POV / IS 四个切片器的核心逻辑,并基于历史 bar 做执行成本对比。代码假设「每个 bar 自己的成交价是 bar VWAP,自己的成交量受限于 bar 总量的 cap,自己的临时冲击按平方根律」。
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import List, Tuple
@dataclass
class Bar:
t: int # bar 序号 0..N-1
vwap: float # 该 bar 的市场 VWAP(参考价)
volume: float # 该 bar 的市场总成交量(股)
sigma_bps: float # 该 bar 内 mid 价波动(bps)
@dataclass
class Slice:
t: int # 触发的 bar
qty: float # 计划执行数量
@dataclass
class Fill:
t: int
qty: float
price: float # 含冲击的成交均价
@dataclass
class CostReport:
arrival_price: float
avg_fill_price: float
is_bps: float # 相对到达价的滑点
twap_bps: float # 相对区间 TWAP
vwap_bps: float # 相对区间 VWAP
fills: List[Fill] = field(default_factory=list)
class SliceAlgo:
"""主干算法接口:接收母单与历史/预测 bar 序列,返回切片计划。"""
def plan(self, total_qty: float, bars: List[Bar]) -> List[Slice]:
raise NotImplementedError
class TWAP(SliceAlgo):
def __init__(self, jitter: float = 0.0, rng: np.random.Generator = None):
self.jitter = jitter
self.rng = rng or np.random.default_rng(0)
def plan(self, total_qty: float, bars: List[Bar]) -> List[Slice]:
n = len(bars)
base = total_qty / n
plan = []
cum = 0.0
for i, b in enumerate(bars):
if i == n - 1:
qty = total_qty - cum # 余量并入最后一片
else:
noise = 1 + self.jitter * self.rng.uniform(-1, 1)
qty = max(0.0, base * noise)
cum += qty
plan.append(Slice(b.t, qty))
return plan
class VWAP(SliceAlgo):
def __init__(self, hist_volume_curve: np.ndarray):
# 归一化历史日内量分布(长度 = bar 数),sum = 1
s = hist_volume_curve.sum()
if s <= 0:
raise ValueError("invalid volume curve")
self.curve = hist_volume_curve / s
def plan(self, total_qty: float, bars: List[Bar]) -> List[Slice]:
if len(bars) != len(self.curve):
raise ValueError("bar count != volume curve length")
plan = []
cum = 0.0
for i, b in enumerate(bars):
if i == len(bars) - 1:
qty = total_qty - cum
else:
qty = total_qty * self.curve[i]
cum += qty
plan.append(Slice(b.t, qty))
return plan
class POV(SliceAlgo):
def __init__(self, rho: float, cap_ratio: float = 0.3):
if not 0 < rho < 1:
raise ValueError("rho must be in (0, 1)")
self.rho = rho
self.cap_ratio = cap_ratio
def plan(self, total_qty: float, bars: List[Bar]) -> List[Slice]:
plan = []
remaining = total_qty
for i, b in enumerate(bars):
target = self.rho * b.volume
cap = self.cap_ratio * b.volume
qty = min(target, cap, remaining)
remaining -= qty
plan.append(Slice(b.t, qty))
# 若到末仍有剩余,强制并入最后一片
if remaining > 0 and plan:
plan[-1] = Slice(plan[-1].t, plan[-1].qty + remaining)
return plan
class IS_AlmgrenChriss(SliceAlgo):
"""
基于 Almgren-Chriss 解析解的 IS 切片器。
参数:
lam - 风险厌恶(单位价格²/股²/bar)
eta - 临时冲击系数(线性)
sigma - bar 内波动率(同上一致单位)
解析轨迹 x*(t) = X * sinh(kappa*(T-t)) / sinh(kappa*T)
"""
def __init__(self, lam: float, eta: float, sigma: float):
if lam < 0 or eta <= 0 or sigma < 0:
raise ValueError("invalid AC params")
self.lam = lam
self.eta = eta
self.sigma = sigma
def plan(self, total_qty: float, bars: List[Bar]) -> List[Slice]:
T = len(bars)
if T <= 1:
return [Slice(bars[0].t, total_qty)]
if self.lam == 0:
# 退化为 TWAP
return TWAP().plan(total_qty, bars)
kappa = np.sqrt(self.lam * self.sigma ** 2 / self.eta)
# 离散时间 t=0..T,x(0)=X,x(T)=0
ts = np.arange(T + 1)
sh_T = np.sinh(kappa * T)
x = total_qty * np.sinh(kappa * (T - ts)) / sh_T
plan = []
for i, b in enumerate(bars):
qty = x[i] - x[i + 1]
plan.append(Slice(b.t, max(0.0, qty)))
# 浮点误差修正
diff = total_qty - sum(s.qty for s in plan)
if abs(diff) > 1e-9:
plan[-1] = Slice(plan[-1].t, plan[-1].qty + diff)
return plan
# ============ 模拟撮合(含临时冲击) ============
def simulate_execution(
plan: List[Slice],
bars: List[Bar],
eta_bps_per_pct: float = 80.0,
cap_ratio: float = 0.3,
) -> CostReport:
"""
简单仿真:bar 内自己的成交均价 = bar VWAP * (1 + sign * impact)。
impact = eta_bps_per_pct * (qty / bar_volume) bps(线性 + 平方根混合的简化版本,
工程里应替换成本机构拟合的冲击模型)。
"""
fills = []
arrival = bars[0].vwap
for s, b in zip(plan, bars):
qty = min(s.qty, cap_ratio * b.volume) # cap 防爆量
if qty <= 0:
continue
participation = qty / max(b.volume, 1.0)
impact_bps = eta_bps_per_pct * np.sqrt(participation)
# 假设买方向:自己付 impact
exec_price = b.vwap * (1.0 + impact_bps / 1e4)
fills.append(Fill(b.t, qty, exec_price))
if not fills:
return CostReport(arrival, np.nan, np.nan, np.nan, np.nan, [])
total_qty = sum(f.qty for f in fills)
avg = sum(f.qty * f.price for f in fills) / total_qty
twap = np.mean([b.vwap for b in bars])
mkt_vwap = (
sum(b.vwap * b.volume for b in bars) /
sum(b.volume for b in bars)
)
return CostReport(
arrival_price=arrival,
avg_fill_price=avg,
is_bps=(avg / arrival - 1) * 1e4,
twap_bps=(avg / twap - 1) * 1e4,
vwap_bps=(avg / mkt_vwap - 1) * 1e4,
fills=fills,
)
# ============ 演示:在合成 U 型量分布下对比四种算法 ============
def make_synthetic_bars(T: int = 24, seed: int = 0) -> Tuple[List[Bar], np.ndarray]:
rng = np.random.default_rng(seed)
# U 型量曲线(开收盘高、午盘低)
t = np.linspace(-1, 1, T)
shape = 0.6 + 1.5 * t ** 2
volume_curve = shape / shape.sum()
# 随机扰动得到当日真实量
daily_volume = 1e7
realized_vol = volume_curve * (1 + 0.2 * rng.standard_normal(T))
realized_vol = np.maximum(realized_vol, 0.005)
realized_vol /= realized_vol.sum()
volumes = realized_vol * daily_volume
# 价格随机游走
sigma_bar = 30.0 # bps
log_ret = rng.standard_normal(T) * sigma_bar / 1e4
prices = 100.0 * np.exp(np.cumsum(log_ret))
bars = [
Bar(i, float(prices[i]), float(volumes[i]), sigma_bar)
for i in range(T)
]
return bars, volume_curve # 返回真实曲线与历史先验(这里取一致)
def compare_algos(seed: int = 0) -> pd.DataFrame:
bars, hist_curve = make_synthetic_bars(T=24, seed=seed)
total = 5e5 # 50 万股,相当于日量 5%
algos = {
"TWAP": TWAP(jitter=0.2, rng=np.random.default_rng(1)),
"VWAP": VWAP(hist_volume_curve=hist_curve),
"POV(10%)": POV(rho=0.10, cap_ratio=0.3),
"IS(λ=1e-7)": IS_AlmgrenChriss(lam=1e-7, eta=1e-3, sigma=30.0),
"IS(λ=1e-5)": IS_AlmgrenChriss(lam=1e-5, eta=1e-3, sigma=30.0),
}
rows = []
for name, algo in algos.items():
plan = algo.plan(total, bars)
rep = simulate_execution(plan, bars, eta_bps_per_pct=80.0)
rows.append({
"algo": name,
"n_slices": sum(1 for s in plan if s.qty > 0),
"avg_price": round(rep.avg_fill_price, 4),
"IS_bps": round(rep.is_bps, 2),
"vs_TWAP_bps": round(rep.twap_bps, 2),
"vs_VWAP_bps": round(rep.vwap_bps, 2),
})
return pd.DataFrame(rows)
if __name__ == "__main__":
# 多种子求平均,避免单次结果误导
seeds = list(range(50))
frames = [compare_algos(s) for s in seeds]
avg = (
pd.concat(frames)
.groupby("algo", as_index=False)
.mean(numeric_only=True)
.round(2)
)
print(avg)跑这段代码可以观察到几条反复出现的现象:
- VWAP 的 vs_VWAP_bps 接近 0——这是它的设计目标。如果当日量分布与历史先验偏离不大,VWAP 算法相对市场 VWAP 的滑点就在 ±2 bps 以内。
- TWAP 的 vs_TWAP_bps 接近 0、vs_VWAP_bps 不为 0——TWAP 不优化成交量基准。
- POV 的滑点取决于参与率——参与率越大,冲击越大;越小越接近 VWAP/TWAP(但完成时间更长)。
- IS(λ 大) 把成交集中在前段 bar 里,滑点 IS_bps 接近 0(前段 bar 的价格就是到达价,未来漂移影响小),但冲击成本上升;IS(λ 小) 接近 TWAP,承担更多波动风险。
工程评估时不能只看一个种子,必须对 50–200 个种子做统计,看分布与置信区间,才能判断算法之间的差异是否显著。
八·五 与 OMS / SOR 的接口
生产里上面的 SliceAlgo.plan()
接口在每个事件触发时被调用,返回的 Slice
不直接发到交易所,要先经过 SOR
决定「这个数量切到哪些场所、用什么子单类型」。这一步是下一篇《智能订单路由》的主题。
母单 / 子单 / 路由后单 / 成交回报四级状态的工程一致性,是本节最后一个工程要点。生产系统普遍的实现方式是事件溯源(event sourcing):所有状态变化以事件流形式持久化(每个 ExecutionReport 都进 Kafka / Pulsar),状态机本身从事件流回放重建,任何一致性问题都可以通过回放 + 比对发现。这套机制的好处是任何一笔成交都能审计到秒级,缺点是实现复杂、对消息中间件依赖重。机构里常见的折中是「关键路径事件溯源 + 非关键路径同步状态」。
八·六 运行结果解读与回测警告
下面这张表展示了上面 compare_algos 函数对 50
个随机种子求平均后的典型形态(具体数字会随种子集合与冲击参数变化,下表是在文中默认参数
eta_bps_per_pct=80、母单占日量约
5%、母单方向买入下的代表性数值,仅用于解释趋势,不代表任何品种的实测值):
| 算法 | 切片数 | IS_bps(相对到达价) | vs_TWAP_bps | vs_VWAP_bps |
|---|---|---|---|---|
| TWAP(jitter 0.2) | 24 | 与价格漂移正相关 | ≈ 0 | 由量分布偏离决定 |
| VWAP | 24 | 由价格漂移决定 | 由量分布决定 | ≈ 0 |
| POV(10%) | 24 | 居中 | 居中 | 居中 |
| IS(λ=1e-7) | 24 | 接近 TWAP | ≈ 0 | 略偏离 VWAP |
| IS(λ=1e-5) | 24 | 更小(前置) | 明显大于 0 | 明显大于 0 |
最后一列 IS(λ=1e-5) 的「明显大于 0」是预期现象:高风险厌恶把成交集中在前段,而前段在 U 型量分布里恰好量较少,自己单子相对市场量占比更高,临时冲击更大,相对市场 VWAP 的滑点显著上升。「IS_bps 更小」与「vs_VWAP_bps 更大」并非矛盾——两者衡量的不是同一件事,前者衡量「相对到达价的总体偏离」,后者衡量「相对市场参与节奏的偏离」,IS 算法用后者换前者就是它的设计目标。
任何回测结果都只是「在这套假设下的相对排序」,决不能被当作生产参数。文中仿真隐含了几条非常乐观的假设:
- 自己的成交不影响后续 bar 的市场量与价格(无永久冲击反馈);
- 流动性按线性 cap 截断(实际订单簿厚度分档非线性);
- 没有跨 bar 的取消与重发损耗;
- 没有 latency;
- 假设可以在每个 bar 末获得理想的 bar VWAP 作为成交均价。
机构生产 TCA 系统的仿真器至少要在前四条上做更精细的建模,且要按品种分别拟合冲击参数。任何「跨品种通用的冲击系数」都是糊弄。
八·七 限价、IOC、TIF 与子单类型选择
切片器决定「数量与时刻」,但每条子单的「类型」是另一组关键决策。常见的子单类型在执行算法里的典型用法:
Limit + Day:默认挂单,挂在被动一侧或贴近 mid。VWAP 的被动模式默认使用,TIF=Day 让它在挂单期间持续等待对手方。
Limit + IOC(Immediate-or-Cancel):挂单后未即时成交的部分立刻撤销。POV 的「主动模式」、IS 的「跟进模式」常用。优点是不会留下挂单暴露算法意图,缺点是 spread 成本由自己承担。
Limit + FOK(Fill-or-Kill):要么全部成交、要么全部撤销。极少在切片器里用(切片本身已经把数量切小),但在「整笔吃对手大单」场景偶尔出现。
Market:市价单。机构里通常禁用,因为对手方可以反向 sweep。所有「市价」需求统一改写为「以远档限价 + IOC」的等价形式。
Pegged(中心 / 主 / 副):挂单价格随某个参考价滚动调整(pegged to mid / pegged to best bid / pegged to best ask)。在美股暗池与现货 ECN 中常见,A 股不支持 pegged,需要算法自己每次撤改单模拟。
Iceberg / Reserve:挂单显示量小于实际量。在 LSE、Eurex、CME 等支持 iceberg 的场所,VWAP/POV 算法的被动模式可以用 iceberg 隐藏意图,A 股不支持,需要算法自己拆。
子单类型的选择应当随算法状态动态调整,绝不是「在 OMS
里固定一个类型」就能糊弄。生产里这套决策逻辑通常封装成
OrderTypeSelector 模块,独立测试。
八·八 一致性、幂等性与回放
执行引擎是典型的金钱关键系统:任何一次「以为没下单实际下了」「以为撤了实际没撤」「以为成交了实际未成交」都对应真金白银的损失。工程上要靠三件事保证一致性:
第一是幂等性。每个客户端订单 ID(ClOrdID)全局唯一且持久化,对同一 ClOrdID 的重复请求被网关识别并合并为一次撮合。FIX 协议层 ClOrdID 的唯一性由客户端保证,机构里通常用「机构编号 + 时间戳 + 计数」生成。
第二是事件溯源。所有 inbound(行情、回报)与 outbound(下单、撤改)事件都先 append 到持久化日志(Kafka topic、Aeron archive、MySQL append-only 表),再驱动状态机。任何状态都可以从事件日志重建,任何「state corruption」可以通过回放定位。
第三是时钟与时序。算法状态机依赖事件时序,事件时间戳必须按「交易所时间 → 网关时间 → 引擎时间」三层标记。NTP 同步在毫秒级、PTP 在微秒级。同一秒内多条回报必须按交易所事件序号排序,不能按本地接收时间。
故障恢复路径必须演练:
- 网关重启:FIX session 重连,消息序号 gap 用 ResendRequest(MsgType=2)补齐。
- 引擎崩溃:从最近 snapshot + 增量事件回放,重建母单 / 子单状态。
- 行情丢失:切换备用行情源,行情 gap 期内禁止下新单(避免基于陈旧行情决策)。
- 回报丢失:定时调用 OrderStatusRequest,对未确认子单做状态校验。
每条故障路径在生产前必须做混沌工程演练(Chaos Engineering):人为注入网关重启、行情中断、Kafka 分区迁移等故障,验证执行引擎在故障后能正确恢复到一致状态。
八·九 监控指标
执行引擎的运行健康靠下面这组指标实时监控:
业务指标:
- 滑点分布:每笔母单的 IS_bps、vs_VWAP_bps、vs_TWAP_bps,按品种、算法、PM、时段分组。异常值(如 IS_bps > 50)告警。
- 完成率:截至母单 deadline 的成交完成百分比。低完成率告警(< 95%)。
- 撤单率:撤单数 / 下单数。过高表示算法激进度调整异常或对手方反向操纵。
- 部分成交率:部分成交子单数 / 总子单数。过高表示子单粒度切得太大或限价太被动。
系统指标:
- 下单延迟:从算法决策到 ACK 接收的端到端延迟,p50/p99 分别监控。
- 回报延迟:从交易所撮合时间戳到引擎处理完成时间戳的延迟。
- 行情滞后:本地行情时间戳与上游交易所时间戳的差。
风险指标:
- 单一品种集中度:当前活跃母单按品种分布,任何品种集中度过高告警。
- 参与率超限:自己当日成交占市场量的比例超过预设阈值(如 25%)告警。
- 限价偏离:子单限价相对到达价的偏离过大告警(防止「乌龙指」式参数错误)。
监控指标的告警策略应当区分「立即拉闸」「人工确认」「事后归因」三档,不能所有告警一刀切。
九、参考文献
- Almgren, R., & Chriss, N. (2000). “Optimal execution of portfolio transactions.” Journal of Risk, 3(2), 5–39.
- Almgren, R., Thum, C., Hauptmann, E., & Li, H. (2005). “Direct estimation of equity market impact.” Risk, 18(7), 58–62.
- Bialkowski, J., Darolles, S., & Le Fol, G. (2008). “Improving VWAP strategies: A dynamic volume approach.” Journal of Banking & Finance, 32(9), 1709–1722.
- Bertsimas, D., & Lo, A. W. (1998). “Optimal control of execution costs.” Journal of Financial Markets, 1(1), 1–50.
- Cont, R., Kukanov, A., & Stoikov, S. (2014). “The price impact of order book events.” Operations Research, 62(4), 875–897.
- Gatheral, J. (2010). “No-dynamic-arbitrage and market impact.” Quantitative Finance, 10(7), 749–759.
- Kissell, R. (2013). The Science of Algorithmic Trading and Portfolio Management. Academic Press.
- Nevmyvaka, Y., Feng, Y., & Kearns, M. (2006). “Reinforcement learning for optimized trade execution.” In Proceedings of the 23rd International Conference on Machine Learning (ICML), 673–680.
- Obizhaeva, A. A., & Wang, J. (2013). “Optimal trading strategy and supply/demand dynamics.” Journal of Financial Markets, 16(1), 1–32.
- Perold, A. F. (1988). “The implementation shortfall: Paper versus reality.” Journal of Portfolio Management, 14(3), 4–9.
- FIX Trading Community. FIX Protocol Specification, version 5.0 SP2. 关于 ExecutionReport (MsgType=8)、NewOrderSingle (MsgType=D)、TargetStrategy (Tag 847) 字段定义。
- SEC. (2005). Regulation NMS, 17 CFR Parts 200, 201, 230, 240, 242, 249, and 270. Order Protection Rule(Rule 611)与 NBBO 定义。
- SEC. (2008). Regulation SHO, 17 CFR §242.201. Short Sale Price Test Restrictions(Rule 201)。
- 中国证监会, 上海证券交易所, 深圳证券交易所. 《股票交易规则》《集合竞价规则》《临时停牌规则》(2024 年版)。
- CME Group. (2024). CME Globex Reference Guide:报单类型、撮合规则、最小价位、收盘 cross 机制。
- Binance. (2024). Binance API Documentation:REST API rate limit、WebSocket stream、永续合约资金费率结算规则。
十、系列导航
附录:执行算法工程清单
把这一篇文章里的工程要点汇总成一份生产部署前的对照清单。任何机构上线一套自研执行引擎之前,至少要在每一项上有明确答案:
算法层
状态机层
对账层
风控层
监控层
合规层
这份清单本身不能保证一套执行系统是「正确的」,但任何一项答 No 都意味着系统在生产里有具体的失败模式正在等待被触发。本文从算法到工程到合规这十节文字,本质都是在为这份清单背后每一条提供论据与实现思路。把这份清单贴在执行台显示器上,是这一篇文章最直接的实用价值。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【量化交易】量化交易全景:从信号到订单的工程链路
量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。
量化交易
从因子研究到生产执行的量化交易全栈工程。覆盖市场微结构、数据管线、因子构造、组合优化、回测方法论、执行算法、做市策略、高频架构到生产运维。面向策略研究员与工程师。
【量化交易】市场结构:交易所、做市商、暗池、ECN
系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。
【量化交易】市场微结构:订单簿、价差、流动性、冲击
系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。