土法炼钢 · 系统与基础设施

【量化交易】交易成本模型:冲击成本、滑点、Implementation Shortfall 与 TCA

文章导航

分类入口
quant
标签入口
#tca#slippage#market-impact#square-root-law#almgren-chriss#implementation-shortfall#propagator#transaction-cost

量化交易系列导航

按系列顺序继续阅读,而不是停在单篇。

系列目录上一篇:【量化交易】头寸管理:Kelly、波动率目标、风险预算下一篇:【量化交易】回测引擎设计:事件驱动与向量化

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

回测里写的是「按收盘价成交、扣 5 bps 手续费」,实盘的单子却要排队、吃多档、推动价格,成交均价和那个收盘价之间的差额从一开始就没进模型。常见误区是把成本当成一个常数 bps:冲击随委托量凹增长、随执行时间拉长而下降,机会成本和时间风险却随执行时间上升,两者此消彼长。本文把成本拆成四层,先给速查表,再讲平方根律的经验证据与标定、Almgren-Chriss 最优执行、Implementation Shortfall(IS)与 TCA、回测里怎样嵌入成本,最后给出按 2026 年官方规则核对的 A 股、美股、CME、币安费率口径。订单簿、价差分解、Kyle λ 和永久/临时冲击这些概念的微观机制放在姊妹篇《市场微结构》,本文只讲成本模型怎么标定、怎么用。

成本层 定义(正值 = 对自己不利) 典型量级或建模方法 见
显性费用 结算单上逐笔列出的佣金、税、规费 按官方费率表逐项计。例:A 股卖方印花税 0.05%(2023-08-28 起);美股 SEC Section 31 为每百万美元卖出额 USD 20.60(2026-04-04 起) 第一、七节
滑点 成交均价相对参考价(到达价 mid、VWAP、决策价)的偏离,含半个价差 必须写明参考价;主动单、被动单分开统计;参考价只能向后取 第二节
冲击 自己的委托把价格推向不利方向的部分,分永久与临时 凹函数:\(\propto \sigma (Q/V)^{\delta}\),\(\delta\) 多在 0.4 到 0.7。Almgren et al.(2005)对一只日波动 1.57% 的大盘股、买入 10% ADV 估计:永久 20 bp,临时 22 / 15 / 8 bp(执行 0.1 / 0.2 / 0.5 天) 第三节
机会成本(含延迟) 未成交部分相对决策价的漂移,以及决策到到达之间的漂移 IS 分解中的 \((1-f)\,s\,(P_l - P_d)\,Q\) 与 \(f\,s\,(P_a - P_d)\,Q\);与冲击此消彼长,用 Almgren-Chriss 的风险厌恶 \(\lambda\) 权衡 第三、四、五节

四条结论:

  1. 滑点必须写清参考价。统一约定「正值 = 对自己不利」,参考价只能向后查找(merge_asof(direction='backward')),否则 TCA 本身就在偷看未来。见第二节。
  2. 冲击是凹的,但指数和普适性仍有争论。Almgren et al.(2005)拟合的临时冲击指数为 \(0.600 \pm 0.038\),Sato & Kanazawa(2025)在东京证券交易所全账户数据上测得 \(\delta = 1/2\),Zarinelli et al.(2015)认为对数形式拟合范围更宽。见第三、九节。
  3. 拆得越慢不一定越便宜。在第三节的 Almgren-Chriss 示例中,从 TWAP 切到 \(\lambda = 10^{-5}\) 的前重后轻轨迹,成本标准差从 35.7 bp 降到 15.9 bp,期望成本从 37.5 bp 升到 51.3 bp。见第三、四节。
  4. 回测里的成本要按市场分别标定、留安全边际。用自有成交回报拟合冲击参数,向量化回测加安全系数,事件驱动回测仍要补冲击的反事实。见第六节。

如果你是从回测失真一路查过来的,推断层面的问题(前视偏差、过拟合、数据窥视)见《回测陷阱》;成本模型怎么变成下单策略,见《执行算法:TWAP、VWAP、POV、IS》。

风险提示与适用范围:本文不构成任何投资建议。代码、数据与示例数值只用于说明方法,未经过生产审计;文中的冲击系数和滑点数值来自论文或示例,不能直接当作生产参数,真实参数应使用本机构成交回报按本文方法重新拟合。费率按 2026-10-08 可查到的官方文件核对,之后可能调整,使用前请回到第七节列出的原文核对。最佳执行(best execution)的合规口径以各辖区原文为准,例如美国 FINRA Rule 5310、SEC Rule 605 / 606,欧盟 MiFID II(Directive 2014/65/EU)第 27 条,本文不替代这些文件。


一、交易成本的分类

按是否在结算单上落账,成本分成两类:显性成本(explicit cost)与隐性成本(implicit cost)。前者有发票,后者要靠一个参考价才能算出来。

显性成本

显性成本是结算单、对账单、清算流水上有具体一行的支出,费率由交易所、清算机构、监管机构或经纪合同明文规定。各市场的具体费率和生效日期集中在第七节,这里只讲口径:

印花税这类单边税对换手率是硬约束。以 A 股卖方 0.05% 为例,单边年换手 20 倍的策略每年卖出约 20 倍资产,仅印花税就是 \(20 \times 5\,\text{bp} = 1\%\) 的资产。

显性成本的特点是确定、可预测、可全额计入回测。它们应该作为一张 cost_table 从配置读入,按交易所、品种、方向、账户等级查表后乘到名义额上;只用一个 commission_bps=5 带过的回测,会把卖方税和按股计费的部分算错。

隐性成本

隐性成本是没有明确发票、不在结算单上单列、要靠「假设一个不存在的基准价格」才能算出来的成本。它包括:

这五项不是并列关系,而是覆盖同一条时间轴上的不同区间。Perold(1988)提出的 Implementation Shortfall(IS) 把它们放进同一个账本:纸面组合(按决策价瞬间成交、无成本)与真实组合的收益差。显性成本是确定支出,隐性成本是随市场状态波动的支出,两者之和才是策略真实承担的成本。

下图把决策、到达、执行、期末四个时间点和延迟、执行、机会、显性四段成本画在一条时间轴上,图中 \(P_x\) 即正文的成交均价 \(\bar P\)。第四节的 IS 公式、第五节的 TCA 报表都在这条时间轴上切分。

Implementation Shortfall 分解:决策价、到达价、成交均价、期末价与四段成本

二、滑点的多种定义

「滑点」这个词在散户交易者的语言里指「下了一个限价单结果成交价比想象的差了一点」,定义松散且常和「点差」混用。在机构 TCA 里,滑点必须明确写出参考价(benchmark),否则讨论无法进行。同一笔成交相对不同的参考价,「滑点」的数值与符号都可能完全不同。

三种主流的参考价

到达价(arrival price)。下单系统接收到子任务(child order)的那一刻,市场上的参考价。一般取 mid(最优买一与卖一的中点),有时取 last(最近一笔成交),机构默认用 mid 以避免被「最近一笔大单成交价」污染。到达价滑点(arrival slippage)测量的是「我从把单子发出去那一刻起,相对当时市场的公允价,我多付/少付了多少」,是衡量执行算法本身好坏的最常用指标。

VWAP(Volume Weighted Average Price,成交量加权均价)。一段时间内全市场所有成交的「成交额 / 成交量」。VWAP 滑点是机构经纪与买方之间默认的考核指标——客户给券商一个母单(parent order),约定按当日 VWAP 结算,券商承诺执行价不差于 VWAP 多少 bps。VWAP 的好处是平滑、几乎不可被操纵(除非占据全天大部分成交量);坏处是它把自己的成交也算进了 VWAP,导致大单的「VWAP 滑点」被自己稀释(自我成就,self-fulfilling)。

TWAP(Time Weighted Average Price,时间加权均价)。一段时间内 mid 价按等时间间隔的均值,与成交量无关。TWAP 主要用在两类场景:(1) 流动性很差的品种,VWAP 容易被几笔大单偏倚;(2) 监管报告里要求时间均价的合规口径。

