土法炼钢兴趣小组的算法知识备份

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

文章导航

分类入口
quant
标签入口
#tca#slippage#market-impact#almgren-chriss#transaction-cost

目录

很多人写策略,回测里夏普 3.0,上线之后跑成 0.5,复盘的时候归因到「市场风格变了」「最近行情不好」「机器学习模型衰减」。这些原因都可能成立,但更常见的、几乎所有中频以下策略上线第一年都会撞上的真实原因是另一个:回测假设的成交价根本不存在。回测里写的是「以收盘价成交,扣 5bps 手续费」,实盘里发出去的单子要排队、要吃多档、要被对手方主动让出来或者主动闪开,最终拿到的成交均价比回测里那个「收盘价」差几十甚至上百个 bps。这部分差额不是黑天鹅、不是因子失效,是工程上从一开始就没建模的交易成本。

交易成本这件事,在卖方研究员的研报里被一带而过——他们关心的是 alpha,把成本当作给基金经理算账的小问题;但在做市商、对冲基金、量化资管的执行台 (execution desk)、券商的电子交易部门里,成本是日常工作的全部内容:客户的钱亏在哪一档买卖盘上、自营单子如何拆才能不被前置交易(front running)、做市报价要给多少 spread 才覆盖逆向选择、机构客户的 TCA(Transaction Cost Analysis,交易成本分析)报告是不是又把锅甩给券商。这些问题没有一个能用「夏普」「IR」之类的策略指标回答,必须用一整套独立的成本模型与归因方法学来处理。

本文把交易成本从「报表上一行手续费」展开成一个四层结构:显性成本、滑点、冲击成本、机会成本。每一层各自有自己的测量方法、估计模型、监控手段。关键结论按公式与文献一一对照,所有 Python 代码片段可以直接拿去拟合自己的成交回报数据。最后一节给出跨市场(A 股、美股、CME、币安)的成本口径差异,以及在生产 TCA 系统里我们实际做的工程权衡。

风险提示与适用范围:本文不构成任何投资建议。所有代码、数据与示例数值均用于说明算法,未经过完整生产审计,直接套用产生的资金损失由使用者自负。文中冲击成本系数、滑点估计值仅用于教学,绝对不能直接当作生产环境的费率参数——真实参数应使用本机构的成交回报数据按本文方法重新拟合。TCA 报告涉及客户告知、最佳执行(best execution)合规义务的部分(如 MiFID II RTS 28、FINRA Rule 5310、SEC Rule 605/606、中国证监会《证券公司客户资产管理业务管理办法》等)有专门的口径与披露要求,本文不替代任何监管文档。


一、交易成本的分类

写代码之前先要把「成本」这个词拆清楚,否则后面所有的归因、归责、调参讨论都会陷入鸡同鸭讲。从机构成交回报数据里能直接看见的东西,按是否在结算单上落账分成两类:显性成本(explicit cost)隐性成本(implicit cost)

显性成本

显性成本是结算单、对账单、清算所流水上有具体一行的支出,跑不掉、抄进会计科目、按交易所与监管口径有明文规定。常见的几条:

第一是佣金(commission)。券商为客户提供经纪服务收取的费用,A 股按成交金额的固定比例收取(万 1.5 到万 3 是机构客户的常见区间,零售账户略高),美股机构经纪可以做到每股 USD 0.0005-0.002,币安现货按成交额收 0.1% 普通账户、0.075% 抵扣 BNB、做市商负费率最低到 -0.005%。佣金对量化策略的影响最容易看见,也最容易被高估——除了零佣金的零售账户,机构端佣金通常是所有成本里比例最低的一项。

第二是印花税(stamp duty / transfer tax)。中国大陆 A 股卖方按成交额 0.05%(2023 年 8 月起从 0.1% 减半),中国香港买卖双边 0.1%,英国伦敦买方 0.5%(Stamp Duty Reserve Tax,电子结算),法国对市值超过 10 亿欧元的本国上市股票买方收 0.3% 的金融交易税(FTT),美股不收印花税但 SEC 收 Section 31 卖方监管费(2024 年费率 USD 27.80 / USD 1,000,000 名义额)。印花税是策略换手率的硬约束,A 股 T+1 单边卖出收 5bps 这一项,意味着年换手 200% 的策略仅这一项就要交 10% 收益作税。

