跳至正文
MODULE 07全真回测与交易执行3 讲 · BACKTEST & EXECUTION
LECTURE 28共 31 讲预计阅读 55 分钟

第 28 讲:真实交易摩擦全真模拟 ​

本讲核心目标

在量化买方机构中有一句名言:“业余者研究收益,专业者研究风险,顶级专家研究交易成本。”

很多在实验室中跑出年化 30% 超额的优秀模型,一经实盘上线,超额收益便迅速被交易摩擦无情抹平,甚至因资金规模扩大引发流动性踩踏而陷入亏损。本讲将系统解构 A 股全套显性交易成本(印花税、机构佣金、过户费与最低佣金门槛)与隐性执行成本(买卖价差与市场冲击);严格推导微观结构学界著名的平方根法则(Square-Root Law)冲击模型;并以严密的数学微积分推导出策略最大管理规模(AUM Capacity)与资金利润峰值的闭式解析解!


1. 经典文献与工业界理论基石 ​

交易摩擦与最优执行领域的核心经典文献与学术里程碑:

  1. Almgren, R., Thum, C., Hauptmann, E., & Li, H. (2005). "Equity market impact", Risk, 18(7): 58-62.

    依托 Citigroup 数十万笔真实机构大单,实证检验了不同市场波动率、调仓速度与股票流动性下的非线性冲击模型。

  2. Bouchaud, J.-P., Farmer, J. D., & Lillo, F. (2009). "How markets slowly digest changes in supply and demand", Handbook of Financial Markets: Dynamics and Evolution. North-Holland.

    全球微观结构权威综述,详尽证明了限价订单簿(LOB)深度的凹性特征是诱发平方根冲击定律的物理根源。

  3. Tóth, B., Lemperiere, Y., Deremble, C., de Lataillade, J., Kockelkoren, J., & Bouchaud, J.-P. (2011). "Anomalous Price Impact and the Emergence of Arbitrage in Worldwide Markets", Physical Review X, 1(2): 021006.

    跨越美股、欧股、大宗商品和期货市场的大规模实证研究,证明了 I(Q)∝σQ/ADV 平方根法则在跨资产类别中的普遍成立性。

  4. Kissell, Robert (2013). The Science of Algorithmic Trading and Portfolio Management. Academic Press.

    算法交易与交易成本分析(TCA)工业界权威著作,提供了买卖价差、时延滑点与实施落差(Implementation Shortfall)的度量体系。


2. A 股真实交易成本体系:显性规费 vs 隐性冲击 ​

课程图示
可放大查看细节,点击百分比恢复原始大小。

2.1 逐笔显性成本精算规则 ​

对于单笔成交金额为 Q 元的订单,全套显性费用核算规则如下:

成本项目收费方向A 股现行标准费率最低收费门槛限制计费基数与实务要点
印花税(Stamp Duty)仅卖出侧征收0.05%(万分之五,5 bps)无最低门槛国家税收,2023 年 8 月减半征收;公募与私募均无豁免权。
券商佣金(Commission)买入与卖出双向买方机构大客户约 0.015%∼0.025%零售账户设单笔 5 元保底;机构子母账户通常月结分摊包含证券交易所经手费与证监会监管费。
证券过户费(Transfer Fee)买入与卖出双向0.001%(十万分之一,0.1 bp)无最低门槛中国证券登记结算公司(ChinaClear)收取。

最低佣金陷阱(Minimum Commission Trap)

在量化模型回测中,若未设置最低 5 元佣金门槛:当策略调仓分散在 500 只股票中、单只股票仅调仓 1,000 元时,实际有效佣金率为 5/1000=0.50%(万分之五十!),比预期费率高出整整 30 倍!这会导致高频小额调仓策略在实盘中发生剧烈亏损。


3. 平方根法则(Square-Root Law)市场冲击模型 ​

当调仓金额巨大时,隐性市场冲击会迅速超越显性税费,成为吞噬策略利润的最核心杀手。

3.1 平方根冲击定律公式推导 ​