除此之外还有一些次要参考价:前一交易日收盘价(previous close)用于隔夜决策的策略;当日开盘价(open)用于「在开盘前生成的 alpha 信号」;决策时刻 mid(decision-time mid)用于 IS 中区分延迟成本与执行成本;逆向 mid (contra mid)——成交那一刻对手方价格——用于衡量做市策略的 mark-to-market loss。

滑点公式与符号约定

记买方向 \(s \in \{+1, -1\}\)(买入 +1、卖出 -1),成交量 \(q\),成交均价 \(\bar P\),参考价 \(P_b\)。符号统一约定:滑点正值表示对自己不利:

\[ \text{Slippage}_{\text{bps}} = s \cdot \frac{\bar P - P_b}{P_b} \cdot 10^4 \]

买入时若 \(\bar P > P_b\)(多付了),滑点为正;卖出时若 \(\bar P < P_b\)(少收了),滑点也为正。这条符号约定在生产 TCA 系统里必须严格执行——一旦买卖方向上的符号搞错,整张报表会变成「买入策略看起来在赚钱,卖出策略看起来在亏钱」的灾难。

主动单与被动单的滑点本质不同

把所有成交无脑混到一起算「平均滑点」是初级 TCA 的常见错误。同一笔母单在执行算法里会被拆成大量子单,子单按是否吃对手方分为:

衡量主动单的好坏看「相对到达 mid 多付了多少 spread」;衡量被动单的好坏看「成交率(fill ratio)」与「逆向选择损失(adverse selection cost)」——后者指挂单成交后短时间内(一般 5 秒到 60 秒)价格反向跑掉的部分。一个 TCA 报表如果只报「平均滑点 X bps」却不区分 aggressive / passive,本质上就是把高频做市的「主动让利」和择时单的「主动吃单」混到一起算,结论无意义。

可运行代码:到达价滑点与决策价滑点

下面给一段可以直接跑的 Python 代码,计算到达价滑点与决策价滑点。输入是一张子单成交回报表 fills(来自 FIX 引擎的 ExecutionReport,字段 parent_id, side, qty, price, ts, liquidity_flag),一份 mid 行情快照 quotes,以及母单元数据 parent_meta。VWAP 滑点需要全市场成交流,放在后面的工程要点里单独说。

import pandas as pd
import numpy as np

def compute_slippage(fills: pd.DataFrame,
                     quotes: pd.DataFrame,
                     parent_meta: pd.DataFrame) -> pd.DataFrame:
    """
    fills: child-order fills, columns:
        parent_id, side(+1/-1), qty, price, ts(datetime64[ns]), liquidity_flag('A'/'P')
    quotes: mid price snapshots, columns:
        symbol, ts, mid
    parent_meta: parent orders, columns:
        parent_id, symbol, side, decision_ts, arrival_ts, target_qty
    Returns:
        per-parent row with arrival / decision slippage in bps.
    """
    # 1) 子单 reduce 到母单层
    exec_qty = fills.groupby('parent_id')['qty'].sum()
    exec_notional = (fills['qty'] * fills['price']).groupby(fills['parent_id']).sum()
    exec_vwap = exec_notional / exec_qty

    # 2) 取每个母单在决策、到达时刻「已经看得见」的最后一个 mid
    quotes_sorted = quotes.sort_values('ts')

    def mid_at(ts_col: str) -> pd.Series:
        return pd.merge_asof(
            parent_meta.sort_values(ts_col), quotes_sorted, by='symbol',
            left_on=ts_col, right_on='ts', direction='backward',
        ).set_index('parent_id')['mid']

    arrival_mid = mid_at('arrival_ts')
    decision_mid = mid_at('decision_ts')

    # 3) 按方向归一,正值表示对自己不利
    side = parent_meta.set_index('parent_id')['side']
    return pd.DataFrame({
        'exec_qty': exec_qty,
        'exec_vwap': exec_vwap,
        'arrival_mid': arrival_mid,
        'decision_mid': decision_mid,
        'arrival_slip_bps': side * (exec_vwap - arrival_mid) / arrival_mid * 1e4,
        'decision_slip_bps': side * (exec_vwap - decision_mid) / decision_mid * 1e4,
    })

用一笔合成买单检验:决策时 mid 为 10.00,到达时(决策后 1.5 秒)最后可见的 mid 为 10.01,两笔成交 100 股 @10.02、200 股 @10.04,成交均价 10.0333,函数输出到达价滑点 23.3 bps、决策价滑点 33.3 bps,两者之差就是延迟成本。本文所有代码块在 Python 3.12.10、numpy 2.5.3、pandas 3.0.6、scipy 1.18.1、statsmodels 0.15.0 下逐段执行过,正文引用的输出都来自这次运行。两个工程要点:

第一,对齐参考价用 merge_asof 而不是简单 join。母单 arrival_ts 是连续时间,行情 quotes 是离散采样,必须用「向后查找最近的 quote」(direction='backward')才能拿到「下单瞬间市场上看得见的最后一个 mid」。在生产中如果用了 direction='nearest' 会偷看未来——这是 TCA 报告里最常见的口径错误。

第二,VWAP 必须独立估计。生产里不能用「fills 表本身」当 VWAP 基准,因为 fills 是自己的成交,会被自己污染。VWAP 必须从交易所成交回报流(market trades feed)里另算,再减去自己的成交(self-trade exclusion),剩下的才是「除我之外的市场 VWAP」(market-ex-self VWAP),这才是公允考核基准。


三、冲击成本模型

冲击成本(market impact)指自己的下单行为推动价格向不利方向移动所产生的成本。它和滑点的边界并不锐利——简单来说,滑点是观测量,冲击是滑点里「能归因到自己下单行为」的那部分。冲击模型回答两类问题:(1) 给定母单总量 \(Q\) 与时间窗 \(T\),预计承担多少冲击?(2) 给定冲击预算,最优拆单策略是什么?

线性冲击模型

最简单的形式假设瞬时冲击与下单速率成线性关系。记瞬时下单速率为 \(v_t = dx_t / dt\)(手 / 秒),日均成交速率为 \(V\)(手 / 秒),价格冲击为:

\[ \Delta P_t = \eta \cdot \sigma \cdot \frac{v_t}{V} \]

其中 \(\sigma\) 是当日波动(通常用 daily volatility),\(\eta\) 是临时冲击系数(temporary impact),无量纲。线性模型最早出现在 Bertsimas & Lo (1998)、Almgren & Chriss (2000) 的最优执行框架里。优点是数学处理简单,能解出 closed-form 最优执行轨迹;缺点是与实证不符——主流的成交回报实证研究(Torre 1997、Almgren et al. 2005、Tóth et al. 2011、Frazzini, Israel & Moskowitz 2018)报告的冲击都是随委托量增长的凹函数而非线性。

平方根律

Barra 在 1990 年代的 MIM(Market Impact Model,Torre 1997)里就把冲击设为与 \(\sqrt{Q/V}\) 成正比;Almgren et al. (2005) 用 Citigroup 美股交易台 2001–2003 年的大样本母单数据做了系统拟合,得到下面的形式:

\[ \text{Cost}_{\text{permanent}} = \gamma \cdot \sigma \cdot \left( \frac{Q}{V} \right) \]

\[ \text{Cost}_{\text{temporary}} = \eta \cdot \sigma \cdot \left( \frac{Q}{V \cdot T} \right)^{0.6} \]

永久冲击对累计量近似线性,临时冲击对下单速率的指数论文估计约为 0.6,工程上取 0.5(即平方根)差别不大。把二者合到 IS 公式里,给定 \(Q\) 与 \(T\),单位名义额冲击成本近似为:

\[ \text{IS}(Q, T) \approx \frac{1}{2} \gamma \sigma \frac{Q}{V} + \eta \sigma \left( \frac{Q}{VT} \right)^{0.6} \]

第一项与 \(T\) 无关、与 \(Q\) 线性;第二项与 \(T\) 有关、随 \(T\) 减小、随 \(Q\) 凹。后面 Almgren-Chriss 优化就是在这两项与时间风险之间做权衡。

