你下一笔大单的”成交价”,其实混着两种东西:市场本来就要走的路,和你的单子把价格推离的那部分。绝大多数团队只看 VWAP − 到达价(实现差额,Implementation Shortfall),就以为那就是冲击成本。但市场那段时间可能本来就在涨——这部分不该算在你的头上。
要算清”我自己的单子到底造成了多少冲击”,本质是一个因果问题:反事实地回答”如果我不交易,价格会怎样”。本文用一套可复现模拟讲清临时/永久冲击的分解,以及怎么用外生的市场因子构建反事实基线。
一、冲击拆成两半:临时 + 永久#
经典 Almgren-Chriss 框架把冲击拆成:
- 临时冲击(temporary):你正在吃单时,价格被你推离中间价;一旦你停手,这部分回归。它决定你的执行成本。
- 永久冲击(permanent):你的净买单改变了市场对均衡价的看法,交易结束后价格永久停在更高处。它决定你对组合已实现盈亏的拖累。
把两者混为一谈,是交易成本分析里最常见的错:用临时冲击去估组合层面的隐性成本(高估),或用永久冲击去估单笔执行质量(低估)。
二、为什么这是因果 / 反事实问题#
交易不是随机的(内生性):你决定大单买入,往往是因为你预期要涨;同时市场那段时间可能本来就在涨。朴素 IS 把”市场自己涨的”和”你推的”一起算成成本,于是高估了你的冲击,甚至可能”证明”你每笔都在亏执行成本——而真正的原因是市场 beta。
正确做法是估计反事实无交易路径 cf_t:如果当天你没有出现,价格会怎么走。然后
交易自身冲击_t = 实际价格_t − 反事实价格_tplaintext关键识别假设:反事实基线必须外生于你的单子。单只股票的交易影响不了整个市场指数,所以用市场指数(或板块 ETF)做基准就满足外生性。
三、可复现实验#
模拟一个交易日(390 分钟),ADV=100万股、日波动 2%;你买入 5万股(=5% ADV),分 180 分钟匀速执行。冲击用平方根律建模:临时冲击 ∝ σ·√(参与率),永久冲击 ∝ σ·(Q/ADV)。
import numpy as np
rng = np.random.default_rng(20260711)
M, ADV = 390, 1_000_000
adv_per_min = ADV / M
daily_sigma, sigma_min = 0.02, 0.02 / np.sqrt(M)
# 反事实无交易路径:温和日内波 + 微噪,零净漂移(让冲击清晰可见)
x = np.arange(M)
log_p = 0.003 * np.sin(2 * np.pi * x / M) + rng.normal(0, 0.0015, M)
log_p = log_p - np.linspace(0, log_p[-1], M) # 钉住 p0[0]=p0[-1]=100
p0 = 100 * np.exp(log_p)
def apply_impact(Q, t0=60, Tex=180, temp_coef=0.45, perm_coef=1.5, seed=0):
sigma_bps = daily_sigma * 1e4
rng2 = np.random.default_rng(seed)
path = p0.copy()
em = np.arange(t0, min(t0 + Tex, M))
v_per_min = Q / len(em); part = v_per_min / adv_per_min
temp = temp_coef * sigma_bps * np.sqrt(part) * (p0[0] / 1e4) # 临时冲击(价格单位)
for i, t in enumerate(em):
path[t] += temp * (1 - i / len(em)) # 执行期间线性衰减
perm = perm_coef * sigma_bps * (Q / ADV) * (p0[-1] / 1e4) # 永久水平位移
path[t0 + Tex:] += perm
return path, em, part, temp, perm
Q = 50_000
path, em, part, temp, perm = apply_impact(Q, seed=7)python3.1 反事实基线 + 冲击分解#
我们用市场因子模型估计无交易路径:以到达价为锚,按市场指数的相对变动外推。由于市场指数不被单只股票交易影响,它提供了干净的反事实。在模拟里真实无交易路径就是 p0,我们直接用它展示分解:
arrival = p0[em[0]]
# 执行成本 = 实际成交价 − 反事实路径(同一分钟)
IS_bps = np.mean(path[em] - p0[em]) / np.mean(p0[em]) * 1e4
temp_peak = temp / p0[0] * 1e4
perm_bps = perm / p0[-1] * 1e4
total_impact = temp_peak / 2 + perm_bps # 临时均值 + 永久
print(f"临时冲击峰值: {temp_peak:.1f} bps, 均值: {temp_peak/2:.1f} bps")
print(f"永久冲击: {perm_bps:.1f} bps")
print(f"执行成本(IS,路径相对): {IS_bps:.1f} bps")
print(f"反事实总冲击(临时+永久): {total_impact:.1f} bps")python
在我们的设定下得到:临时冲击峰值约 29.6bps、均值约 14.8bps;永久冲击约 15bps;反事实分解出的总冲击约 30bps;而相对反事实路径的执行成本(IS)约 14.9bps。图里红色点是实际成交价——执行期间被推离无交易路径,交易结束后价格永久抬升了一个台阶,正是”临时回归 + 永久留下”的直观写照。
3.2 跨订单规模扫描:冲击呈平方根律#
把订单规模从 0.1% ADV 扫到 10% ADV,对每个规模都用反事实分解出总冲击,看它如何随规模缩放:
fracs = np.logspace(-3, -1, 25) # Q/ADV: 0.1% ~ 10%
est = []
for fr in fracs:
Qq = int(fr * ADV)
p, em_, _, tp, pm = apply_impact(Qq, seed=100 + int(fr * 1000))
is_b = np.mean(p[em_] - p0[em_]) / np.mean(p0[em_]) * 1e4
est.append(is_b + pm / p0[-1] * 1e4) # 临时 + 永久 = 总冲击
est = np.array(est)
slope, _ = np.polyfit(np.sqrt(fracs), est, 1)
print(f"平方根律拟合: impact ≈ {slope:.0f} · √(Q/ADV) bps")python
结果符合经典square-root law:总冲击 ≈ 162·√(Q/ADV) bps。这意味着冲击随参与率的平方根上升——小单边际冲击低,大单的边际冲击陡峭上升,这也是为什么大单必须切分、必须用 POV/ TWAP 把参与率压住。
3.3 用市场 beta 估计反事实(更贴近实盘)#
模拟里我们直接拿真实无交易路径 p0 做基线。实盘里 p0 不可观测,要用外生的市场指数估计:先用交易前窗口回归出 β,再外推无交易路径。
# 用交易前 60 分钟估计个股对市场的 beta
pre = slice(0, 60)
beta_hat = np.cov(p0[pre], market[pre])[0, 1] / np.var(market[pre])
cf_est = arrival * np.exp(beta_hat * (market - market[em[0]])) # 外生反事实
# 实际 − 反事实 = 交易自身造成的冲击(含临时 + 永久)
est_impact = np.mean(path[em] - cf_est[em]) / arrival * 1e4python要点:市场指数不被单只股票的交易影响,因此 cf_est 是干净的反事实;但 β 本身会时变,必须用滚动短窗口估计,且选错基准(如用高相关的另一只个股当基线)会让反事实也被你的交易带偏。经验上,指数 / 板块 ETF 比单只同业股更适合做基线。
3.4 对算法选择的启示#
临时冲击由参与率驱动(平方根律),所以压低单分钟参与率是降执行成本的主抓手:POV / TWAP 把大单拆细、用被动算法避免主动吃掉对手盘。永久冲击由净方向单量驱动(∝ Q/ADV),拆单帮不了它——能降永久冲击的只有少做方向性净单、或用衍生品 / 篮子对冲暴露。一句话:临时冲击靠「拆」,永久冲击靠「少」。这也正是 TCA 报告必须把两者拆开、分别发给执行团队与组合团队的原因。
四、工程落地:把反事实做成 TCA 系统#
- 建外生基线:用市场指数 / 同板块等权指数做反事实基准,单笔交易无法撼动它,满足外生性。
- 拆临时/永久:临时冲击喂给执行团队(评价算法、券商);永久冲击喂给组合团队(归因到决策本身的隐性成本)。
- 聚合而非单笔:单笔交易的反事实估计噪声极大,必须跨多笔、同条件聚合才能得到稳定的冲击系数。
- 滚动校准:冲击系数(尤其永久部分)会随时间衰减,用滚动窗口重估,别拿三年前的参数当真理。
五、真实陷阱(按危害排序)#
- 内生性 / 选择偏差(最致命):你交易是因为你预期要动,市场也可能本来就要动。不用外生基线,IS 会把 beta 算成你的冲击,高估成本、误判算法。
- 临时/永久混淆:用错口径,执行质量评估和组合归因会同时失真。
- 反事实模型设定错误:β 不稳、选了错误基准(如用高相关的另一只股票当基线,它也被你的交易带偏),反事实本身就偏。
- 到达价 vs 决策价口径不一:决策时、下单时、成交时的”参考价”不同,IS 口径必须全公司统一。
- 冲击系数的 alpha 衰减:历史估计的冲击参数会过时,尤其流动性regime切换时。
- 单笔不可识别:单笔交易的反事实误差远大于信号,必须靠统计聚合。
结语#
市场冲击成本的本质,是”如果没有我,价格会怎样”这个反事实问题。朴素 IS 把市场的波动也算进你的账单,而正确的做法是用外生的市场/板块因子构建无交易基线,把临时冲击(执行成本)和永久冲击(组合隐性成本)分开度量。平方根律告诉我们大单的边际冲击陡峭上升——这既是拆单算法的理论依据,也是为什么冲击成本控制,是 alpha 死生死问题的那道隐形闸门。对一个日均成交 100 万股、准备吃下 5% ADV 的单子,本文模拟给出约 30bps 的反事实总冲击;这意味着冲击成本一年复利下来足以吃掉一个 marginal 策略的全部利润,所以大单的冲击建模不是锦上添花,而是 survival 问题。