第三是过户费、清算费、交易所规费。沪深两市过户费按面额 0.001%(即百万分之十)双边收取,由中国结算公司统一收;美股 FINRA TAF(Trading Activity Fee)按成交股数 USD 0.000166 / 股(卖方上限 USD 8.30 每笔,2024 费率),CME 期货按合约收(小型 E-mini S&P 单合约约 USD 0.50 含交易所、清算、监管费),币安永续合约 maker 0.02% / taker 0.05% 是「佣金 + 清算 + 交易所」一票打包的「fee」,内部并不区分。

第四是借券费(borrow fee)与融资利息。做空必须借券,借券费按年化对名义市值收,A 股融券标的池窄、券源紧张、借券费可达年化 8%-12%,美股大盘股借券费 25-50bps、热门做空标的可以飙到三位数 bps,币安永续按资金费率(funding rate)每 8 小时结算(多空双方互付)。这些费用在持仓期摊销,是隐藏在「持仓成本」里的显性成本。

第五是外汇交割成本。跨市场交易需要把本币换成结算货币,外汇 spread 按规模与对手方差异在 5bps 到 50bps 之间。这一项常被国内策略团队忽略——做美股的策略应当在回测里直接用美元计价收益、把汇率波动剥离到独立的 FX hedging 子策略里。

第六是监管费、自律费。SEC Section 31 fee、FINRA TAF、欧盟 FTT、中国证券业协会规定的部分会费按业务规模计提。这部分金额小但口径杂,需要靠券商提供的费率表逐项核对。

显性成本的特点是:确定、可预测、可全额计入回测。在写策略的时候它们应该作为一个 cost_table 字典从配置文件里读进来,按交易所 + 品种 + 方向 + 客户等级查出来直接乘到名义额上。任何把显性成本只用一个「commission_bps=5」糊弄过去的回测都是在欺骗自己。

隐性成本

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

这五项之间的关系不是并列、而是连续覆盖一段时间轴上的不同区间。Perold 在 1988 年提出的 Implementation Shortfall(IS) 框架把它们统一到一个公式里,本文第五节会展开。先记住一个直觉:显性成本是固定支出,隐性成本是不确定的滑动支出,二者的总和才是策略真实承担的全成本

下面这张图是后面整篇文章的几何骨架,把决策、到达、执行、收盘四个时间点和三段成本(延迟、执行、机会)画在一条时间轴上。

Implementation Shortfall 分解结构

把这张图记牢。后面无论讨论 VWAP 算法、Almgren-Chriss 最优执行、TCA 归因,本质都在这条时间轴上做切分。


二、滑点的多种定义

「滑点」这个词在散户交易者的语言里指「下了一个限价单结果成交价比想象的差了一点」,定义松散且常和「点差」混用。在机构 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,本质上就是把高频做市的「主动让利」和择时单的「主动吃单」混到一起算,结论无意义。

实测代码:计算到达价滑点与 VWAP 滑点