为什么是平方根而不是线性?两个角度可以理解。一是潜在流动性(latent liquidity):Tóth et al.(2011)论证,对一个缓慢执行的大单而言,起作用的不是屏幕上的挂单,而是随价格移动陆续显现的潜在供给,其密度在当前价格附近近似随距离 \(\delta\) 线性增长。于是 mid 附近 \(\delta\) 之内的累计供给量是 \(O(\delta^2)\),消耗 \(Q\) 的委托把价格推动 \(O(\sqrt{Q})\)。如果挂单密度是常数,累计量与 \(\delta\) 成正比,得到的反而是线性冲击。二是Bouchaud 等人的 propagator 框架,从订单流的长记忆出发给出另一种解释(见下文)。

Bouchaud propagator 模型

Bouchaud, Gefen, Potters & Wyart (2004) 把冲击建模为线性响应函数:

\[ \Delta P_t = \sum_{s \le t} G(t - s) \cdot \epsilon_s \cdot f(v_s) + \text{noise} \]

其中 \(\epsilon_s = \pm 1\) 是 \(s\) 时刻成交方向,\(f(v_s)\) 是对成交量的凹变换(论文使用 \(\log v\) 形式),\(G(\tau)\) 是衰减核(propagator),实证上按幂律衰减 \(G(\tau) \sim \tau^{-\beta}\)。成交方向本身有长记忆,自相关按 \(\tau^{-\gamma}\) 衰减、\(0 < \gamma < 1\);要让价格仍近似随机游走,两个指数需满足 \(\beta = (1 - \gamma)/2\),所以 \(\beta\) 落在 0 到 1/2 之间,具体值随市场和样本而变。

关键观察是:冲击不是瞬时的,而是带长记忆衰减的。Bouchaud 框架下「方向流(order flow)有强自相关,价格序列接近随机游走」这两个事实可以同时成立——后续相反方向的成交把前一笔的冲击抵消掉一部分,留下来的「永久部分」远小于即刻冲击。这与 Hasbrouck (1991) 的「永久 vs 临时」VAR 分解是相通的。

工程上 propagator 模型主要用在做市与高频策略:拟合自己的下单序列对 mid 的响应,反推 \(G(\tau)\),用于挂单价位与撤单时机的策略调参。中频策略一般不需要自己拟合 propagator,直接用平方根律即可。

Almgren-Chriss 最优执行

Almgren & Chriss (2000) 把执行问题写成「成本期望 + 风险厌恶 × 成本方差」的均值-方差最优化:

\[ \min_{x_t} \quad \mathbb{E}[C(x)] + \lambda \cdot \mathrm{Var}[C(x)] \]

其中 \(x_t\) 是 \(t\) 时刻剩余持仓,\(C(x)\) 是从 \(0\) 到 \(T\) 的累计执行成本,\(\lambda\) 是风险厌恶系数。令永久冲击系数为 \(\gamma\)、临时冲击为 \(\eta\)、价格波动为 \(\sigma\),离散时间下:

\[ x_k = X \cdot \frac{\sinh(\kappa (T - t_k))}{\sinh(\kappa T)}, \quad \kappa = \sqrt{\frac{\lambda \sigma^2}{\eta}} \]

风险厌恶 \(\lambda \to 0\) 时退化为 TWAP(线性轨迹);\(\lambda\) 大则前重后轻(front-loaded),尽快交易避免持仓暴露。

下面给一段实现 Almgren-Chriss 解的 Python 代码,可以直接喂母单参数得到最优拆单计划:

import numpy as np

def almgren_chriss_schedule(X: float, T: float, N: int,
                            sigma: float, eta: float, gamma: float,
                            lam: float) -> np.ndarray:
    """
    X: 母单总量(股 / 手 / 张)
    T: 执行时间窗(秒、分钟均可,需与 sigma 单位一致)
    N: 拆分成 N 段
    sigma: 价格波动(与 T 单位一致)
    eta: 临时冲击系数(USD per (unit/time))
    gamma: 永久冲击系数(USD per unit)
    lam: 风险厌恶系数 (1/USD)
    返回长度 N+1 的数组,x_k = 第 k 段开始时剩余仓位
    """
    tau = T / N
    # 离散化的 kappa:eta_tilde = eta - gamma * tau / 2
    eta_tilde = eta - 0.5 * gamma * tau
    kappa_tilde2 = lam * sigma**2 / eta_tilde
    kappa = np.arccosh(0.5 * kappa_tilde2 * tau**2 + 1) / tau
    t = np.arange(N + 1) * tau
    # 持仓轨迹 x_k:从 X 衰减到 0
    return X * np.sinh(kappa * (T - t)) / np.sinh(kappa * T)

def schedule_to_trades(schedule: np.ndarray) -> np.ndarray:
    """把持仓轨迹 x_k 转换为每段成交量 n_k = x_{k-1} - x_k"""
    return -np.diff(schedule)

# 例:100 万股、股价 50 美元、日波动 2%,30 分钟内分 6 段执行
X, T, N = 1_000_000, 30, 6
sigma = 50.0 * 0.02 / np.sqrt(240)   # 美元 / sqrt(分钟),按每日 240 分钟折算
eta = 2.5e-6     # 示意值,量纲:USD per (share/minute)
gamma = 2.5e-7   # 示意值,量纲:USD per share
for lam in (1e-7, 1e-6, 1e-5):
    x = almgren_chriss_schedule(X, T, N, sigma, eta, gamma, lam)
    print(f'lambda={lam:g}:', np.round(schedule_to_trades(x)).astype(int))

实际运行输出(每段成交股数):

lambda=1e-07: [175014 170431 166794 164085 162286 161390]
lambda=1e-06: [239581 197335 166053 143995 129938 123099]
lambda=1e-05: [517909 250081 121186  59617  31169  20037]

\(\lambda = 10^{-7}\) 时每段约 16 万到 17.5 万股,接近 TWAP 的每段 16.7 万股;\(\lambda = 10^{-5}\) 时第一段就成交一半以上,典型的前重后轻。\(\eta\)、\(\gamma\) 是示意值,只用来展示 \(\lambda\) 的作用方向。

实现要点:

第一,\(\eta\) 与 \(\gamma\) 必须按本机构成交回报数据拟合。论文里给的数值是 Citigroup 2001-2003 的美股拟合值,直接套到 A 股或币安永续上无意义。本节后面的「平方根律的实证拟合」给出最小拟合代码。

第二,\(\lambda\) 的量纲容易搞错。Almgren-Chriss 原始论文里 \(\lambda\) 量纲是 1 / 货币(即 1/USD),实际工程里更直观的做法是直接指定「半轨迹时间」\(T_{1/2} = \log 2 / \kappa\) 或者「能容忍的执行价波动 std」反推 \(\lambda\)。

第三,实盘里 Almgren-Chriss 是计划,不是命令。计划给出每分钟应当成交的目标量,真正的子单仍由更下层的 child router 在 100ms 时间尺度上决定挂被动单还是主动单、挂在哪一档、挂多少。两层之间用「执行进度容忍度(schedule deviation tolerance)」连接,进度落后超过阈值则切到主动单赶进度,进度超前则切回被动单省 spread。

Kyle λ 与微观结构视角

把冲击放回到信息经济学的基础上看,Kyle (1985) 的连续拍卖模型给出了一个最经典的线性形式:

\[ \Delta P = \lambda \cdot Q \]

其中 \(\lambda\) 是「Kyle 系数」,刻画了「每单位订单流推动 mid 多少」的逆向选择强度。Kyle 模型的核心假设是市场上存在一个掌握私人信息的 informed trader 与一组 noise trader,做市商通过观察总订单流推断信息含量并相应调价;\(\lambda\) 越大,意味着市场对订单流越敏感、流动性越差。

Kyle 模型的线性结论与实证的平方根律似乎冲突,二者其实在不同尺度成立:

工程上 \(\lambda\) 的估计可以由 Hasbrouck (1991) 的 VAR 框架给出:把订单流(signed volume)与 mid return 拟合一个二变量 VAR,订单流排在前面做 Cholesky 正交化,意为当期成交可以影响当期价格、反之不行;一单位非预期订单流对价格的长期累积响应,就是永久冲击系数。

