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”

返回目录:组合管理