下面给一段可以直接跑的 Python 骨架。输入是一张子单成交回报表 fills(来自 FIX 引擎的 ExecutionReport,字段 parent_id, child_id, side, qty, price, timestamp, venue, liquidity_flag),以及一份按毫秒采样的 mid 行情 quotes

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 TCA row with all slippage variants in bps.
    """
    # --- 1) 子单 reduce 到母单层 ---
    grp = fills.groupby('parent_id')
    exec_qty = grp['qty'].sum()
    exec_notional = (fills['qty'] * fills['price']).groupby(fills['parent_id']).sum()
    exec_vwap = exec_notional / exec_qty

    # --- 2) 取每个母单 arrival_ts 的 mid ---
    quotes_sorted = quotes.sort_values(['symbol', 'ts'])
    arrival_mid = pd.merge_asof(
        parent_meta.sort_values('arrival_ts'),
        quotes_sorted, by='symbol',
        left_on='arrival_ts', right_on='ts',
        direction='backward'
    )[['parent_id', 'mid']].set_index('parent_id')['mid']

    decision_mid = pd.merge_asof(
        parent_meta.sort_values('decision_ts'),
        quotes_sorted, by='symbol',
        left_on='decision_ts', right_on='ts',
        direction='backward'
    )[['parent_id', 'mid']].set_index('parent_id')['mid']

    # --- 3) 当日 VWAP(基于母单覆盖的时间窗)---
    # 用成交回报代表市场量在工程上不准确,生产中应使用 trades feed
    market_trades = quotes  # placeholder: 实际应是 trades 表,含 px 和 size
    # 占位:留给读者按本机构数据接入

    side = parent_meta.set_index('parent_id')['side']
    arrival_slip = side * (exec_vwap - arrival_mid) / arrival_mid * 1e4
    decision_slip = side * (exec_vwap - decision_mid) / decision_mid * 1e4

    out = pd.DataFrame({
        'exec_qty': exec_qty,
        'exec_vwap': exec_vwap,
        'arrival_mid': arrival_mid,
        'decision_mid': decision_mid,
        'arrival_slip_bps': arrival_slip,
        'decision_slip_bps': decision_slip,
    })
    return out

代码的意思都写在文档字符串里。两个工程要点:

第一,对齐参考价用 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、Frazzini, Israel & Moskowitz 2018)都报告冲击是凹函数而非线性。

平方根律

Barra 早在 1990 年代的 MIM(Market Impact Model)里就提出冲击系数与 \(\sqrt{Q/V}\) 成正比;Almgren et al. (2005) 在 Citigroup 的 70 万笔成交回报上做了完整拟合,得到经典形式:

\[ \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 次方(论文给出 95% 置信区间 [0.55, 0.65],工程上取 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 优化就是在这两项与时间风险之间做权衡。

为什么是平方根而不是线性?两个角度可以理解。一是有限流动性供给:限价簿里在 mid 附近 \(\delta\) 之内的累计挂单量近似与 \(\delta\) 线性,因此把 mid 推动 \(\delta\) 需要消耗 \(O(\delta^2)\) 的市价单,反之消耗 \(Q\) 单子产生 \(O(\sqrt Q)\) 的价格位移。二是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)\) 是对成交量的非线性变换(实证 \(f(v) \propto \log v\)\(f(v) \propto v^{0.5}\)),\(G(\tau)\) 是衰减核(propagator),实证上 \(G(\tau) \sim \tau^{-\beta}\)\(\beta \approx 0.4 \sim 0.6\)

关键观察是:冲击不是瞬时的,而是带长记忆衰减的。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
    kappa_tilde2 = lam * sigma**2 / (eta * (1 - 0.5 * gamma * tau / eta))
    kappa = np.arccosh(0.5 * kappa_tilde2 * tau**2 + 1) / tau
    t = np.arange(N + 1) * tau
    # 持仓轨迹 x_k:从 X 衰减到 0
    x = X * np.sinh(kappa * (T - t)) / np.sinh(kappa * T)
    return x

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

# 例:100 万股,30 分钟,5 分钟一段 6 段;σ=2bps/分钟,η/γ 为典型值
X, T, N = 1_000_000, 30, 6
sigma = 0.02 / np.sqrt(390)  # 假设日波动 2%、240/390 分钟
eta = 2.5e-7   # 拟合得到,量纲:USD per (share/minute)
gamma = 2.5e-8
lam_low, lam_high = 1e-8, 1e-6

x_low = almgren_chriss_schedule(X, T, N, sigma, eta, gamma, lam_low)
x_high = almgren_chriss_schedule(X, T, N, sigma, eta, gamma, lam_high)
print('风险厌恶低(接近 TWAP):', schedule_to_trades(x_low))
print('风险厌恶高(前重后轻):', schedule_to_trades(x_high))

实现要点:

第一,\(\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,永久部分对累计签名量的偏导即 \(\lambda\)

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

def estimate_kyle_lambda(returns: np.ndarray,
                         signed_volume: np.ndarray,
                         lags: int = 5) -> float:
    """
    Hasbrouck VAR:returns_t 与 signed_volume_t 双变量 VAR。
    冲击响应函数(IRF)的 long-run 累积值 / 单位 signed_volume = Kyle lambda。
    """
    data = np.column_stack([returns, signed_volume])
    model = smt.VAR(data)
    result = model.fit(lags)
    irf = result.irf(periods=200)
    # 第二个变量 (signed_volume) 对第一个变量 (returns) 的累积响应
    cum = irf.cum_effects[-1, 0, 1]
    return cum

VAR 残差通常厚尾、非正态,标准的 t 检验置信区间偏窄,工程里要配合 bootstrap 给 95% 区间。

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

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

按 Almgren et al. (2005) 的拟合,永久冲击约占总冲击的 30%-50%,剩下是临时冲击。Bouchaud propagator 框架则给出更细:临时冲击按 \(\tau^{-0.4}\) 缓慢衰减,5-10 分钟后大部分回归。

工程上拆分的方法:把母单结束后 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]}

工程经验值:经过样本筛除(剔除极端母单、流动性受限品种、停牌前后异常单)后,A 股全市场拟合 \(\beta\) 通常落在 [0.55, 0.70],美股大盘股 \(\beta \approx 0.55\)、小盘股 \(\beta \approx 0.65\),币安主流币永续 \(\beta \approx 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 报表的核心:

def implementation_shortfall(parent_meta: pd.DataFrame,
                             fills: pd.DataFrame,
                             quotes: pd.DataFrame,
                             fee_table: dict) -> pd.DataFrame:
    """
    标准 IS 四段式分解。所有 bps 列正值代表对持有人不利。
    """
    # 子单 reduce
    grp = fills.groupby('parent_id')
    exec_qty = grp['qty'].sum().rename('exec_qty')
    exec_vwap = (grp.apply(lambda d: (d['qty'] * d['price']).sum() / d['qty'].sum())
                    .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(按决策价归一)
    notional = df['target_qty'] * df['decision_px']
    for c in ['delay_cost', 'exec_cost', 'opp_cost', 'fees']:
        df[c.replace('_cost', '_bps').replace('fees', 'fees_bps')] = df[c] / notional * 1e4

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

骨架要点:

第一,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']],
        }

    def render_pdf(self, out_path):
        # 占位:留给具体绘图库(matplotlib + reportlab / weasyprint)
        raise NotImplementedError

生产 TCA 系统在这个骨架上还要叠加:当日实时滚动报告(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 股(沪深)

显性费用:

隐性成本特性:

冲击成本拟合经验值:\(\beta \approx 0.55-0.70\),散户参与度高的中小盘股更接近 0.7(凹性更弱)。

美股

显性费用:

隐性成本特性:

冲击成本拟合:大盘 \(\beta \approx 0.55\),小盘 \(\beta \approx 0.65\),sector ETF \(\beta \approx 0.5\)

CME(期货)

显性费用:

隐性成本特性:

冲击成本:ES、NQ、CL、GC 等主力合约 \(\beta \approx 0.5\),深度极高,冲击系数低;二线合约(TY 长债、6E 欧元期货)\(\beta \approx 0.55\)

币安(现货 + 永续)

显性费用:

隐性成本特性:

冲击成本:BTCUSDT \(\beta \approx 0.5\),ETHUSDT \(\beta \approx 0.5\),市值前 50 之外 \(\beta \approx 0.55-0.65\)

跨市场建模总结

把上面四个市场的关键参数列在一张表里:

市场 主要显性 fee 隐性 β 时段特性 TCA 关键维度
A 股 万 1.5 + 卖印 0.05% 0.55-0.70 T+1, 涨跌停, 集合竞价 时段、停牌前后
美股 监管 fee 极低, maker rebate 0.50-0.65 Reg NMS, 盘前盘后 venue, liquidity flag
CME 单合约固定 fee ≈ 0.5 24x6, 深度集中 合约月份, roll period
币安 现货 0.075%, 永续 0.02/0.05 0.50-0.65 24/7, U 型流动性 时区, 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(history_fills, history_params, alpha=0.05):
    """
    每日收盘后调用:把当日 fills 拟合得到的当日参数,
    按 EWMA 系数 alpha 与历史参数混合。
    """
    today_params = fit_impact(history_fills.tail_today())
    new_params = {}
    for k in ['b', 'beta']:
        new_params[k] = (1 - alpha) * history_params[k] + alpha * today_params[k]
    return new_params

工程经验值:\(\alpha = 0.05\)(半衰期约 14 天)能在「跟上市场变化」与「不被单日噪声放大」之间取得平衡。重大事件(FOMC、国内政治会议、加密黑天鹅)窗口期内 \(\alpha\) 临时调到 0.2。

改进抓手:从 TCA 到策略

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

  1. alpha-cost 联合优化:原来策略层是「最大化夏普」,改成「最大化 (夏普 − 0.5 × IS_bps × 换手)」,换手在策略目标函数里被惩罚而不只是事后扣减。这个变化对中频策略效果显著,能把策略生命周期从 6 个月延长到 18 个月。

  2. alpha-decay 与执行时长的联动:原来执行时长由风控固定(如「30 分钟内必须执行完」),改成按 alpha 半衰期反推。半衰期 1 小时的 alpha,30 分钟执行窗合理;半衰期 5 分钟的 alpha,30 分钟窗会让执行成本吃掉一半。让执行时长 = α 半衰期 × 系数。

  3. 拒绝低质量信号:策略层产生的信号,预估 IS 成本超过预期 alpha 的 70% 直接拒单(不发到执行台)。这个简单规则能切掉 20%-40% 的低质量单子,对总 PnL 是显著正贡献。

一个完整的实时 TCA 数据流

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

策略层 ─┐                    ┌─→ 风控审批 ─→ FIX 引擎 ─→ 交易所
        │                    │                              │
        └─→ 母单 (parent) ─┐ │                              │
                           │ │                              ↓
                           ↓ │                          成交回报
                       Order Manager                    (ExecReport)
                           │ │                              │
                           ↓ │                              │
                       拆单算法                              │
                       (VWAP/IS)                            │
                           │                                │
                           ↓                                │
                       子单 (child) ───────────────────────┘
                           │
        ┌──────────────────┴──────────────────┐
        ↓                                     ↓
    Kafka: fills                        Kafka: quotes (L1/L2)
        │                                     │
        └──────────────┬──────────────────────┘
                       ↓
               Flink / Spark Streaming
                       │
        ┌──────────────┼─────────────────────────────────┐
        ↓              ↓                                 ↓
    实时 IS       告警 (3σ / fill_ratio < X)         冷数据归档
    (5min 滚动)   ──→ Slack/钉钉                     (ClickHouse / KDB+)
                                                          │
                                                          ↓
                                                  日终批处理 TCA
                                                  (按算法/经纪/品种归因)
                                                          │
                                                          ↓
                                                  反馈到策略 / 执行调参

工程上几个关键点:

第一,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 拆分,两套口径并行。

与执行算法选型的反馈

最后给一张实战上常用的「成本 - 算法 - 市场状态」三维查找表(数值仅为某机构 A 股中盘股近一年实测中位数,不代表通用结论):

市场状态  算法 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

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

系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。

2026-05-01 · quant

【量化交易】量化交易全景:从信号到订单的工程链路

量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。

2026-05-01 · quant

【量化交易】市场结构:交易所、做市商、暗池、ECN

系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。

2026-05-01 · quant

【量化交易】订单类型与执行语义:限价、市价、IOC、FOK、冰山

把 Limit、Market、IOC、FOK、Iceberg、Stop、MOO/MOC 这些常被混为一谈的订单类型还原为价格、数量、时效、可见性、触发五个独立维度,并对照 A 股、港股、美股、CME、Binance 五个市场的实际语义差异,给出量化系统中的订单工厂、状态机与风控前置校验的工程实现。


By .