import numpy as np
import statsmodels.tsa.api as smt

def estimate_kyle_lambda(returns: np.ndarray,
                         signed_volume: np.ndarray,
                         lags: int = 5,
                         horizon: int = 200) -> float:
    """
    Hasbrouck VAR:变量顺序为 [signed_volume, returns]。
    返回一单位订单流新息(innovation)对价格的长期累积响应。
    """
    data = np.column_stack([signed_volume, returns])
    result = smt.VAR(data).fit(lags)
    irf = result.irf(periods=horizon)
    shock_sd = np.sqrt(result.sigma_u[0, 0])
    # orth_cum_effects[h, i, j]:变量 j 一个标准差正交冲击对变量 i 的累积响应
    return irf.orth_cum_effects[-1, 1, 0] / shock_sd

变量顺序和正交化不能省:如果直接用非正交的 cum_effects,当期价格对当期成交的响应会被留在残差协方差里,估计值接近零。在合成数据上验证(订单流为 AR(1)、价格对订单流新息的真实响应分别设为 0.1、0.3、0.8,各 5000 个样本),上面的函数分别估出 0.109、0.285、0.736。VAR 残差通常厚尾、非正态,标准的 t 检验置信区间偏窄,工程里要配合 bootstrap 给 95% 区间。

永久冲击与临时冲击的分离

工程上经常需要把冲击拆成「永久」与「临时」两部分,因为二者对策略的意义不同:

两部分各占多少随市场、品种和母单规模变化很大,没有可以直接套用的比例;Bouchaud propagator 框架给出的图景是临时部分按幂律缓慢衰减,而不是在母单结束的瞬间消失,所以测量窗口的选择会直接影响拆分结果。

工程上拆分的方法:把母单结束后 30 分钟内的 mid 回归量记为 \(\Delta P_{\text{revert}}\),则:

\[ \text{Permanent} \approx (\bar P - P_a) - \Delta P_{\text{revert}}, \quad \text{Temporary} \approx \Delta P_{\text{revert}} \]

这条估计的工程价值在于:永久冲击对应「策略 alpha 的 information leakage」,可以通过更隐蔽的拆单(dark pool、随机化、隐藏量)压低;临时冲击对应「执行算法的 spread loss」,可以通过更被动的挂单策略压低。两条优化路径不同,必须先拆开再分别处理。

平方根律的实证拟合

一段最小代码,给一张母单 + 成交回报样本表 parents(字段 Q, V, T, sigma, IS_bps),拟合 \(\text{IS} = a + b \cdot \sigma \cdot (Q/(V T))^{\beta}\):

import numpy as np
from scipy.optimize import curve_fit

def sqrt_law(X, b, beta):
    sigma, q_over_vt = X
    return b * sigma * np.power(q_over_vt, beta)

def fit_impact(parents):
    """
    parents: DataFrame with columns Q, V, T, sigma_daily, IS_bps
    Q: 母单数量
    V: 当日 ADV(同一单位)
    T: 执行时长(占当日交易时间的比例,0~1)
    sigma_daily: 日波动(小数,如 0.02 = 2%)
    IS_bps: 实测 IS(bps,正值表示成本)
    """
    sigma = parents['sigma_daily'].values * 1e4  # 转 bps
    q_over_vt = (parents['Q'] / (parents['V'] * parents['T'])).values
    y = parents['IS_bps'].values
    popt, pcov = curve_fit(sqrt_law, (sigma, q_over_vt), y, p0=[0.1, 0.6])
    b_hat, beta_hat = popt
    err = np.sqrt(np.diag(pcov))
    return {'b': b_hat, 'beta': beta_hat, 'b_se': err[0], 'beta_se': err[1]}

拟合前要做样本筛除:剔除极端母单、流动性受限品种、停牌前后异常单。公开文献给出的指数大多落在 0.4 到 0.7 之间(Almgren et al. 2005 约 0.6,Tóth et al. 2011 报告的多个市场接近 0.5),不同市场、不同规模区间的差异需要用自己的成交回报拟合出来,不能借用别人的数值。

下面这张图把三种典型假设(线性、\(\sqrt{}\)、\(q^{0.6}\))的成本曲线画在同一组坐标上,便于直观对比。

成本随委托量增长的曲线

四、机会成本

机会成本(opportunity cost)是 IS 框架里最容易被回测忽略的一项:该买没买到、该卖没卖出去,于是市场跑掉一段,你少赚的那部分。它的存在挑战了一个朴素直觉——拆得越散、走得越慢,冲击成本越小,是不是就一定越便宜?答案是否定的:拆散降低了冲击与滑点,但抬高了机会成本,二者之和有最优点。

未成交部分的机会成本

记母单目标量 \(Q\),实际成交 \(Q_{\text{exec}}\),未成交比例 \(1 - f = 1 - Q_{\text{exec}} / Q\),决策价 \(P_d\),期末参考价 \(P_l\)(一般取期末 mid 或当日收盘)。未成交部分的机会成本为:

\[ C_{\text{opp}} = (1 - f) \cdot s \cdot (P_l - P_d) \cdot Q \]

注意这里也按方向 \(s\) 归一化:买入时如果 \(P_l > P_d\)(市场涨了,没买够),机会成本为正(你少赚了);卖出时如果 \(P_l < P_d\)(市场跌了,没卖完),机会成本同样为正(你少跑了)。

延迟成本

延迟成本(delay cost)是信号触发到子单真正发到交易所之间那段时间里价格走掉的部分:

\[ C_{\text{delay}} = s \cdot (P_a - P_d) \cdot Q \]

延迟成本的来源细分有:

延迟成本看起来是工程问题(拼网络延迟),但它的真正治理在策略侧:只要 alpha 信号衰减时间常数大于延迟时间常数,延迟成本就有限。如果 alpha 半衰期是 30 分钟、下单延迟 10ms,延迟成本可以忽略;如果 alpha 半衰期是 10 秒、下单延迟 50ms,延迟成本会吃掉一半信号。

时间风险与冲击的权衡

把上面三块(执行、机会、延迟)加在一起,再叠加显性 fee,写成 Implementation Shortfall 完整形式:

\[ \text{IS} = f \cdot s \cdot (\bar P - P_d) \cdot Q + (1 - f) \cdot s \cdot (P_l - P_d) \cdot Q + \text{fees} \]

第一项进一步拆为延迟 + 执行:

\[ f \cdot s \cdot (\bar P - P_d) = f \cdot s \cdot (P_a - P_d) + f \cdot s \cdot (\bar P - P_a) \]

前者是延迟成本(成交那部分),后者是执行成本(含滑点 + 冲击 + 时间风险)。所有三项都按符号 \(s\) 与方向归一化为「正值 = 成本」。这是 Perold (1988) 给出的标准分解,也是后面所有 TCA 报告的口径基础。

时间风险(timing risk)是 Almgren-Chriss 框架下的「成本方差」对应物:拉长执行时间窗 \(T\) 降低冲击,但价格的随机游走会让最终成交价的方差增大。其量化形式为:

\[ \mathrm{Var}[C(T)] \approx \sigma^2 \int_0^T x_t^2 \, dt \]

剩余持仓 \(x_t\) 越大、暴露时间越长,时间风险越大。这一项不是「期望成本」,而是成本的不确定性,必须配合风险厌恶系数 \(\lambda\) 进入决策。

取消单与重新下单的核算

未成交部分常见三种处理:(1) 彻底放弃——母单到点撤单,剩余量不再尝试;(2) 滚动到下一窗——挂单失败的部分自动进入下一个执行窗;(3) 强制 catch-up——剩余量切主动单,立即吃完。三种处理对应三种机会成本核算口径:

def opportunity_cost(parent_meta, fills, end_quote):
    """
    parent_meta: parent_id -> {target_qty, side, decision_px, expire_ts}
    fills:        per-parent executed_qty
    end_quote:    parent_id -> reference price at end-of-window
    返回未成交部分的机会成本(按 side 归一化为正值表示成本)
    """
    out = {}
    for pid, meta in parent_meta.items():
        executed = fills.get(pid, 0.0)
        unfilled = meta['target_qty'] - executed
        if unfilled <= 0:
            out[pid] = 0.0
            continue
        delta = end_quote[pid] - meta['decision_px']
        # side: +1 buy, -1 sell
        out[pid] = meta['side'] * delta * unfilled
    return out

