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

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

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

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

先把最常用的几条结论放在前面:

  1. 滑点必须写清参考价。同一笔成交相对到达价、VWAP、决策价的滑点可以符号都不同;统一约定「正值 = 对自己不利」,并且参考价只能向后查找(merge_asof(direction='backward')),否则 TCA 本身就在偷看未来。见第二节。
  2. 冲击是凹的。主流实证文献报告冲击随委托量近似按 \(\sqrt{Q/V}\) 增长,Almgren et al.(2005)拟合的临时冲击指数约 0.6;线性模型只适合做数学推导。见第三节。
  3. 拆得越慢不一定越便宜。冲击成本随执行时间下降,机会成本和时间风险随之上升,Implementation Shortfall 把延迟、执行、机会成本和显性费用放进同一个账本。见第四、五节。
  4. 回测里的成本要保守。按自有成交回报拟合平方根律,再乘 1.5 到 2 的安全系数,并按市场分别拟合。见第六节。

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

风险提示与适用范围:本文不构成任何投资建议。所有代码、数据与示例数值均用于说明算法,未经过完整生产审计,直接套用产生的资金损失由使用者自负。文中冲击成本系数、滑点估计值仅用于教学,绝对不能直接当作生产环境的费率参数——真实参数应使用本机构的成交回报数据按本文方法重新拟合。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% 收益作税。

第三是过户费、清算费、交易所规费。沪深两市过户费自 2022 年 4 月 29 日起按成交金额 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」糊弄过去的回测都是在欺骗自己。

隐性成本

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

  • 买卖价差成本(spread cost / half-spread):用 mid 价当公允价,主动单吃单要支付一半 spread;
  • 滑点(slippage):从决策那一刻的参考价到实际成交均价之间的差距,包含所有「不确定」的成分;
  • 冲击成本(market impact):自己的单子推动了价格朝不利方向走,导致后续成交逐档变贵;
  • 机会成本(opportunity cost):因为想避免冲击而拆得太散、太慢,没成交完就被市场跑走,剩下没成交的部分以更差的价格被动成交、或者干脆错过;
  • 延迟成本(delay cost):从信号触发到下单系统把单子真正发到交易所之间的时间窗口里,价格已经走掉的部分。

这五项之间的关系不是并列、而是连续覆盖一段时间轴上的不同区间。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 的常见错误。同一笔母单在执行算法里会被拆成大量子单,子单按是否吃对手方分为:

  • 主动单(aggressive / taker):发送时吃掉对手方挂单,立刻成交,付半个 spread;
  • 被动单(passive / maker):挂在自己一侧排队,等别人来吃,省半个 spread 但承担排队风险与逆向选择;
  • 跨档单(marketable limit):限价吃多档,占用多档流动性,付的 spread 大于半个;
  • 冰山单(iceberg):被动挂单的一种,只显示部分量,用于隐藏总意图。

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

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

下面给一段可以直接跑的 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,两者之差就是延迟成本。两个工程要点:

第一,对齐参考价用 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 模型的线性结论与实证的平方根律似乎冲突,二者其实在不同尺度成立:

  • 极短时间(秒级以内):Kyle 线性是合理近似,对应做市商的 inventory-based quote adjustment;
  • 中等时间(分钟到小时):平方根律是更好的拟合,因为限价簿深度被「翻新」(fresh liquidity 进入),冲击的边际成本随累计量减小;
  • 长时间(日级别以上):永久冲击近似线性,对应 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% 区间。

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

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

  • 永久冲击(permanent impact):自己下单永久改变了 mid,相当于自己把价格水位推高了一段,不会回退。这部分对自己是真实损失,对市场是「信息泄露」。
  • 临时冲击(temporary impact / transient impact):自己下单瞬间消耗了一档流动性,mid 被推高,但订单结束后 mid 会逐步回归。这部分对自己是损失(多付了),但对市场是无信息的「流动性消费」。