根据 Bouchaud et al. (2009) 与 Tóth et al. (2011) 的微观物理推导,大单在连续限价订单簿中推进时,引发的价格瞬时相对冲击率 I(Q) 并不随交易量线性上升,而是呈现出高度一致的凹函数平方根规律(Sub-linear Square-Root Law):

I(Q)=Y⋅σdailyQADV

其中:

  • Q:当前订单的计划成交金额(元);
  • ADV:该股票在调仓决策前已知的历史 20 日日均成交金额(元);
  • QADV:订单占日均成交量的比例(Participation Ratio);
  • σdaily:个股最近 60 个交易日的日收益率波动率;
  • Y:无量纲经验比例常数,全球成熟市场经验值约为 Y≈0.5∼0.7。

3.2 冲击成本金额的非线性放大 ​

由于冲击成本金额等于成交额乘以冲击率:

Cimpact(Q)=Q⋅I(Q)=Y⋅σdaily⋅Q3/2ADV
  • 幂律放大效应:若单笔交易金额 Q 扩大 4 倍,冲击成本率 I(Q) 扩大 4=2 倍,而总冲击成本金额 Cimpact 将暴增 43/2=8 倍!
课程图示
可放大查看细节,点击百分比恢复原始大小。

4. AUM 策略容量测算与最优利润峰值解析解 ​

现在,我们将交易摩擦、换手率与管理规模统一在严密的代数方程中,推导量化买方机构最关心的策略黄金管理容量(Optimal AUM Capacity)。

4.1 符号设定与年化成本推导 ​

设:

  • A:策略管理资产净值(AUM,元);
  • τ:策略设定的单边年化换手率(Annual Single-Sided Turnover);
  • D=250:全年交易日数;
  • N:投资组合平均持股只数;
  • ADV―:股票池平均日均成交额;
  • σ¯:股票池平均日波动率;
  • s=0.05%:卖方单向印花税;t=0.001%:双向过户费;c=0.015%:双向佣金率。

在每个交易日,单只股票在单边的调仓金额为:

q=A⋅τD⋅N

在最低佣金不生效的前提下,全年由交易摩擦产生的总费率占 AUM 的比例为:

Cannual(A)=τ[s+2t+2c⏟线性税费率 c0+2Yσ¯AτDNADV―⏟非线性冲击费率]

4.2 扣费净超额与总利润函数 ​

设策略未经摩擦扣除的毛预期年化超额收益为 αgross。

策略扣除摩擦后的净年化超额收益率为:

αnet(A)=αgross−Cannual(A)=αgross−τc0−2Yσ¯τ3/2ADNADV―

整个资管产品每年为投资人创造的**超额利润绝对总金额(Dollar Alpha / Profit)**为:

Π(A)=A⋅αnet(A)=A(αgross−τc0)−(2Yσ¯τ3/21DNADV―)A3/2
课程图示
可放大查看细节,点击百分比恢复原始大小。

4.3 最佳容量 A∗ 的数学解析极值求解 ​

令总利润对规模的一阶导数等于零 dΠ(A)dA=0:

dΠ(A)dA=(αgross−τc0)−32⋅2Yσ¯τ3/21DNADV―⋅A=0(αgross−τc0)=3Yσ¯τ3/2A∗DNADV―

两边平方,即可得到策略最大化超额利润的黄金容量解析解(Optimal AUM Closed-Form Solution):

A∗=D⋅N⋅ADV―9Y2σ¯2τ3(αgross−τc0)2

4.4 工业界核心推论 ​

  1. 换手率的三次方反比律(τ3 Penalty):策略容量反比于换手率的三次方!高频策略(τ=40)的容量通常只有低频基本面策略(τ=2)的数千分之一!
  2. 股票池广度平方根律:持股只数 N 扩大一倍,或将股票池拓展至流动性更佳的品种(ADV―↑),策略容量呈线性同比例扩张。
  3. 规模停募红线:若继续扩充规模直至 αnet(A)=0,此时的规模称为策略的“零超额极限容量”,满足 Azero=94A∗=2.25A∗。当规模超过 2.25A∗ 后,基金买得越多亏得越惨!

5. 工业级交易摩擦与容量测算引擎实战 ​

