第 28 讲:真实交易摩擦全真模拟
本讲核心目标
在量化买方机构中有一句名言:“业余者研究收益,专业者研究风险,顶级专家研究交易成本。”
很多在实验室中跑出年化
1. 经典文献与工业界理论基石
交易摩擦与最优执行领域的核心经典文献与学术里程碑:
- Almgren, R., Thum, C., Hauptmann, E., & Li, H. (2005). "Equity market impact", Risk, 18(7): 58-62.
依托 Citigroup 数十万笔真实机构大单,实证检验了不同市场波动率、调仓速度与股票流动性下的非线性冲击模型。
- 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)深度的凹性特征是诱发平方根冲击定律的物理根源。
- 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.
跨越美股、欧股、大宗商品和期货市场的大规模实证研究,证明了
平方根法则在跨资产类别中的普遍成立性。 - Kissell, Robert (2013). The Science of Algorithmic Trading and Portfolio Management. Academic Press.
算法交易与交易成本分析(TCA)工业界权威著作,提供了买卖价差、时延滑点与实施落差(Implementation Shortfall)的度量体系。
2. A 股真实交易成本体系:显性规费 vs 隐性冲击
2.1 逐笔显性成本精算规则
对于单笔成交金额为
| 成本项目 | 收费方向 | A 股现行标准费率 | 最低收费门槛限制 | 计费基数与实务要点 |
|---|---|---|---|---|
| 印花税(Stamp Duty) | 仅卖出侧征收 | 无最低门槛 | 国家税收,2023 年 8 月减半征收;公募与私募均无豁免权。 | |
| 券商佣金(Commission) | 买入与卖出双向 | 买方机构大客户约 | 零售账户设单笔 5 元保底;机构子母账户通常月结分摊 | 包含证券交易所经手费与证监会监管费。 |
| 证券过户费(Transfer Fee) | 买入与卖出双向 | 无最低门槛 | 中国证券登记结算公司(ChinaClear)收取。 |
最低佣金陷阱(Minimum Commission Trap)
在量化模型回测中,若未设置最低 5 元佣金门槛:当策略调仓分散在 500 只股票中、单只股票仅调仓 1,000 元时,实际有效佣金率为
3. 平方根法则(Square-Root Law)市场冲击模型
当调仓金额巨大时,隐性市场冲击会迅速超越显性税费,成为吞噬策略利润的最核心杀手。
3.1 平方根冲击定律公式推导
根据 Bouchaud et al. (2009) 与 Tóth et al. (2011) 的微观物理推导,大单在连续限价订单簿中推进时,引发的价格瞬时相对冲击率
其中:
:当前订单的计划成交金额(元); :该股票在调仓决策前已知的历史 20 日日均成交金额(元); :订单占日均成交量的比例(Participation Ratio); :个股最近 60 个交易日的日收益率波动率; :无量纲经验比例常数,全球成熟市场经验值约为 。
3.2 冲击成本金额的非线性放大
由于冲击成本金额等于成交额乘以冲击率:
- 幂律放大效应:若单笔交易金额
扩大 4 倍,冲击成本率 扩大 倍,而总冲击成本金额 将暴增 倍!
4. AUM 策略容量测算与最优利润峰值解析解
现在,我们将交易摩擦、换手率与管理规模统一在严密的代数方程中,推导量化买方机构最关心的策略黄金管理容量(Optimal AUM Capacity)。
4.1 符号设定与年化成本推导
设:
:策略管理资产净值(AUM,元); :策略设定的单边年化换手率(Annual Single-Sided Turnover); :全年交易日数; :投资组合平均持股只数; :股票池平均日均成交额; :股票池平均日波动率; :卖方单向印花税; :双向过户费; :双向佣金率。
在每个交易日,单只股票在单边的调仓金额为:
在最低佣金不生效的前提下,全年由交易摩擦产生的总费率占 AUM 的比例为:
4.2 扣费净超额与总利润函数
设策略未经摩擦扣除的毛预期年化超额收益为
策略扣除摩擦后的净年化超额收益率为:
整个资管产品每年为投资人创造的**超额利润绝对总金额(Dollar Alpha / Profit)**为:
4.3 最佳容量 的数学解析极值求解
令总利润对规模的一阶导数等于零
两边平方,即可得到策略最大化超额利润的黄金容量解析解(Optimal AUM Closed-Form Solution):
4.4 工业界核心推论
- 换手率的三次方反比律(
Penalty):策略容量反比于换手率的三次方!高频策略( )的容量通常只有低频基本面策略( )的数千分之一! - 股票池广度平方根律:持股只数
扩大一倍,或将股票池拓展至流动性更佳的品种( ),策略容量呈线性同比例扩张。 - 规模停募红线:若继续扩充规模直至
,此时的规模称为策略的“零超额极限容量”,满足 。当规模超过 后,基金买得越多亏得越惨!
5. 工业级交易摩擦与容量测算引擎实战
在 tutorial/examples/lecture_28_transaction_friction_and_market_impact.py 中,我们实现了生产级 AshareTransactionCostEngine:
- 精准实现买卖不对称印花税、中登过户费与机构最低佣金判定;
- 实现基于日成交额 ADV 与波动率的平方根法则冲击模型;
- 内置
evaluate_capacity_curve()函数,扫掠不同 AUM 下的冲击、换手、净超额与利润曲线。
可在终端直接运行验证:
python3 -m examples.lecture_28_transaction_friction_and_market_impact"""显式费率与平方根冲击假设下的订单成本、等额日调仓容量情景。"""
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 | 典型持股只数 | 典型单边年换手 | 毛超额预期 | 测算黄金容量 |
|---|---|---|---|---|---|---|
| 沪深 300 指增 | 核心大盘蓝筹 | 15 亿 ~ 30 亿元 | 120 ~ 180 只 | 150 亿 ~ 300 亿元 | ||
| 中证 500 指增 | 中盘成长核心 | 3 亿 ~ 8 亿元 | 200 ~ 300 只 | 50 亿 ~ 100 亿元 | ||
| 中证 1000 指增 | 小盘高弹性股 | 1.5 亿 ~ 3 亿元 | 300 ~ 500 只 | 20 亿 ~ 50 亿元 | ||
| 中证 2000 / 微盘股 | 小微盘流动性前沿 | 3,000 万 ~ 8,000 万元 | 400 ~ 800 只 | 5 亿 ~ 15 亿元 |
7. 本讲小结与核心自测
| 核心考点 | 工业标准回答要点 | 常见工程/学术陷阱 |
|---|---|---|
| 为什么大额交易成本呈现非线性的平方根规律? | 订单簿深度是有限且非平稳的,吃单越大越需要打穿深层档位并诱发其他微观做市商撤单,导致边际冲击递增。 | 误假设市场冲击是固定比例常数(如固定千一),导致策略在百亿规模测算下严重失真。 |
| A 股现行印花税的收取规则是什么? | 仅对卖出方单向征收,税率为成交金额的 | 错误对买卖双边重复征税,或仍沿用 2023 年 8 月前的单边 |
| 策略最佳利润容量 | 误以为容量与换手率线性反比,导致中高频策略募资失控引发业绩崩盘。 |
在攻克了交易摩擦之后,A 股市场还有一个让无数海外量化巨头铩羽而归的独特战场——涨跌停板、流动性锁仓与停牌钉子户。
在下一讲中,我们将进入 第 29 讲:A 股特殊撮合规则代码级实现,推导一字涨跌停被动锁仓、申报价格笼子与分红送转全生命周期账本闭环!