两部分各占多少随市场、品种和母单规模变化很大,没有可以直接套用的比例;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 信号的算力延迟,毫秒到分钟不等;
  • 风控审批延迟:策略发出母单到风控/合规系统放行的延迟,机构端通常 < 100ms,部分券商定制接入 < 10μs;
  • 路由延迟:从下单 API 到 FIX 引擎到交易所网关的网络延迟;
  • 决策保留延迟:人工干预环节(PM 审核、夜盘/早盘集合竞价等待)造成的几分钟到一小时级别延迟。

延迟成本看起来是工程问题(拼网络延迟),但它的真正治理在策略侧:只要 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(Transaction Cost Analysis,交易成本分析)是把上面四节内容工程化为可定期产出的报表与仪表盘的活动。机构里 TCA 同时服务于三类干系人:

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

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

Implementation Shortfall 主报表

主报表的字段骨架:

下面这段代码是把上面 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

实战经验:

  • 仅看均值会低估尾部成本。p95 比 mean 更能暴露执行算法的失败模式。
  • size_bucket 必须按 ADV 分位(如 [0, 0.5%], [0.5%, 2%], [2%, 5%], [5%, +∞)),按绝对名义额分桶在跨品种比较时无意义。
  • hour 维度上要把开盘前 30 分钟与收盘前 30 分钟单独拎出来——两段尾盘的成本结构与中间时段差距巨大。
  • A 股要按「集合竞价 / 连续竞价 / 收盘集合」三段分别看,连续竞价段内再按「9:30-10:00 / 10:00-11:30 / 13:00-14:30 / 14:30-15:00」四个小段。

报表骨架的最小可运行版

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)\) 是单位名义额成本函数,依赖于换手量。最常见的两层模型:

  • 常数 bps 模型:\(c_i = c_0\),最简单,零成本拟合,绝大多数公开教程用这个。
  • 平方根模型:\(c_i = a + b \sqrt{q_i / V_i}\),按本机构数据拟合 \(a, b\),能对换手大小敏感。

向量化代码示例:

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)层面工作,每个母单按事件序列模拟拆单、挂单、撤单、成交。它的成本不是后验计算出来的,而是在模拟成交那一刻按当时的订单簿状态自然产生的。

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

  • last-trade fill:按 trades feed 里下一笔成交价直接成交,不考虑队列、不考虑深度。最乐观,最常见的初级实现。
  • next-tick mid + spread:成交价 = 下一 tick mid ± half-spread,按订单方向加 spread 损失。中等保守。
  • L2 book walking:限价单按队列位置等候、市价单沿订单簿逐档吃,吃不完的部分按 latency-aware 时间补足。最贴近实盘,但需要完整 L2 数据与队列模型。

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

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

  • 被动假设:默认自己不影响市场,所有成交假设市场会按历史 L2 演化。适用于小单(占 ADV < 0.1%)。
  • 冲击叠加:在 L2 walking 之外再叠加一个统计冲击模型,按 \(\sigma \sqrt{Q/V}\) 抬一下成交价。适用于中单到大单。
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 股(沪深)

显性费用:

  • 佣金:机构客户万 1.5 - 万 3,零售万 2 - 万 5(已含规费);
  • 印花税:单边卖出 0.05%(2023 年 8 月起);
  • 过户费:按成交金额双边 0.001%(百万分之十,由中国结算统一收);
  • 经手费、证管费:经手费 0.00341%(2023 年 8 月 28 日起下调后的沪深标准)加证管费 0.002%,合计约 0.00541%,通常已并入券商佣金报价。

隐性成本特性:

  • T+1 制度导致同日不能买卖,套利策略无法在日内完成,机会成本结构与 T+0 市场不同。
  • 涨跌停(10%、ST 5%、创业板/科创板 20%)造成「板上无成交」事件,机会成本不可控。
  • 集合竞价(9:15-9:25、9:25 撮合、14:57-15:00 收盘集合竞价)成交价为单一价格,与连续竞价口径不同。
  • 北上资金(沪深港通)有独立的额度与申报口径,成本结构含港交所端的费用。

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

美股

显性费用:

  • 佣金:机构 USD 0.0005-0.002 / 股,零售零佣金(Robinhood / IBKR Lite);
  • SEC Section 31 fee:卖方 USD 27.80 / USD 1,000,000(2024);
  • FINRA TAF:卖方 USD 0.000166 / 股,单笔最大 USD 8.30;
  • 不收印花税(除纽约州 STT 法律存在但豁免缴纳);
  • ATS / dark pool:另有按 venue 的额外 fee 与 rebate。