工程要点:核算口径必须由策略而非执行台决定。如果策略本身允许放弃未成交(如做市策略),那未成交不应被计入策略 PnL 也不应计入 TCA 机会成本(策略已经主动选择不要这部分敞口);如果策略要求必须成交(如指数追踪、ETF 申购赎回),未成交必须按上述公式扣作机会成本。混淆这两类口径会导致 TCA 报告把执行台的好处理(按时撤单)当成执行台的坏处理(没成交完)。


五、TCA 框架

TCA(Transaction Cost Analysis,交易成本分析)是把上面四节内容工程化为可定期产出的报表与仪表盘的活动。机构里 TCA 同时服务于三类干系人:

  1. 基金经理 / 策略组:评估自己的信号是否在执行后还有 alpha 剩下;
  2. 执行台 / 经纪商关系:评估每个执行算法、每家经纪、每个流动性场所(venue)的优劣;
  3. 合规 / 监管报送:MiFID II(欧)、FINRA Rule 5310 / SEC Rule 606(美)、《证券公司客户资产管理业务管理办法》(中)规定的最佳执行义务,需要按季 / 半年披露。

三类干系人的成本口径并不相同。基金经理关心 IS 与决策价的差距(决策价之前发生的事不计入),执行台关心 arrival slippage(接到母单之后的事),合规关心一系列硬性指标(与全市场 NBBO 比较、与基准价格对比的偏离比例等)。一份合格的生产 TCA 系统必须同时支持三套口径的并行报表。

Implementation Shortfall 主报表

主报表的字段骨架:

字段 含义 量纲
parent_id 母单标识 文本
symbol 标的 文本
side +1/-1 整型
target_qty 母单目标量 股/手/张
decision_ts 决策时刻 时间
arrival_ts 到达时刻 时间
end_ts 收盘 / 母单到期 时间
decision_px 决策价 价
arrival_px 到达价 mid 价
exec_vwap 自身成交均价 价
end_px 期末参考价 价
exec_qty 已成交量 同上
fees 显性费用合计 货币
delay_bps 延迟成本 bps
exec_bps 执行成本 bps
opp_bps 机会成本 bps
fees_bps 显性 bps
IS_bps 合计 bps

下面这段代码是把上面 fills、parent_meta、quotes 三张表 reduce 成一张 IS 报表的核心。parent_meta 在第二节的字段之外再加一列 end_ts(母单到期或收盘时刻)。先定义两个辅助函数:按时间向后取 mid,以及按一张最简费率表计费(真实系统应按第一节逐项计费):

def lookup_quote(quotes: pd.DataFrame, symbols: pd.Series, ts: pd.Series) -> np.ndarray:
    """对每一行 (symbol, ts) 取该时刻之前最后一个 mid,保持输入顺序。"""
    left = pd.DataFrame({'symbol': symbols.values, 'ts': ts.values, '_row': np.arange(len(ts))})
    m = pd.merge_asof(left.sort_values('ts'), quotes.sort_values('ts'),
                      on='ts', by='symbol', direction='backward')
    return m.sort_values('_row')['mid'].to_numpy()

def compute_fees(row: pd.Series, fee_table: dict) -> float:
    """示意费率表:双边佣金 + 卖方印花税,均以 bps 计。"""
    if row['exec_qty'] <= 0:
        return 0.0
    bps = fee_table['commission_bps'] + (fee_table['sell_tax_bps'] if row['side'] < 0 else 0.0)
    return row['exec_qty'] * row['exec_vwap'] * bps / 1e4

def implementation_shortfall(parent_meta: pd.DataFrame,
                             fills: pd.DataFrame,
                             quotes: pd.DataFrame,
                             fee_table: dict) -> pd.DataFrame:
    """
    标准 IS 四段式分解。所有 bps 列正值代表对持有人不利。
    """
    # 子单 reduce
    exec_qty = fills.groupby('parent_id')['qty'].sum().rename('exec_qty')
    exec_notional = (fills['qty'] * fills['price']).groupby(fills['parent_id']).sum()
    exec_vwap = (exec_notional / exec_qty).rename('exec_vwap')

    df = parent_meta.join(exec_qty, on='parent_id').join(exec_vwap, on='parent_id')
    df['exec_qty'] = df['exec_qty'].fillna(0)
    df['fill_ratio'] = df['exec_qty'] / df['target_qty']

    # 价格基准
    df['decision_px'] = lookup_quote(quotes, df['symbol'], df['decision_ts'])
    df['arrival_px']  = lookup_quote(quotes, df['symbol'], df['arrival_ts'])
    df['end_px']      = lookup_quote(quotes, df['symbol'], df['end_ts'])

    side = df['side']

    # 各项成本(货币)
    df['delay_cost']   = df['fill_ratio']         * side * (df['arrival_px'] - df['decision_px']) * df['target_qty']
    df['exec_cost']    = df['fill_ratio']         * side * (df['exec_vwap']  - df['arrival_px'])  * df['target_qty']
    df['opp_cost']     = (1 - df['fill_ratio'])   * side * (df['end_px']     - df['decision_px']) * df['target_qty']
    df['fees']         = df.apply(lambda r: compute_fees(r, fee_table), axis=1)

    # 转 bps(按决策价归一);完全未成交的母单 exec_vwap 为 NaN,执行成本记 0
    notional = df['target_qty'] * df['decision_px']
    df['delay_bps'] = df['delay_cost'] / notional * 1e4
    df['exec_bps'] = df['exec_cost'].fillna(0.0) / notional * 1e4
    df['opp_bps'] = df['opp_cost'] / notional * 1e4
    df['fees_bps'] = df['fees'] / notional * 1e4

    df['IS_bps'] = df['delay_bps'] + df['exec_bps'] + df['opp_bps'] + df['fees_bps']
    return df

沿用第二节那笔合成买单,把目标量设为 400 股(成交 300 股,fill_ratio = 0.75),期末 mid 为 10.03,佣金 2 bps:函数输出延迟 7.5 bps、执行 17.5 bps、机会 7.5 bps、费用 1.5 bps,合计 IS 34.0 bps。

骨架要点:

第一,fill_ratio 必须乘到 delay 与 exec 项上,机会成本则乘 1 - fill_ratio。许多自实现的 TCA 漏掉这个权重,结果把没成交的那部分也按已成交计延迟与执行成本,IS 总和被高估。

第二,decision_px 需要明确取哪一个。Perold 原始定义是「策略生成信号那一刻的可见价格」。如果策略在收盘后跑模型、第二天开盘下单,则 decision_px 是前一日收盘 mid;如果是日内信号、立即下单,则 decision_px = arrival_px(此时延迟成本为 0)。

第三,end_px 取期末 mid 而非期末 last。last 价容易被最后一笔大单污染,实证里能差出 5-10bps 的口径噪声。

IS attribution

把 IS 拆出来之后,下一步是把它归因到不同维度:策略、经纪商、算法、时段、品种、流动性条件、订单大小桶。归因的工程实现是「按维度 group by 之后对 IS 做 quantile + ANOVA」:

def is_attribution(is_df: pd.DataFrame, dim: str) -> pd.DataFrame:
    """
    按 dim 维度(如 strategy / broker / algo / venue / hour / size_bucket)
    汇总 IS 与其分量。
    """
    agg = (is_df.groupby(dim)
                .agg(n=('parent_id', 'count'),
                     notional=('target_qty', lambda s: (s * is_df.loc[s.index, 'decision_px']).sum()),
                     delay_bps=('delay_bps', 'mean'),
                     exec_bps=('exec_bps', 'mean'),
                     opp_bps=('opp_bps', 'mean'),
                     fees_bps=('fees_bps', 'mean'),
                     IS_bps_mean=('IS_bps', 'mean'),
                     IS_bps_p50=('IS_bps', 'median'),
                     IS_bps_p95=('IS_bps', lambda s: np.quantile(s, 0.95)))
                .sort_values('notional', ascending=False))
    return agg

