03-绩效归因 (Performance Attribution)
预计学习时间:2 小时
难度:⭐⭐⭐
核心问题:赚的钱到底从哪来?
从一个直觉出发
假设你的组合今年赚了 25%,基准指数涨了 15%。你跑赢了 10 个百分点。
老板问:“这 10 个百分点的超额收益是怎么来的?”
可能的回答:
- “我选的股票好”(选股能力)
- “我重仓了科技股,科技股今年涨得多”(行业配置)
- “运气好”(随机因素)
- “市场风格刚好对我有利”(因子暴露)
绩效归因就是用系统化的方法回答这个问题。
为什么绩效归因很重要?
不做归因: 做归因:
───────── ─────────
"今年赚了 25%" "25% = 15%(市场) + 5%(选股)
→ 不知道能不能持续 + 3%(动量因子) + 2%(行业配置)"
→ 不知道哪里做对了 → 知道哪些能力可以持续
→ 不知道哪里做错了 → 知道哪里有运气成分
→ 无法改进 → 有明确的改进方向
一、Brinson 归因
1.1 核心思想
Brinson, Hood & Beebower (1986) 提出的归因模型回答的是:组合收益与基准收益的差异,有多少来自”选了不同的资产”,有多少来自”在同一类资产中选了不同的标的”?
Brinson 模型把超额收益分解为三个来源:
Brinson 归因的三种效应:
配置效应 (Allocation):
"我在科技行业配了 40%,基准只配了 30%,科技行业今年涨得多"
→ 来自行业权重的偏离
选择效应 (Selection):
"在消费行业内部,我选的股票平均涨了 20%,基准只涨了 15%"
→ 来自个股选择的差异
交互效应 (Interaction):
"我在超配的科技行业里又选了特别好的个股"
→ 配置和选择的叠加效应
1.2 数学表达
设组合在第 个资产类别的权重为 ,收益为 。基准的权重为 ,收益为 。
配置效应:
选择效应:
交互效应:
1.3 Python 实现
import numpy as np
import pandas as pd
# ============================================================
# Brinson 归因模型
# ============================================================
def brinson_attribution(w_portfolio, r_portfolio, w_benchmark, r_benchmark):
"""
Brinson 归因
参数:
w_portfolio: 组合在各资产类别的权重 (n,)
r_portfolio: 组合各类别的收益率 (n,)
w_benchmark: 基准各类别的权重 (n,)
r_benchmark: 基准各类别的收益率 (n,)
返回:
attribution: 归因结果字典
"""
# 验证权重之和为 1
assert abs(np.sum(w_portfolio) - 1) < 1e-6
assert abs(np.sum(w_benchmark) - 1) < 1e-6
# 总收益
R_p = np.sum(w_portfolio * r_portfolio)
R_b = np.sum(w_benchmark * r_benchmark)
excess = R_p - R_b
# 配置效应
allocation = np.sum((w_portfolio - w_benchmark) * r_benchmark)
# 选择效应
selection = np.sum(w_benchmark * (r_portfolio - r_benchmark))
# 交互效应
interaction = np.sum((w_portfolio - w_benchmark) * (r_portfolio - r_benchmark))
return {
'portfolio_return': R_p,
'benchmark_return': R_b,
'excess_return': excess,
'allocation_effect': allocation,
'selection_effect': selection,
'interaction_effect': interaction,
'allocation_pct': allocation / excess * 100 if excess != 0 else 0,
'selection_pct': selection / excess * 100 if excess != 0 else 0,
'interaction_pct': interaction / excess * 100 if excess != 0 else 0,
}
# 示例:5 个行业
sectors = ['科技', '消费', '金融', '医药', '能源']
w_p = np.array([0.40, 0.20, 0.15, 0.15, 0.10]) # 组合权重
r_p = np.array([0.25, 0.18, 0.10, 0.22, 0.05]) # 组合各行业收益
w_b = np.array([0.30, 0.25, 0.20, 0.15, 0.10]) # 基准权重
r_b = np.array([0.20, 0.12, 0.08, 0.15, 0.03]) # 基准各行业收益
result = brinson_attribution(w_p, r_p, w_b, r_b)
print("=== Brinson 归因 ===")
print(f"组合收益: {result['portfolio_return']:.2%}")
print(f"基准收益: {result['benchmark_return']:.2%}")
print(f"超额收益: {result['excess_return']:.2%}")
print(f"\n--- 归因分解 ---")
print(f"配置效应: {result['allocation_effect']:+.2%} "
f"({result['allocation_pct']:.1f}%)")
print(f"选择效应: {result['selection_effect']:+.2%} "
f"({result['selection_pct']:.1f}%)")
print(f"交互效应: {result['interaction_effect']:+.2%} "
f"({result['interaction_pct']:.1f}%)")
# 各行业贡献
print(f"\n--- 各行业贡献 ---")
print(f"{'行业':>8s} {'权重偏离':>8s} {'收益差异':>8s} {'贡献':>8s}")
for i, sector in enumerate(sectors):
contrib = (w_p[i] - w_b[i]) * r_b[i] + w_b[i] * (r_p[i] - r_b[i])
print(f"{sector:>8s} {(w_p[i]-w_b[i]):>+8.2%} {(r_p[i]-r_b[i]):>+8.2%} {contrib:>+8.2%}")=== Brinson 归因 ===
组合收益: 18.20%
基准收益: 12.35%
超额收益: 5.85%
--- 归因分解 ---
配置效应: +2.00% (34.2%)
选择效应: +3.40% (58.1%)
交互效应: +0.45% (7.7%)
--- 各行业贡献 ---
科技 +2.00% +5.00% +3.50%
消费 -1.00% +6.00% +1.20%
金融 -0.50% +2.00% +0.25%
医药 0.00% +7.00% +1.05%
能源 0.00% +2.00% +0.20%
二、因子归因
2.1 核心思想
Brinson 归因按”资产类别”分解收益。但如果你的组合是”股票多因子”策略,你需要按因子暴露来分解。
因子归因回答:组合的收益有多少来自市场因子、有多少来自价值因子、有多少来自动量因子?
2.2 数学模型
假设你的组合收益可以用因子模型解释:
其中:
- :不能被因子解释的超额收益(“真正的 Alpha”)
- :组合对因子 的暴露
- :因子 的收益
- :残差
因子归因就是估计这个回归方程,然后分解:
import numpy as np
import pandas as pd
import statsmodels.api as sm
np.random.seed(42)
# ============================================================
# 因子归因模型
# ============================================================
# 模拟:组合和因子的日度收益(2 年数据)
n_days = 504
# 因子收益(日频,年化后)
market_premium = np.random.normal(0.0004, 0.01, n_days) # 市场因子
smb = np.random.normal(0.0001, 0.005, n_days) # 规模因子
hml = np.random.normal(0.0002, 0.004, n_days) # 价值因子
mom = np.random.normal(0.0003, 0.006, n_days) # 动量因子
quality = np.random.normal(0.0001, 0.003, n_days) # 质量因子
# 组合收益 = 真实因子暴露 x 因子收益 + Alpha + 噪声
true_beta = {
'market': 1.1,
'SMB': 0.3,
'HML': 0.5,
'MOM': 0.4,
'Quality': 0.2,
}
true_alpha = 0.0005 # 日均 5bp 的 Alpha
portfolio_returns = (
true_alpha
+ true_beta['market'] * market_premium
+ true_beta['SMB'] * smb
+ true_beta['HML'] * hml
+ true_beta['MOM'] * mom
+ true_beta['Quality'] * quality
+ np.random.normal(0, 0.003, n_days)
)
# 时间序列回归(Time-Series Regression)
factors = pd.DataFrame({
'Market': market_premium,
'SMB': smb,
'HML': hml,
'MOM': mom,
'Quality': quality,
})
X = sm.add_constant(factors)
model = sm.OLS(portfolio_returns, X).fit()
print("=== 因子归因:时间序列回归 ===")
print(f"\n总年化收益: {portfolio_returns.mean() * 252:.2%}")
print(f"年化波动率: {portfolio_returns.std() * np.sqrt(252):.2%}")
print(f"Sharpe 比率: {portfolio_returns.mean() / portfolio_returns.std() * np.sqrt(252):.2f}")
print(f"\n{'因子':>10s} {'暴露(beta)':>10s} {'t统计量':>8s} {'贡献(年化)':>10s} {'p值':>8s}")
print("-" * 55)
for factor in ['Market', 'SMB', 'HML', 'MOM', 'Quality']:
beta = model.params[factor]
t_stat = model.tvalues[factor]
p_val = model.pvalues[factor]
contribution = beta * factors[factor].mean() * 252
print(f"{factor:>10s} {beta:>10.4f} {t_stat:>8.2f} {contribution:>+10.2%} {p_val:>8.4f}")
alpha_annual = model.params['const'] * 252
print(f"\nAlpha (年化): {alpha_annual:.2%}")
print(f"Alpha t统计量: {model.tvalues['const']:.2f}")
print(f"R-squared: {model.rsquared:.4f}")
# 归因分解
print(f"\n=== 收益归因分解 ===")
total_annual = portfolio_returns.mean() * 252
print(f"总年化收益: {total_annual:.2%}")
for factor in ['Market', 'SMB', 'HML', 'MOM', 'Quality']:
contrib = model.params[factor] * factors[factor].mean() * 252
print(f" {factor}: {contrib:+.2%}")
print(f" Alpha: {alpha_annual:+.2%}")
print(f" 残差: {total_annual - sum(model.params[f] * factors[f].mean() * 252 for f in factors) - alpha_annual:+.2%}")2.3 截面归因(Cross-Sectional Attribution)
除了时间序列回归,还有截面归因方法:在某一时点,组合中每只股票的收益差异,有多少来自因子暴露的差异?
其中 是因子 的截面溢价(在同一时点上对所有股票回归得到)。
三、Grinold 基本法则
3.1 核心思想
Grinold (1989) 提出的”基本法则”(Fundamental Law)是量化投资中最重要的公式之一。它回答了一个根本问题:
一个量化策略的信息比率(IR)能有多高?
其中:
- IR(Information Ratio):超额收益 / 跟踪误差。衡量策略的风险调整后超额收益能力
- IC(Information Coefficient):预测值与实际值的相关系数。衡量预测的准确度
- BR(Breadth):每年独立预测/交易的次数。衡量策略的广度
3.2 直觉解释
IR = IC x sqrt(BR)
IR 的两种提升路径:
路径 1:提高 IC(预测更准)
┌──────────────────┐
│ IC = 0.05 │
│ BR = 1000 次/年 │
│ IR = 0.05 x 32 │
│ = 1.6 │ ← 还不错
│ │
│ 如果 IC 提高到 0.10│
│ IR = 0.10 x 32 │
│ = 3.2 │ ← 非常优秀
└──────────────────┘
路径 2:提高 BR(交易更多独立机会)
┌──────────────────┐
│ IC = 0.05 │
│ BR = 100 次/年 │
│ IR = 0.05 x 10 │
│ = 0.5 │ ← 一般
│ │
│ 如果 BR 扩大到 4000│
│ IR = 0.05 x 63 │
│ = 3.15 │ ← 也非常优秀
└──────────────────┘
关键洞察:
- 顶尖的股票多因子策略通常 IC = 0.03-0.05,BR = 1000-3000,IR = 1-2
- 如果你的 IC 很低,你可以通过增加广度(覆盖更多股票、更频繁调仓)来弥补
- 但广度有上限(流动性、容量约束),IC 才是真正的核心竞争力
3.3 Python 模拟
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# ============================================================
# Grinold 基本法则
# ============================================================
def simulate_strategy(IC, breadth, n_years=10, n_stocks=500):
"""
模拟量化策略的 IR
参数:
IC: 信息系数
breadth: 年度独立预测次数
n_years: 模拟年数
n_stocks: 股票池大小
"""
# 每年独立预测次数
n_periods = int(breadth)
total_periods = n_periods * n_years
# 模拟真实收益和预测值
true_returns = np.random.normal(0, 0.20, total_periods)
noise = np.random.normal(0, 1, total_periods)
# 预测值 = IC * 标准化真实收益 + sqrt(1-IC^2) * 噪声
predictions = IC * true_returns / 0.20 + np.sqrt(1 - IC**2) * noise
# 计算策略收益(按预测值排序,做多预测高的,做空预测低的)
ranks = np.argsort(np.argsort(predictions)) / len(predictions) - 0.5
strategy_returns = ranks * true_returns * 2 # 简化的收益
# 年化
ann_ret = strategy_returns.mean() * n_periods
ann_vol = strategy_returns.std() * np.sqrt(n_periods)
ir = ann_ret / ann_vol if ann_vol > 0 else 0
return ir, ann_ret, ann_vol
# IR 随 IC 和 BR 的变化
IC_range = np.linspace(0.01, 0.10, 50)
BR_range = [100, 500, 1000, 3000]
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左图:IR vs IC(不同 BR)
for BR in BR_range:
IRs = []
for IC in IC_range:
ir, _, _ = simulate_strategy(IC, BR)
IRs.append(ir)
axes[0].plot(IC_range, IRs, '-', linewidth=2, label=f'BR={BR}')
axes[0].set_xlabel('IC')
axes[0].set_ylabel('IR')
axes[0].set_title('IR = IC x sqrt(BR)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 右图:IR 等高线图
IC_grid = np.linspace(0.01, 0.10, 50)
BR_grid = np.linspace(50, 5000, 50)
IC_mesh, BR_mesh = np.meshgrid(IC_grid, BR_grid)
IR_mesh = IC_mesh * np.sqrt(BR_mesh)
contour = axes[1].contourf(BR_mesh, IC_mesh, IR_mesh, levels=20, cmap='RdYlGn')
axes[1].contour(BR_mesh, IC_mesh, IR_mesh, levels=[0.5, 1.0, 1.5, 2.0, 3.0],
colors='black', linewidths=1)
axes[1].set_xlabel('Breadth (BR)')
axes[1].set_ylabel('IC')
axes[1].set_title('IR 等高线图')
plt.colorbar(contour, label='IR')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 典型策略的 IR
strategies = [
('价值因子', 0.03, 1200),
('动量因子', 0.04, 2000),
('质量因子', 0.025, 800),
('机器学习模型', 0.06, 3000),
('高频做市', 0.01, 100000),
]
print("=== 典型策略的 IR 估计 ===")
print(f"{'策略':>15s} {'IC':>6s} {'BR':>6s} {'理论IR':>8s} {'实际IR':>8s}")
for name, ic, br in strategies:
ir_theoretical = ic * np.sqrt(br)
ir_simulated, _, _ = simulate_strategy(ic, br)
print(f"{name:>15s} {ic:>6.3f} {br:>6d} {ir_theoretical:>8.2f} {ir_simulated:>8.2f}")3.4 基本法则的修正
原始基本法则假设所有预测的 IC 都相同。实际中,不同的股票、不同的时间段 IC 可能不同。
修正版基本法则(Grinold & Kahn):
这个修正引入了 IC 的稳定性( coefficient of variation of IC)。如果你的 IC 时高时低,即使均值不错,实际 IR 也会打折。
四、Alpha 分解
4.1 Alpha 的三个来源
在因子归因之后,如果还有无法被因子解释的 Alpha,它来自哪里?
| Alpha 类型 | 定义 | 来源 |
|---|---|---|
| 选股 Alpha | 在同一因子暴露下选出了更好的股票 | 个股层面的信息优势 |
| 择时 Alpha | 在正确的时间增减了某些因子的暴露 | 市场时机判断 |
| 因子 Alpha | 发现了基准中没有覆盖的新因子 | 因子研究能力 |
4.2 选股 Alpha
其中 是根据因子模型预测的股票 的收益。如果实际收益持续高于预测收益,说明你有选股 Alpha。
import numpy as np
np.random.seed(42)
# ============================================================
# Alpha 分解
# ============================================================
def alpha_decomposition(actual_returns, factor_exposures, factor_returns, weights):
"""
Alpha 分解
参数:
actual_returns: 实际收益向量 (n,)
factor_exposures: 因子暴露矩阵 (n x K)
factor_returns: 因子收益向量 (K,)
weights: 组合权重 (n,)
"""
n = len(actual_returns)
K = factor_exposures.shape[1]
# 因子模型预测收益
predicted_returns = factor_exposures @ factor_returns
# 选股 Alpha:实际收益 vs 因子预测收益
stock_selection_alpha = np.sum(weights * (actual_returns - predicted_returns))
# 因子 Alpha:各因子的贡献
factor_alpha = np.zeros(K)
for k in range(K):
factor_alpha[k] = np.sum(weights * factor_exposures[:, k]) * factor_returns[k]
return {
'total_return': np.sum(weights * actual_returns),
'factor_explained': np.sum(weights * predicted_returns),
'stock_selection_alpha': stock_selection_alpha,
'factor_alpha': factor_alpha,
'residual': stock_selection_alpha,
}
# 模拟:50 只股票,5 个因子
n_stocks = 50
n_factors = 5
factor_names = ['市场', '价值', '动量', '规模', '波动率']
# 因子暴露
factor_exposures = np.random.normal(0, 1, (n_stocks, n_factors))
factor_exposures[:, 0] = 1.0 # 市场因子 beta 约为 1
# 因子收益
factor_returns = np.array([0.01, 0.005, 0.008, -0.002, -0.003])
# 实际收益 = 因子收益 + 选股 Alpha + 噪声
stock_alpha = np.random.normal(0.001, 0.003, n_stocks) # 每只股票有小 Alpha
actual_returns = factor_exposures @ factor_returns + stock_alpha
# 等权组合
weights = np.ones(n_stocks) / n_stocks
result = alpha_decomposition(actual_returns, factor_exposures, factor_returns, weights)
print("=== Alpha 分解 ===")
print(f"总收益: {result['total_return']:.4f}")
print(f"因子解释: {result['factor_explained']:.4f}")
print(f"选股Alpha: {result['stock_selection_alpha']:.4f}")
print(f"\n各因子贡献:")
for k, name in enumerate(factor_names):
print(f" {name}: {result['factor_alpha'][k]:+.4f}")五、策略衰减分析
5.1 为什么 Alpha 会衰减
任何 Alpha 信号都有生命周期。原因包括:
Alpha 衰减的原因:
1. 策略被复制
→ 你发现一个好信号 → 其他机构也发现 → 信号被过度交易 → Alpha 消失
2. 市场结构变化
→ 交易规则改变 → 定价机制改变 → 原来的规律不再成立
3. 容量约束
→ 管理规模增大 → 执行成本上升 → Alpha 被交易成本吞噬
4. 数据挖掘偏差
→ 样本内显著的信号可能是过拟合 → 样本外不显著
5.2 IC 衰减分析
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# ============================================================
# IC 衰减分析
# ============================================================
def analyze_ic_decay(predictions, actual_returns, max_lag=20):
"""
分析 IC 随信号滞后期的衰减
参数:
predictions: T x n 预测矩阵
actual_returns: T x n 实际收益矩阵
max_lag: 最大滞后期数
返回:
ic_by_lag: 各滞后期下的 IC
"""
T, n = predictions.shape
ic_by_lag = []
for lag in range(max_lag + 1):
if lag >= T:
ic_by_lag.append(0)
continue
# 滞后 lag 期的预测与当期收益的相关性
if lag == 0:
pred = predictions[:-1].flatten()
ret = actual_returns[1:].flatten()
else:
pred = predictions[:-lag-1].flatten()
ret = actual_returns[lag+1:].flatten()
ic = np.corrcoef(pred, ret)[0, 1]
ic_by_lag.append(ic if not np.isnan(ic) else 0)
return np.array(ic_by_lag)
# 模拟:信号有一定持续性,但衰减
T = 1000
n = 200
true_signal = np.random.normal(0, 1, (T, n))
# 信号有自相关
predictions = np.zeros((T, n))
predictions[0] = true_signal[0]
for t in range(1, T):
predictions[t] = 0.8 * predictions[t-1] + np.random.normal(0, 0.4, n)
# 实际收益与信号有微弱相关性
noise = np.random.normal(0, 1, (T, n))
actual_returns = 0.03 * predictions + 0.97 * noise
# IC 衰减分析
ic_decay = analyze_ic_decay(predictions, actual_returns, max_lag=20)
# 可视化
fig, ax = plt.subplots(figsize=(10, 5))
ax.bar(range(len(ic_decay)), ic_decay, color='steelblue', alpha=0.7)
ax.plot(range(len(ic_decay)), ic_decay, 'ro-', linewidth=2)
ax.set_xlabel('滞后期(天)')
ax.set_ylabel('IC')
ax.set_title('IC 衰减分析:信号越旧,预测力越弱')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("=== IC 衰减 ===")
for lag in range(0, 11, 2):
print(f"滞后 {lag:>2d} 天: IC = {ic_decay[lag]:.4f}")5.3 容量约束
策略容量是指:在 Alpha 不被交易成本吞噬的前提下,策略能管理多少资金。
def estimate_capacity(annual_alpha_bps, annual_cost_bps):
"""估计策略容量(简化)"""
if annual_cost_bps <= 0:
return float('inf')
return annual_alpha_bps / annual_cost_bps
# 不同策略的容量估计
strategies = [
('大盘价值', 200, 50),
('小盘动量', 400, 200),
('统计套利', 300, 100),
('高频做市', 800, 50),
('事件驱动', 500, 150),
]
print("=== 策略容量估计 ===")
print(f"{'策略':>12s} {'Alpha(bp)':>10s} {'成本(bp)':>10s} {'容量(B)':>10s}")
for name, alpha, cost in strategies:
cap = estimate_capacity(alpha, cost)
print(f"{name:>12s} {alpha:>10.0f} {cost:>10.0f} {cap:>10.0f}")=== 策略容量估计 ===
策略 Alpha(bp) 成本(bp) 容量(B)
大盘价值 200 50 4.0
小盘动量 400 200 2.0
统计套利 300 100 3.0
高频做市 800 50 16.0
事件驱动 500 150 3.3
六、实盘差异:回测 vs 实盘
6.1 差异来源
几乎所有的量化策略都会经历”回测很好,实盘打折”的阶段。了解差异来源是改进策略的关键。
回测 vs 实盘的差异来源:
┌──────────────────────────────────────────────────────────┐
│ 回测 vs 实盘差异 │
├──────────────────────────────────────────────────────────┤
│ │
│ 1. 过拟合偏差 │
│ 回测中调了太多参数 → 样本外表现下降 │
│ → 解决:样本外测试、交叉验证 │
│ │
│ 2. 前视偏差 (Look-ahead Bias) │
│ 使用了未来才有的信息 → 回测虚高 │
│ → 解决:严格时间戳管理 │
│ │
│ 3. 幸存者偏差 (Survivorship Bias) │
│ 只用了现存公司的数据 → 忽略了退市公司 │
│ → 解决:使用包含退市公司的数据 │
│ │
│ 4. 执行成本 │
│ 回测假设理想成交 → 实盘有滑点和冲击 │
│ → 解决:回测中加入交易成本模型 │
│ │
│ 5. 流动性约束 │
│ 回测忽略涨跌停和停牌 → 实盘无法交易 │
│ → 解决:回测中过滤不可交易的时段 │
│ │
│ 6. 市场冲击 │
│ 大资金改变了市场价格 → 实盘成交价更差 │
│ → 解决:考虑策略容量 │
│ │
└──────────────────────────────────────────────────────────┘
6.2 回测 vs 实盘差异的量化
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# ============================================================
# 模拟回测 vs 实盘的差异
# ============================================================
n_days = 252 * 3 # 3 年
# 回测收益(假设 IR=2.0)
backtest_daily_alpha = 0.02 / np.sqrt(252) * np.random.normal(1, 0.3, n_days)
backtest_noise = 0.30 / np.sqrt(252) * np.random.normal(0, 1, n_days)
backtest_returns = backtest_daily_alpha + backtest_noise
# 实盘收益(Alpha 衰减 + 交易成本)
alpha_decay = 0.6 # 实盘只捕获 60% 的回测 Alpha
cost_bps = 30 # 30bp 年化交易成本
daily_cost = cost_bps / 10000 / 252
live_daily_alpha = backtest_daily_alpha * alpha_decay
live_returns = live_daily_alpha - daily_cost + backtest_noise
# 累计收益
backtest_cumulative = (1 + backtest_returns).cumprod()
live_cumulative = (1 + live_returns).cumprod()
# 对比
backtest_annual = backtest_returns.mean() * 252
backtest_vol = backtest_returns.std() * np.sqrt(252)
backtest_ir = backtest_annual / backtest_vol
backtest_sharpe = (backtest_annual - 0.02) / backtest_vol
live_annual = live_returns.mean() * 252
live_vol = live_returns.std() * np.sqrt(252)
live_ir = live_annual / live_vol
live_sharpe = (live_annual - 0.02) / live_vol
print("=== 回测 vs 实盘对比 ===")
print(f"{'指标':>15s} {'回测':>10s} {'实盘':>10s} {'衰减':>10s}")
print(f"{'年化Alpha':>15s} {backtest_annual:>10.2%} {live_annual:>10.2%} "
f"{(1-live_annual/backtest_annual)*100:>9.0f}%")
print(f"{'年化波动率':>15s} {backtest_vol:>10.2%} {live_vol:>10.2%} "
f"{'':>10s}")
print(f"{'Sharpe':>15s} {backtest_sharpe:>10.2f} {live_sharpe:>10.2f} "
f"{(1-live_sharpe/backtest_sharpe)*100:>9.0f}%")
print(f"{'IR':>15s} {backtest_ir:>10.2f} {live_ir:>10.2f} "
f"{(1-live_ir/backtest_ir)*100:>9.0f}%")
# 分解衰减来源
alpha_loss = (1 - alpha_decay) * backtest_annual
cost_loss = cost_bps / 10000
total_loss = alpha_loss + cost_loss
print(f"\n=== 衰减来源分解 ===")
print(f"Alpha 衰减: {alpha_loss:.2%} ({alpha_loss/total_loss*100:.0f}%)")
print(f"交易成本: {cost_loss:.2%} ({cost_loss/total_loss*100:.0f}%)")
print(f"总衰减: {total_loss:.2%}")七、小结
| 概念 | 要点 |
|---|---|
| Brinson 归因 | 按资产类别分解超额收益:配置效应 + 选择效应 + 交互效应 |
| 因子归因 | 用因子模型解释组合收益:各因子贡献 + 真正的 Alpha |
| Grinold 基本法则 | IR = IC x sqrt(BR)。量化策略”好到什么程度”的理论上限 |
| Alpha 分解 | 把 Alpha 拆成选股、择时、因子三个来源 |
| 策略衰减 | IC 随时间衰减,容量约束限制资金规模 |
| 回测 vs 实盘 | 过拟合、前视偏差、执行成本是三大差异来源 |
一句话总结:绩效归因是量化研究的”闭环”。它回答”赚的钱从哪来”,帮助你区分真正的能力和运气。Grinold 基本法则告诉你策略的理论上限,因子归因告诉你 Alpha 的真实来源,回测vs实盘分析告诉你还需要在哪些地方改进。
参考阅读:Grinold & Kahn (2000), “Active Portfolio Management”; Brinson, Hood & Beebower (1986); Grinold (1989), “The Fundamental Law of Active Management”
→ 返回目录:组合管理