隐性成本特性:

  • Reg NMS 强制 NBBO(National Best Bid Offer),订单可被 reroute 到任意场所,TCA 必须按 venue 拆分。
  • T+1 结算(自 2024 年 5 月 28 日起,从 T+2 缩短);
  • Maker-taker rebate 模型导致挂单可获得返佣(典型 NASDAQ 0.0029 USD/股 maker rebate),TCA 报表里 maker 单的「成本」可能是负值。
  • 盘前盘后(pre-market 4:00-9:30,after-hours 16:00-20:00)流动性降至日均的 5%-15%,成本翻 3-5 倍。

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

CME(期货)

显性费用:

  • 交易所费用:按合约固定,小型 E-mini S&P 500(ES)约 USD 0.32 / 合约;
  • NFA fee:USD 0.02 / 合约;
  • 清算费:USD 0.05-0.15 / 合约;
  • 经纪商佣金:机构 USD 0.10-0.30 / 合约 / 双边。

隐性成本特性:

  • 单一场所、深度集中(CME Globex),无 dark pool,TCA 的 venue 维度退化;
  • 合约规格固定(ES 一手 USD 50 × 指数点),按手数下单而非按金额;
  • 跨期价差(calendar spread)与 BTIC(Basis Trade at Index Close)有专门撮合机制,成本口径独立于 outright;
  • 流动性按时段波动剧烈:亚洲早盘流动性约美东盘中的 1/5,开盘前 30 分钟流动性最差;
  • 滚仓成本:合约到期日前的 roll 操作集中在 8 个交易日内(roll period),冲击成本是平时的 1.5-2 倍。

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

币安(现货 + 永续)

显性费用:

  • 现货:普通 0.1% / 0.1%(maker/taker),抵扣 BNB 0.075% / 0.075%,VIP 9 级 0.020% / 0.040%;
  • 永续 USDT 本位:0.02% / 0.05%(VIP 0),VIP 9 级 -0.005% / 0.017%(maker 负费率,做市补贴);
  • Funding rate:每 8 小时按 USDT 永续合约 mark-index spread 结算,由多空互付(不流向交易所)。Funding 是「持仓成本」而非「交易成本」,但持仓策略必须计入。
  • 没有印花税、没有过户费。

隐性成本特性:

  • 24/7 全天候交易,没有收盘价锚点,日内和周末的流动性差异明显,冲击参数要按时段分组拟合;
  • API 限频:现货按 request weight 计(撰写时为 6000 weight/分钟),期货另有下单频率限制,具体以官方文档为准;过载会导致部分单子被拒(机会成本来源);
  • WebSocket 推送有 2-5ms 抖动,HFT 必须 collocated;
  • 订单簿存在 wash trading 与 layering,L2 数据噪声大,需要 self-trade 与异常订单过滤;
  • 主流币(BTCUSDT、ETHUSDT)冲击系数极低,long-tail altcoin 冲击可达主流币的 5-10 倍。

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

跨市场建模总结

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

八、监控与改进

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

实时 TCA

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

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

实时 TCA 的工程要求:

  • 流式架构:Kafka 接入 fills + quotes,Flink / Spark Structured Streaming 做窗口聚合,5 分钟滚动 IS。
  • 延迟容忍:成交回报到 IS 落地端到端延迟 < 30 秒。
  • 可重放:所有原始数据(fills / quotes / orderbook snapshots)持久化到时序数据库(KDB+ / ClickHouse / Druid),随时回放。
  • 告警阈值:单只标的 IS 超 3σ、母单 fill ratio 低于 80%、自身 spread loss 占比超 50% 等触发。