实战经验:

报表骨架的最小可运行版

class TCAReport:
    def __init__(self, parent_meta, fills, quotes, fee_table):
        self.is_df = implementation_shortfall(parent_meta, fills, quotes, fee_table)

    def by(self, dim):
        return is_attribution(self.is_df, dim)

    def summary(self):
        return {
            'n_parents': len(self.is_df),
            'total_notional': (self.is_df['target_qty'] * self.is_df['decision_px']).sum(),
            'avg_IS_bps': self.is_df['IS_bps'].mean(),
            'p95_IS_bps': self.is_df['IS_bps'].quantile(0.95),
            'fill_ratio_mean': self.is_df['fill_ratio'].mean(),
            'top_loss_parents': self.is_df.nlargest(20, 'IS_bps')[['parent_id', 'symbol', 'IS_bps']],
        }

生产 TCA 系统在这个骨架上还要叠加:报表渲染(matplotlib 加 PDF 生成库)、当日实时滚动报告(5 分钟刷新一次)、跨日趋势图(30 天移动均值)、客户专属维度(每家券商一份独立 PDF)、异常告警(IS 超过 3σ 自动钉钉/Slack)。这些都是工程问题,公式与口径就是上面这套。


六、回测中嵌入成本

回测里如何近似真实成本,是策略上线前最后一个关键问题。错误的成本假设比错误的 alpha 更要命——alpha 的失效你能在样本外回测里观察到,错误的成本假设直到真金白银上场才会暴露。下面分两条主流回测路径讨论。

向量化回测的成本嵌入

向量化回测在每日(或每分钟)持仓变化层面工作,输入是「目标权重时间序列」,按价格序列求 PnL。成本以「换手 × 单位成本」形式扣减:

\[ \text{Cost}_t = \sum_i |\Delta w_{i,t}| \cdot c_i(\,|\Delta w_{i,t}| \cdot \text{Equity}_t\,) \]

其中 \(c_i(\cdot)\) 是单位名义额成本函数,依赖于换手量。最常见的两层模型:

向量化代码示例:

def apply_costs_vectorized(weights: pd.DataFrame,
                           prices: pd.DataFrame,
                           adv: pd.DataFrame,
                           sigma: pd.DataFrame,
                           equity_series: pd.Series,
                           fee_bps: float = 5.0,
                           sqrt_b: float = 0.10,
                           sqrt_beta: float = 0.6) -> pd.DataFrame:
    """
    weights: T×N 目标权重
    prices:  T×N 收盘价
    adv:     T×N 当日 ADV(货币计量)
    sigma:   T×N 日波动
    返回每日成本(占总资产比例)
    """
    dw = weights.diff().abs().fillna(0.0)
    # 当日换手对应的名义额(货币)
    notional = dw.mul(equity_series, axis=0)
    # ADV 占比
    q_over_v = (notional / adv).clip(upper=0.5).fillna(0.0)
    # 隐性成本(bps):sqrt 律
    impact_bps = sqrt_b * sigma * 1e4 * np.power(q_over_v, sqrt_beta)
    # 显性 + 隐性
    total_bps = fee_bps + impact_bps
    daily_cost = (dw * total_bps / 1e4).sum(axis=1)
    return daily_cost

工程要点:

第一,q_over_v 必须 clip。某些异常日单只换手会被算成 ADV 的 100% 甚至更多(数据错误或停复牌),不 clip 会让冲击成本爆炸到几百 bps。clip 上限取 30%-50% 并触发告警。

第二,保守原则。回测里默认乘一个安全系数(× 1.5 到 × 2.0),避免「拟合得刚好的成本」在实盘里被市场状态变化打破。这条经验来自实务:策略上线第一年成本几乎一定比回测高。

第三,stamp duty 必须按方向计。A 股印花税单边卖出,向量化代码里要写 dw_sell = (-weights.diff().clip(upper=0)).abs(),再单独乘印花税率。把印花税并入「平均费率」会高估买入成本、低估卖出成本。

事件驱动回测的成本嵌入

事件驱动(event-driven)回测在订单簿(或者 trades feed)层面工作,每个母单按事件序列模拟拆单、挂单、撤单、成交。它的成本不是后验计算出来的,而是在模拟成交那一刻按当时的订单簿状态自然产生的。

事件驱动模型的关键是「成交假设」。三种主流写法:

A 股交易所(沪深)的 L2 数据成本高昂、且不公开队列位置(只公开聚合后的 10 档量),队列模型只能近似(如 Cont-Kukanov-Stoikov 的随机泊松流模型)。美股 NASDAQ/NYSE 的 ITCH/PITCH 数据公开,可以重建逐笔订单簿;CME 的 MDP 3.0 同样公开;币安提供 L2 + trades 的 WebSocket,但不提供 L3(无法重建队列)。

事件驱动回测的成本 = 显性 fee + 模拟成交价与 arrival mid 的差距,二者天然合在一起,无需额外加冲击模型。但 L2 walking 不会模拟「如果我没下这个单,市场后续会是什么样」——这是冲击的反事实问题。处理方法两种:

def event_driven_fill(book_snapshot, order_side, order_qty, latency_ms):
    """
    book_snapshot: list of (px, qty) on opposite side, sorted best-first
    order_side: 'BUY' / 'SELL'
    order_qty: 总量
    latency_ms: 模拟下单延迟,决定看的是哪个 tick 的 book
    Returns: (fill_qty, vwap, residual_qty)
    """
    remaining = order_qty
    notional = 0.0
    for px, level_qty in book_snapshot:
        take = min(remaining, level_qty)
        notional += take * px
        remaining -= take
        if remaining <= 0:
            break
    fill_qty = order_qty - remaining
    vwap = notional / fill_qty if fill_qty > 0 else float('nan')
    return fill_qty, vwap, remaining

这是最朴素的「市价单 walk book」实现。生产里要再叠加:(1) latency 之间 book 的更新(不能用「下单瞬间的 book」,要用「下单后 latency_ms 时刻的 book」);(2) 同一时刻其他单的影响(multi-agent 模拟);(3) 冲击叠加层。

两种回测路径的取舍

向量化路径快、易并行、易扫超参,但成本是后验近似;事件驱动慢、需要 L2 数据、调试复杂,但成本贴合实盘。生产里两个都要:用向量化扫超参选出策略候选,用事件驱动做最终确认与上线前 dry-run。中间的过渡:把事件驱动跑出的成本回拟合为向量化的 \(a + b \sqrt{q/v}\) 公式,让二者结果在中等换手区间一致。

保守原则的工程化

回测成本估计的最大风险不是「估错」而是「估得太精」——精确拟合的成本曲线在实盘里被市场状态变化打破后没有缓冲。工程上的几条保守做法:

  1. 乘 1.5-2.0 的安全系数。回测拟合出 \(a, b\) 后,实际跑回测时乘 \(\alpha = 1.5\)。
  2. 加常数下限。cost_bps = max(fee_bps + impact_bps, 8 bps),避免极小单被当成零成本。
  3. 不同市场分别拟合。A 股、美股、CME、币安四套独立参数,绝不混拟合。
  4. 每月重新拟合。市场流动性按月、按季度变化,固定参数 6 个月以上一定漂移。
  5. 跨日 IS 飘移监控。把实盘 IS 的 30 天滚动均值与回测预测对比,差距超过阈值触发参数复盘。

七、跨市场成本差异

到此为止讨论的都是通用结构,下面把四个主要市场(A 股、美股、CME、币安)的成本口径差异列清楚。这部分的具体数值是 2024 年常见费率,使用前应核对最新规则。

A 股(沪深)

显性费用:

隐性成本特性:

冲击成本:涨跌停和集合竞价会让冲击曲线在极端区间失真,拟合时要把触板、集合竞价成交单独分组,不要混进连续竞价样本。

美股

显性费用:

隐性成本特性:

