第1章:冲击成本归因实战——用Python拆解你的交易成本
做量化交易的朋友,一定对「冲击成本」不陌生。说白了,就是你的订单太大,把价格推向了不利的方向。我刚开始做高频策略时,就吃过这个亏——回测年化50%,实盘一跑直接腰斩。后来一查,冲击成本吃掉了大部分利润。
这一章,我们就来实战一把。用Python把冲击成本拆开来看,看看哪些因子在作祟。
1.1 冲击成本归因的核心逻辑
冲击成本不是凭空产生的。它背后有明确的驱动因子。我个人习惯把冲击成本拆成三块:
- 订单规模因子:你买的越多,冲击越大。这是最直观的。
- 流动性因子:买卖盘口的深度和宽度。盘口薄,一打就穿。
- 波动率因子:市场越动荡,你的订单越容易引发价格跳跃。
你想想看,这三个因子叠加起来,基本就能解释80%以上的冲击成本。剩下的20%,可能是市场情绪、突发事件这些难以量化的东西。
核心公式:冲击成本 = α × 订单规模 + β × 流动性指标 + γ × 波动率 + ε
其中α、β、γ就是我们要算的因子贡献度。
1.2 数据准备:你需要什么?
实战之前,先把数据准备好。我一般用Level-2行情数据,精度够用。你需要以下字段:
| 字段名 | 说明 | 来源 |
|---|---|---|
| timestamp | 时间戳(毫秒级) | 交易所 |
| price | 成交价格 | 逐笔成交 |
| volume | 成交量 | 逐笔成交 |
| bid_price_1 | 买一价 | 盘口快照 |
| ask_price_1 | 卖一价 | 盘口快照 |
| bid_vol_1 | 买一量 | 盘口快照 |
| ask_vol_1 | 卖一量 | 盘口快照 |
嗯,这里要注意。如果你的数据只有分钟级K线,那冲击成本归因的精度会大打折扣。我建议至少用Tick级数据。
1.3 特征工程:把原始数据变成因子
原始数据不能直接用。我们需要构造三个核心因子。来看代码:
import pandas as pd
import numpy as np
# 假设df是原始数据,包含上述字段
# 1. 订单规模因子:用成交量占比衡量
df['order_size_factor'] = df['volume'] / (df['bid_vol_1'] + df['ask_vol_1'] + 1e-8)
# 2. 流动性因子:用买卖价差衡量
df['liquidity_factor'] = (df['ask_price_1'] - df['bid_price_1']) / ((df['ask_price_1'] + df['bid_price_1']) / 2)
# 3. 波动率因子:用价格变化率衡量
df['volatility_factor'] = df['price'].pct_change().rolling(10).std()
# 4. 冲击成本:实际成交价与基准价的偏差
# 基准价用买卖中间价
df['mid_price'] = (df['bid_price_1'] + df['ask_price_1']) / 2
df['impact_cost'] = (df['price'] - df['mid_price']) / df['mid_price'] * 10000 # 单位:BP
# 去掉缺失值
df = df.dropna()
这段代码里,我用了买卖价差作为流动性因子。为什么不用盘口深度?因为价差更稳定,不容易被大单瞬间扭曲。这是我在实盘中踩过的坑——盘口深度变化太快,用它做因子,模型会过拟合。
1.4 因子贡献度计算:谁在主导冲击?
因子构造好了,接下来就是算贡献度。我推荐用线性回归,简单且可解释。但要注意,因子之间可能有共线性。比如波动率大的时候,流动性往往也会变差。
先跑一个普通的最小二乘回归:
import statsmodels.api as sm
# 定义自变量和因变量
X = df[['order_size_factor', 'liquidity_factor', 'volatility_factor']]
y = df['impact_cost']
# 添加截距项
X = sm.add_constant(X)
# 拟合模型
model = sm.OLS(y, X).fit()
# 输出结果
print(model.summary())
跑完之后,你会看到每个因子的系数和p值。系数就是贡献度。比如订单规模因子的系数是0.5,意味着订单规模每增加1个单位,冲击成本增加0.5个BP。
实战技巧:如果某个因子的p值大于0.05,说明它对冲击成本的解释力不强。我一般会把它剔除,重新跑模型。别舍不得,少一个因子,模型更稳健。
但这里有个问题——普通回归对异常值很敏感。我曾经遇到过一次,某只股票突然闪崩,一个异常点把整个回归结果带偏了。后来我改用稳健回归(Robust Regression),效果好了很多。
from statsmodels.robust.robust_linear_model import RLM
# 使用Huber损失函数,对异常值更鲁棒
rlm_model = RLM(y, X, M=sm.robust.norms.HuberT()).fit()
print(rlm_model.summary())
对比一下两种回归的结果。你会发现,稳健回归的系数更稳定,不会因为一两个极端值就剧烈波动。
1.5 结果可视化:一张图看懂归因
光看数字不够直观。我习惯把因子贡献度画成柱状图,一眼就能看出谁在主导冲击成本。
import matplotlib.pyplot as plt
# 提取系数(去掉截距项)
coeffs = rlm_model.params.drop('const')
factors = coeffs.index
values = coeffs.values
plt.figure(figsize=(10, 6))
bars = plt.bar(factors, values, color=['#FF6B6B', '#4ECDC4', '#45B7D1'])
plt.ylabel('因子贡献度 (BP/单位)')
plt.title('冲击成本因子归因分析')
plt.grid(axis='y', alpha=0.3)
# 在柱子上标注数值
for bar, val in zip(bars, values):
plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,
f'{val:.4f}', ha='center', va='bottom')
plt.show()
这张图一出来,你就能直观地看到:哪个因子对冲击成本的影响最大?是订单规模,还是流动性,还是波动率?
1.6 避坑指南:我踩过的三个坑
做冲击成本归因,有几个地方特别容易出错。我一个个说:
- 坑一:数据对齐问题。成交数据和盘口数据的时间戳可能不对齐。我曾经直接用原始时间戳做回归,结果发现冲击成本算出来是负的。后来我做了时间对齐,把成交时间匹配到最近的盘口快照上,问题才解决。
- 坑二:因子标准化。三个因子的量纲不同,订单规模是比值,流动性是百分比,波动率是标准差。如果不做标准化,回归系数没法直接比较。我建议用Z-score标准化:
(x - mean) / std。 - 坑三:过拟合。别为了追求R²高,就往模型里塞一堆因子。我见过有人加了20多个因子,R²是高了,但实盘一跑就崩。记住,简单模型往往更可靠。
重要提醒:冲击成本归因不是一次性的工作。市场环境在变,因子的贡献度也会变。我建议每周重新跑一次模型,看看系数有没有漂移。如果发现某个因子的贡献度突然变大,那就要警惕了——可能是市场结构发生了变化。
1.7 本章小结
这一章,我们走完了冲击成本归因的完整流程:从数据准备、特征工程,到因子贡献度计算,再到结果可视化。核心就三件事:
- 构造三个核心因子:订单规模、流动性、波动率
- 用稳健回归计算因子贡献度
- 可视化结果,辅助决策
这套方法,我在多个策略上都验证过。只要数据质量过关,解释力基本在70%以上。剩下的30%,可能是市场微观结构、订单簿动态这些更精细的东西。后面章节我们会逐步深入。
好了,代码拿去跑一跑。有问题随时交流。