第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 本章小结

这一章,我们走完了冲击成本归因的完整流程:从数据准备、特征工程,到因子贡献度计算,再到结果可视化。核心就三件事:

  1. 构造三个核心因子:订单规模、流动性、波动率
  2. 用稳健回归计算因子贡献度
  3. 可视化结果,辅助决策

这套方法,我在多个策略上都验证过。只要数据质量过关,解释力基本在70%以上。剩下的30%,可能是市场微观结构、订单簿动态这些更精细的东西。后面章节我们会逐步深入。

好了,代码拿去跑一跑。有问题随时交流。


交易系统化学习资料 微信Strategy888888