在 tutorial/examples/lecture_28_transaction_friction_and_market_impact.py 中,我们实现了生产级 AshareTransactionCostEngine:

  1. 精准实现买卖不对称印花税、中登过户费与机构最低佣金判定;
  2. 实现基于日成交额 ADV 与波动率的平方根法则冲击模型;
  3. 内置 evaluate_capacity_curve() 函数,扫掠不同 AUM 下的冲击、换手、净超额与利润曲线。

可在终端直接运行验证:

bash
python3 -m examples.lecture_28_transaction_friction_and_market_impact
py
"""显式费率与平方根冲击假设下的订单成本、等额日调仓容量情景。"""
from dataclasses import dataclass
import numpy as np
import polars as pl


@dataclass(frozen=True)
class TradeFrictionDetail:
    symbol: str
    trade_amount: float
    direction: str
    stamp_duty: float
    transfer_fee: float
    commission: float
    impact_cost: float
    total_friction: float
    friction_bps: float


class AshareFrictionEngine:
    """费率为教学配置,非实时税率服务;历史回测须按有效日期选择费率。"""
    def __init__(self, stamp_duty_rate=.0005, commission_rate=.00015,
                 min_commission=5., transfer_fee_rate=.00001, impact_coef_y=.6):
        values = [stamp_duty_rate, commission_rate, min_commission, transfer_fee_rate, impact_coef_y]
        if not np.isfinite(values).all() or min(values) < 0:
            raise ValueError('费率、保底和冲击参数须有限且非负')
        self.stamp_duty, self.commission, self.min_comm, self.transfer_fee, self.impact_y = values

    def calculate_trade_friction(self, symbol, direction, trade_amount, adv_20d,
                                  daily_volatility=.025):
        if direction not in ('BUY', 'SELL'):
            raise ValueError('direction 必须为 BUY/SELL')
        if not np.isfinite([trade_amount, adv_20d, daily_volatility]).all() or trade_amount < 0 or adv_20d <= 0 or daily_volatility < 0:
            raise ValueError('金额须非负、ADV 须为正、波动率须非负,且均须有限')
        if trade_amount == 0:
            return TradeFrictionDetail(symbol, 0., direction, 0., 0., 0., 0., 0., 0.)
        duty = trade_amount * self.stamp_duty if direction == 'SELL' else 0.
        transfer = trade_amount * self.transfer_fee
        commission = max(self.min_comm, trade_amount * self.commission)
        impact = trade_amount * self.impact_y * daily_volatility * np.sqrt(trade_amount / adv_20d)
        total = duty + transfer + commission + impact
        return TradeFrictionDetail(symbol, trade_amount, direction, duty, transfer, commission,
                                   impact, total, total / trade_amount * 10000)


class AUMCapacityEstimator:
    """均匀分散的日调仓假设;单边年换手 τ 对应买/卖各 AUM*τ。

    每日每股每侧一笔订单,因此保底佣金、买卖税差和冲击均通过同一个成本引擎计算。
    不模拟收益随容量变化、订单拆分或流动性动态;结果只是参数情景。
    """
    def __init__(self, gross_alpha_annual=.22, annual_turnover=30., n_stocks=200,
                 mean_adv=80_000_000., mean_vol=.025, trading_days=250,
                 friction_engine=None, max_participation=.1):
        if not np.isfinite([gross_alpha_annual, annual_turnover, mean_adv, mean_vol, max_participation]).all() or annual_turnover < 0 or mean_adv <= 0 or mean_vol < 0 or not 0 < max_participation <= 1:
            raise ValueError('容量参数范围不合法')
        if not isinstance(n_stocks, int) or n_stocks < 1 or not isinstance(trading_days, int) or trading_days < 1:
            raise ValueError('股票数与交易日数须为正整数')
        self.gross_alpha, self.turnover, self.n_stocks = gross_alpha_annual, annual_turnover, n_stocks
        self.mean_adv, self.mean_vol, self.days = mean_adv, mean_vol, trading_days
        self.engine = friction_engine or AshareFrictionEngine()
        self.max_participation = max_participation

    def estimate_capacity_curve(self, aum_candidates):
        if not len(aum_candidates) or not np.isfinite(aum_candidates).all() or min(aum_candidates) <= 0:
            raise ValueError('AUM 候选须非空、有限且为正')
        rows = []
        for aum in aum_candidates:
            q = aum * self.turnover / (self.days * self.n_stocks)
            # 两侧总参与量,不把超过模型适用边界的候选称为可执行容量。
            participation = 2 * q / self.mean_adv
            if participation > self.max_participation:
                raise ValueError('候选的每日双边参与量超出配置上限,请调整执行假设')
            buy = self.engine.calculate_trade_friction('POOL', 'BUY', q, self.mean_adv, self.mean_vol)
            sell = self.engine.calculate_trade_friction('POOL', 'SELL', q, self.mean_adv, self.mean_vol)
            annual_cost = self.days * self.n_stocks * (buy.total_friction + sell.total_friction) / aum
            net_alpha = self.gross_alpha - annual_cost
            rows.append(dict(aum_cny=float(aum), aum_yi=aum / 1e8, daily_two_sided_participation=participation,
                             annual_friction=annual_cost, net_alpha=net_alpha,
                             annual_profit_cny=aum * net_alpha))
        return pl.DataFrame(rows)