分维度归因

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

  • 按策略:策略 A 的执行成本是不是比策略 B 高?为什么?是 alpha 弱(信号小,被成本吃掉)还是单子大(占 ADV 高)?
  • 按算法:VWAP / TWAP / IS / POV / Implementation Shortfall 各算法的成本对比,哪个在哪个市场状态下最优。
  • 按经纪 / venue:同一个母单在不同经纪、不同 venue 上的成本,用于砍掉表现差的对手方。
  • 按时段:开盘前 30 分钟 vs 中段 vs 收盘前 30 分钟。
  • 按订单大小桶:占 ADV 0.1%-0.5% / 0.5%-2% / 2%-5% / 5%+ 四档的成本。
  • 按市场状态:高波动日(IV percentile > 80)vs 低波动日,验证冲击系数是否随波动放大。

每个维度的归因输出都要进入策略侧的反馈:经纪 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。真实数值高度依赖具体股票池、母单规模、参与率、撮合机制与样本区间,必须用自有成交回报数据重新统计,不能照搬:

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

  • 低波动条件下 VWAP / POV 类算法占优,因为时间风险小、慢拆能省 spread;
  • 高波动 + 趋势日 IS-AC 显著占优,因为前重后轻能 hedge 时间风险;
  • POV 类算法在窄 spread 情境下本身没有 spread 优势,反而因强制「保持参与率」被市场利用;
  • TWAP 在窄 spread 低波动下与 VWAP 接近,但任何其他情境下都次于 VWAP,是「永远不是最优、永远不是最差」的稳健选择。

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

TCA 系统的反模式

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

  • 只看均值、不看尾部:均值 IS 8bps 看起来不错,p95 却 35bps,意味着每周有几笔母单严重失败,长期下来这部分尾部成本累积超过总成本的 40%。
  • 报表口径每月变:TCA 报表的字段定义、聚合维度、benchmark 选择频繁变化,跨月对比失去意义。生产里口径要冻结至少 6 个月,新口径作为并行报表 A/B 跑 3 个月再切换。
  • 把 TCA 当合规工具不当工程工具:合规交付的 TCA 只回答「我们执行符合最佳执行义务吗」,工程意义的 TCA 必须回答「下个月哪几笔母单可能成本超标」。两套报表共存,不要混用。
  • 把 venue 归因做得太细:把 venue 分到 50 个 dark pool 并对每个独立估均值 IS,每个桶样本数过少(< 30 笔/月),均值估计标准误大于均值本身,结论无意义。先按场所类型聚类(lit / dark / wholesaler / SDP)再细分。
  • 不区分 algo 内部子算法:报表里只有「VWAP」一种,实际生产 VWAP 算法内部有十几个小开关(aggressive aggression 0-100、catch-up 阈值、被动单档位偏移、参与率上限),不区分这些参数的话「VWAP 算法的成本」是十几个版本的混合均值,归因没法落地。

九、合规与风险声明

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

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

  • 欧盟 MiFID II RTS 27(场所执行质量)、RTS 28(投资公司前 5 大执行场所)、Article 27 Principal Adverse Impact 披露;
  • 美国 SEC Rule 605(市场中心执行质量月度报告)、Rule 606(订单路由季度报告)、FINRA Rule 5310(最佳执行)、Reg NMS Rule 611(订单保护规则);
  • 英国 FCA COBS 11.2A(最佳执行,与 MiFID II 平行);
  • 中国证监会《证券公司客户资产管理业务管理办法》、《证券期货经营机构落实资产管理业务「八条底线」禁止行为细则》对最佳执行义务的相关条款;
  • 中国证券业协会《证券公司客户资产管理业务规范》对成本披露的具体要求。

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

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

【量化交易】执行算法: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

【量化交易】回测陷阱:前视偏差、过拟合、数据窥视与 Deflated Sharpe

回测 Sharpe 漂亮却上线失效,多半死于前视偏差、过拟合和数据窥视。本文给出前视偏差的三条机械自查规则,用可复现仿真说明零信号数据上挑出的最佳策略能通过 PSR 却被 DSR 拒掉,并附 Bonferroni、BH-FDR、DSR 的 Python 实现与 30 条上线前自检清单。

2026-05-01 · quant

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

把投资组合的风险从「一个数字」拆成因子、行业、特异三层结构,给出 Barra 横截面回归的可运行 Python 骨架、风险归因、历史模拟法 VaR 与压力测试情景的全流程实现。