冲击成本:成交分散在多个 lit venue、dark pool 和 wholesaler 上,拟合前要先统一到 consolidated tape 口径,否则 \(Q/V\) 里的 \(V\) 会被低估。

CME(期货)

显性费用:

隐性成本特性:

冲击成本:ES、NQ、CL、GC 等主力合约深度高、冲击系数低,二线合约和远月合约要单独拟合;滚仓窗口内的样本也应单独分组。

币安(现货 + 永续)

显性费用:

隐性成本特性:

冲击成本:BTCUSDT、ETHUSDT 与长尾币种的冲击系数差异很大,要按流动性分层分别拟合;wash trading 过滤前后拟合结果也会明显不同。

跨市场建模总结

把上面四个市场的关键差异列在一张表里。冲击指数一列不给数值:公开文献的估计大多在 0.4 到 0.7 之间,但分市场的可靠估计需要自有数据。

市场 主要显性 fee 冲击拟合注意点 时段特性 TCA 关键维度
A 股 佣金 + 卖方印花税 0.05% 触板、集合竞价单独分组 T+1, 涨跌停, 集合竞价 时段、停牌前后
美股 监管 fee 极低, maker rebate 统一 consolidated 口径的 \(V\) Reg NMS, 盘前盘后 venue, liquidity flag
CME 单合约固定 fee 滚仓窗口单独分组 近 24x5, 深度集中 合约月份, roll period
币安 现货 0.1%(BNB 抵扣 0.075%), 永续 0.02/0.05 按币种流动性分层 24/7, 日内流动性不均 时区, BTC vs altcoin

八、监控与改进

回测里把成本估对、上线时把成本控住,仅是开始。生产里更难的是持续监控成本是否漂移、按维度归因找到改进抓手、与执行算法形成反馈闭环。

实时 TCA

生产 TCA 不能只跑日终批处理。理由有三:

  1. 执行算法的失误(如挂错档位、被前置交易、触发交易所限频)必须在小时级别发现,否则一天可以亏掉一周的 alpha。
  2. 市场状态突变(流动性骤降、波动飙升)要求执行参数动态调整,参数调整需要实时成本反馈作输入。
  3. 监管要求(MiFID II RTS 27 季度披露、Rule 605 月度披露)的报送数据需从实时系统汇集而非事后重组。

实时 TCA 的工程要求:

分维度归因

实时 TCA 仪表盘上必须看到的几个维度:

每个维度的归因输出都要进入策略侧的反馈:经纪 X 的执行总比经纪 Y 差 5bps → 把母单分配比例从 X:Y = 50:50 调到 30:70;高波动日 IS 算法成本比 VWAP 多 8bps → 高波动日切回 VWAP;占 ADV 5% 以上的母单成本不可控 → 拒绝单一信号生成超过 ADV 5% 的目标,强制策略层切日内分散。

与执行算法的联动

成本监控不能只产出 PDF 报告,必须形成调参闭环。下面是两种最有效的反馈:

反馈一:动态切算法。维护「市场状态 → 算法选择」的查找表,按当日开盘时的实现波动、bid-ask spread、ADV 占比预估自动切换算法。

def select_algo(market_state, parent):
    iv_pct = market_state['iv_percentile']  # 0-100
    spread_bps = market_state['spread_bps']
    q_over_adv = parent['target_qty'] / market_state['adv_estimate']

    if q_over_adv > 0.05:
        return 'POV_5'  # Percentage of Volume,限制不超过 5% 参与率
    if iv_pct > 80 and spread_bps > 20:
        return 'VWAP_FULLDAY'  # 高波动 + 宽 spread,拉长执行
    if iv_pct < 20 and parent['urgency'] == 'high':
        return 'IS_AGGRESSIVE'  # 低波动且急单,前置执行
    return 'VWAP_DEFAULT'

反馈二:动态调冲击系数。冲击系数 \(\eta, \gamma\) 不是固定值,按本机构最近 30 天成交回报每日重新拟合。生产里用 EWMA 让新数据权重更高:

def update_impact_params(today_parents: pd.DataFrame,
                         history_params: dict,
                         alpha: float = 0.05) -> dict:
    """
    每日收盘后调用:today_parents 是当日母单样本(字段同第三节 fit_impact),
    拟合出当日参数后按 EWMA 系数 alpha 与历史参数混合。
    """
    today_params = fit_impact(today_parents)
    return {k: (1 - alpha) * history_params[k] + alpha * today_params[k]
            for k in ('b', 'beta')}

\(\alpha = 0.05\) 对应半衰期约 14 个交易日(\(\ln 0.5 / \ln 0.95 \approx 13.5\))。当日样本太少时(例如母单少于几十笔)应跳过更新,否则单日拟合的噪声会直接进入参数;重大事件窗口内可以临时调大 \(\alpha\),让参数更快跟上市场状态。

改进抓手:从 TCA 到策略

最有意义的 TCA 输出是策略层的改进而不是执行台的改进。三类常见改进:

  1. alpha-cost 联合优化:原来策略层是「最大化夏普」,改成在目标函数里直接扣除预估成本(例如「预期收益 − 预估 IS × 换手」),换手在优化时就被惩罚,而不只是事后扣减。换手越高的中频策略,这一改动的影响越大。

  2. alpha-decay 与执行时长的联动:原来执行时长由风控固定(如「30 分钟内必须执行完」),改成按 alpha 半衰期反推。半衰期 1 小时的 alpha,30 分钟执行窗合理;半衰期 5 分钟的 alpha,30 分钟窗意味着执行到后半段时信号已经衰减了大半。让执行时长 = α 半衰期 × 系数。

  3. 拒绝低质量信号:策略层产生的信号,预估 IS 成本超过预期 alpha 的某个比例(例如 70%)就不发到执行台。阈值要用历史母单回放校准:统计被拒信号在事后的实际 alpha,确认拒掉的确实是负期望的单子。

一个完整的实时 TCA 数据流

把上面所有要素串起来,生产里实时 TCA 的数据流大致如下:

flowchart TD
    strategy["策略层"] --> parent["母单 parent"]
    parent --> om["Order Manager"]
    om --> split["拆单算法 (VWAP/IS)"]
    split --> child["子单 child"]
    child --> risk["风控审批"]
    risk --> fix["FIX 引擎"]
    fix --> exch["交易所"]
    exch --> exec["成交回报 (ExecReport)"]
    exec -->|回报| child
    child --> kfills["Kafka: fills"]
    child --> kquotes["Kafka: quotes (L1/L2)"]
    kfills --> stream["Flink / Spark Streaming"]
    kquotes --> stream
    stream --> rtis["实时 IS (5min 滚动)"]
    stream --> alert["告警 (3σ / fill_ratio < X)"]
    alert --> notify["Slack / 钉钉"]
    stream --> cold["冷数据归档 (ClickHouse / KDB+)"]
    cold --> batch["日终批处理 TCA (按算法/经纪/品种归因)"]
    batch --> feedback["反馈到策略 / 执行调参"]

工程上几个关键点:

第一,fills 与 quotes 必须时间戳对齐。FIX 引擎用机器时间,交易所返回的 transaction time 用交易所时间,两者差几十毫秒到几百毫秒不等。生产里以交易所时间为准做 IS 计算,机器时间仅用作 latency 监控的辅助字段。

第二,孤儿单(orphan fills)必须告警。fills 里出现没有对应 parent 的成交(一般是 order modify / cancel/replace 后状态没同步),TCA 要单独记录并触发对账,不能直接丢掉。

第三,rerouted 母单的 IS 归并。一个母单被风控拒了之后被策略层重新生成新母单,二者在 TCA 里要按「策略意图」合并归一(按 strategy_intent_id 关联),而不是按 parent_id 拆成两笔独立的失败单。

第四,多账户聚合。同一策略在多个账户(自营、客户、专户)跑,IS 报表的「策略层口径」要按 strategy_id 聚合,而「合规层口径」要按 account_id 拆分,两套口径并行。

与执行算法选型的反馈