if __name__ == '__main__':
    engine = AshareFrictionEngine()
    print(engine.calculate_trade_friction('DEMO', 'SELL', 1e6, 5e7, .02))
    curve = AUMCapacityEstimator(friction_engine=engine).estimate_capacity_curve(
        [1e7, 5e7, 2e8, 5e8, 1e9, 2e9, 4e9])
    print(curve)
    best = curve.sort('annual_profit_cny', descending=True).row(0, named=True)
    print(f'候选网格内最大模型利润:AUM={best["aum_yi"]:.2f}亿元,利润={best["annual_profit_cny"] / 1e4:.2f}万元')
    print('负利润保留;网格最优不是经真实执行验证的策略容量。')

6. A 股不同股票池的典型容量与执行参数参考 ​

资产池标的范围平均单股 ADV典型持股只数 N典型单边年换手 τ毛超额预期测算黄金容量 A∗
沪深 300 指增核心大盘蓝筹15 亿 ~ 30 亿元120 ~ 180 只3×∼6×6%∼10%150 亿 ~ 300 亿元
中证 500 指增中盘成长核心3 亿 ~ 8 亿元200 ~ 300 只8×∼15×12%∼18%50 亿 ~ 100 亿元
中证 1000 指增小盘高弹性股1.5 亿 ~ 3 亿元300 ~ 500 只15×∼30×18%∼25%20 亿 ~ 50 亿元
中证 2000 / 微盘股小微盘流动性前沿3,000 万 ~ 8,000 万元400 ~ 800 只30×∼60×25%∼40%5 亿 ~ 15 亿元

7. 本讲小结与核心自测 ​

核心考点工业标准回答要点常见工程/学术陷阱
为什么大额交易成本呈现非线性的平方根规律?订单簿深度是有限且非平稳的,吃单越大越需要打穿深层档位并诱发其他微观做市商撤单,导致边际冲击递增。误假设市场冲击是固定比例常数(如固定千一),导致策略在百亿规模测算下严重失真。
A 股现行印花税的收取规则是什么?仅对卖出方单向征收,税率为成交金额的 0.05%(5 bps);买入方不征收。错误对买卖双边重复征税,或仍沿用 2023 年 8 月前的单边 0.1% 老税率。
策略最佳利润容量 A∗ 与换手率 τ 的代数关系?A∗ 与 τ3 成反比,即换手率上升一倍,策略最优资金容量急剧萎缩至原先的八分之一。误以为容量与换手率线性反比,导致中高频策略募资失控引发业绩崩盘。

在攻克了交易摩擦之后,A 股市场还有一个让无数海外量化巨头铩羽而归的独特战场——涨跌停板、流动性锁仓与停牌钉子户。

在下一讲中,我们将进入 第 29 讲:A 股特殊撮合规则代码级实现,推导一字涨跌停被动锁仓、申报价格笼子与分红送转全生命周期账本闭环!

QUANTML · Quantitative Research Handbook