最后给一张实战上常用的「成本 - 算法 - 市场状态」三维查找表。需要强调:下表数值是示意性的量级示例,并非真实回测或实测产出,仅用于说明同一市场状态下不同算法的相对排序;口径为单笔母单相对到达时点 mid 的 IS(单位 bps),假想股票池为 A 股中盘股、单笔规模约 10%-20% ADV。真实数值高度依赖具体股票池、母单规模、参与率、撮合机制与样本区间,必须用自有成交回报数据重新统计,不能照搬:

市场状态  算法 VWAP TWAP IS-AC POV-5% POV-10%
低波动、宽 spread 12 15 18 11 14
低波动、窄 spread 7 10 13 8 12
高波动、宽 spread 25 22 19 28 35
高波动、窄 spread 15 14 12 18 24
趋势日(单边) 18 15 9 22 28

读法:每个格子是相对到达 mid 的 IS(bps)。读出几条经验:

把这张表沉淀进算法选择器,再叠加策略层的 urgency 标签(「这个信号必须 30 分钟内执行」vs「全天均匀执行即可」),就构成了完整的算法选型反馈闭环。表本身不是结论,是工程方法——每家机构应该按自己的成交回报数据把这张表填出来,按月刷新。

TCA 系统的反模式

最后列几条生产 TCA 系统里见过的反模式,避坑:


九、合规与风险声明

最后再次重申合规边界与风险声明:本文所有代码、数值与示例均为教学用途,未经过完整生产审计。商用 TCA 系统应使用机构内部授权的成交回报数据按本文方法重新拟合参数,绝对不能直接套用文中的 \(\beta\)、\(\eta\)、\(\gamma\) 经验值。冲击成本与滑点估计的数值差异可以达到一个数量级,套用错误参数产生的资金损失由使用者自负。

监管口径方面,最佳执行(best execution)义务在不同司法辖区有不同要求:

上述监管文档的口径、字段定义、报送频率、披露范围与本文教学口径有差异,正式合规系统必须按监管文档原文实现,本文不替代任何正式监管文档。任何把示例代码部署到生产环境或合规报送系统的尝试都需要经过完整审计与法律审核,作者不对此承担任何责任。

涉及个人投资者的部分还要遵守适当性管理、客户告知、风险揭示等要求。本文提到的所有费率(佣金、印花税、监管费等)以撰写时的公开规则为准,可能随交易所、监管机构的政策调整而变化,使用前必须核对最新规则。币安、CME、A 股、美股的具体费率口径以官方公告为准。


参考文献

  1. Perold, A. F. (1988). “The Implementation Shortfall: Paper Versus Reality.” Journal of Portfolio Management, 14(3).
  2. Almgren, R., & Chriss, N. (2000). “Optimal Execution of Portfolio Transactions.” Journal of Risk, 3(2).
  3. Almgren, R., Thum, C., Hauptmann, E., & Li, H. (2005). “Direct Estimation of Equity Market Impact.” Risk, 18(7).
  4. Bertsimas, D., & Lo, A. W. (1998). “Optimal Control of Execution Costs.” Journal of Financial Markets, 1(1).
  5. Bouchaud, J.-P., Gefen, Y., Potters, M., & Wyart, M. (2004). “Fluctuations and Response in Financial Markets: The Subtle Nature of ‘Random’ Price Changes.” Quantitative Finance, 4(2).
  6. Bouchaud, J.-P., Farmer, J. D., & Lillo, F. (2009). “How Markets Slowly Digest Changes in Supply and Demand.” In Handbook of Financial Markets: Dynamics and Evolution, North-Holland.
  7. Hasbrouck, J. (1991). “Measuring the Information Content of Stock Trades.” Journal of Finance, 46(1).
  8. Cont, R., Kukanov, A., & Stoikov, S. (2014). “The Price Impact of Order Book Events.” Journal of Financial Econometrics, 12(1).
  9. Frazzini, A., Israel, R., & Moskowitz, T. J. (2018). “Trading Costs.” Working Paper, AQR Capital Management.
  10. Kyle, A. S. (1985). “Continuous Auctions and Insider Trading.” Econometrica, 53(6).
  11. Glosten, L. R., & Milgrom, P. R. (1985). “Bid, Ask and Transaction Prices in a Specialist Market with Heterogeneously Informed Traders.” Journal of Financial Economics, 14(1).
  12. Hasbrouck, J. (2007). Empirical Market Microstructure: The Institutions, Economics, and Econometrics of Securities Trading. Oxford University Press.
  13. Harris, L. (2003). Trading and Exchanges: Market Microstructure for Practitioners. Oxford University Press.
  14. Cartea, Á., Jaimungal, S., & Penalva, J. (2015). Algorithmic and High-Frequency Trading. Cambridge University Press.
  15. Gatheral, J. (2010). “No-Dynamic-Arbitrage and Market Impact.” Quantitative Finance, 10(7).
  16. Obizhaeva, A. A., & Wang, J. (2013). “Optimal Trading Strategy and Supply/Demand Dynamics.” Journal of Financial Markets, 16(1).
  17. Tóth, B., Lemperiere, Y., Deremble, C., De Lataillade, J., Kockelkoren, J., & Bouchaud, J.-P. (2011). “Anomalous Price Impact and the Critical Nature of Liquidity in Financial Markets.” Physical Review X, 1(2).
  18. Madhavan, A. (2002). “VWAP Strategies.” Trading, 2002(1).
  19. Kissell, R. (2013). The Science of Algorithmic Trading and Portfolio Management. Academic Press.
  20. Johnson, B. (2010). Algorithmic Trading & DMA: An Introduction to Direct Access Trading Strategies. 4Myeloma Press.
  21. SEC (2005). “Regulation NMS.” 17 CFR Parts 200, 201, 230, 240, 242, 249, and 270.
  22. ESMA (2017). “Regulatory Technical Standards on the Application of Position Limits to Commodity Derivatives.” MiFID II RTS 27 / RTS 28.
  23. FINRA (2024). “Rule 5310: Best Execution and Interpositioning.”
  24. CME Group (2024). “CME Globex Reference Guide and Fee Schedule.”
  25. Binance (2024). “Spot and Futures Fee Schedule.” Binance Documentation.
  26. 中国证券业协会 (2023). “证券公司客户资产管理业务规范.”
  27. 中国证券登记结算公司 (2023). “结算业务实施细则.”
  28. 上海证券交易所 (2024). “交易规则.”
  29. Torre, N. G. (1997). “BARRA Market Impact Model Handbook.” BARRA Inc.
  30. Grinold, R. C., & Kahn, R. N. (1999). Active Portfolio Management. McGraw-Hill, 2nd edition.

系列导航

延伸阅读

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。

2026-05-01 · quant

【量化交易】执行算法:TWAP、VWAP、POV、IS

把执行算法从「VWAP 跑得过 benchmark 就行」拆到工程能落地的颗粒度:TWAP 的等时切片与抗操纵改造,VWAP 历史量曲线如何拟合、如何在线纠偏,POV 的目标占比与跟随机制,Implementation Shortfall 算法基于 Almgren-Chriss 求最优轨迹,再到自适应执行的 ML-driven 方向、A 股 / 美股 / 加密三市场口径差异、子单状态机与回报对账的工程结构。给出可直接运行的 TWAP/VWAP/POV/IS 切片器,以及在历史 bar 上模拟执行成本对比。

2026-05-01 · quant

【量化交易】市场微结构:订单簿、价差、流动性、冲击

回测按中价成交,实盘却被价差、冲击和逆向选择吃掉收益。本文从限价订单簿出发拆解价差口径与分解、流动性四维、OFI、Kyle λ、VPIN 与 Hawkes,并用币安 LTCUSDT 真实数据验证:10 秒 OFI 解释约六成中价变化,有效价差三分之二以上表现为价格冲击。

2026-05-01 · quant

【量化交易】风险模型:Barra 多因子、风险归因、压力测试

股票数接近样本天数时,样本协方差会把最小方差组合的风险低估到真实值的约 1-N/T 倍。本文用 Barra 式因子模型降维,对比 Ledoit-Wolf 收缩与 eigenfactor 调整,配合欧拉风险归因、VaR/ES 回测和压力测试,用可运行的模拟代码给出各方法的偏差大小与适用边界。


By .