跳到主要内容

绩效归因:把收益拆成「为什么赚」

直觉

一个组合跑赢基准 5%,到底是赌对了板块(科技超配),还是选对了个股(板块内挑得准)?绩效归因就是把超额收益拆成可解释的来源。最常用两种:Brinson 归因按板块把超额拆成「配置效应 + 选股效应」;因子归因把收益回归到风险因子(如 Fama-French),剥离出「扣除因子暴露后的真实 Alpha」。

Brinson-Fachler 分解

设板块 ss 在基准/组合里的权重 Bs,WsB_s, W_s,板块收益 RsB,RsPR_s^B, R_s^P,基准总收益 RBR^B

(WsBs)(RsBRB)配置效应+Ws(RsPRsB)选股效应+(WsBs)(RsPRsB)交互效应\underbrace{(W_s-B_s)(R_s^B-R^B)}_{\text{配置效应}}+\underbrace{W_s(R_s^P-R_s^B)}_{\text{选股效应}}+\underbrace{(W_s-B_s)(R_s^P-R_s^B)}_{\text{交互效应}}
  • 配置效应:超配/低配了涨得比平均好/差的板块吗?
  • 选股效应:在同一板块里,组合选的股票比基准好吗?
「人话」解释:归因到底在问什么?

它把「我赚了多少」翻译成「凭什么赚的」。配置效应问「我是不是把筹码押在了对的赛道」,选股效应问「在同一赛道里,我挑的选手是不是比平均强」。两者一拆,你才知道自己是真有选股能力,还是只是碰巧坐上了某个板块的顺风车——后者换个周期可能就没了。因子归因更进一步:把收益里能被「市场、价值、动量」等系统性因子解释的部分扣除,剩下的才是「纯 Alpha」。

可运行案例①:Brinson 板块归因

基准 = 50 只股票等权;组合 = 按 quality 倾斜(高质量股权重更高)。把平均月度超额收益拆成「配置 + 选股」。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

prices = pd.read_csv('/data/sp500_universe_daily.csv', parse_dates=['date']).set_index('date')
meta = pd.read_csv('/data/sp500_metadata.csv').set_index('ticker')
tickers = list(prices.columns); sec = meta.loc[tickers, 'sector']; quality = meta.loc[tickers, 'quality']
monthly = prices.resample('ME').last(); mret = monthly.pct_change().dropna(how='all')

bw = pd.Series(1/len(tickers), index=tickers)               # 基准等权
qpos = quality - quality.min() + 0.5; pw = qpos / qpos.sum() # 组合: 质量倾斜

alloc = pd.Series(0.0, index=sec.unique()); sel = pd.Series(0.0, index=sec.unique())
Rb = Rp = 0.0; n = 0
for t in mret.index:
  rt = mret.loc[t]; bench_total = (bw*rt).sum(); port_total = (pw*rt).sum()
  Rb += bench_total; Rp += port_total; n += 1
  for s in alloc.index:
      m = sec == s; Bs = bw[m].sum(); Ws = pw[m].sum()
      Rsb = rt[m].mean(); Rsp = (pw[m]*rt[m]).sum() / Ws
      alloc[s] += (Ws - Bs)*(Rsb - bench_total)            # 配置效应
      sel[s]   += Ws*(Rsp - Rsb)                           # 选股效应
alloc /= n; sel /= n; active = Rp/n - Rb/n
print(f"平均月度主动收益 = {active:+.4%}")
print(f"分解合计(配置+选择) = {(alloc.sum()+sel.sum()):+.4%}  ← 应与主动收益吻合")
print(pd.DataFrame({'配置': alloc, '选股': sel}).sort_values('配置').round(4))

df = pd.DataFrame({'配置': alloc, '选股': sel}).sort_values('配置')
plt.figure(figsize=(9, 3.6))
plt.bar(range(len(df)), df['配置'], label='配置效应', color='steelblue')
plt.bar(range(len(df)), df['选股'], bottom=df['配置'], label='选股效应', color='indianred')
plt.xticks(range(len(df)), df.index, rotation=30, fontsize=8)
plt.axhline(0, color='k', lw=0.6); plt.title('Brinson 板块归因(月均贡献)'); plt.legend(fontsize=9)
plt.tight_layout(); plt.show()

可运行案例②:Fama-French 因子归因

把组合月收益回归到五因子,剥离因子暴露,剩下的截距就是「扣除系统性风险后的 Alpha」。

import numpy as np
import pandas as pd

prices = pd.read_csv('/data/sp500_universe_daily.csv', parse_dates=['date']).set_index('date')
meta = pd.read_csv('/data/sp500_metadata.csv').set_index('ticker').loc[prices.columns]
ff = pd.read_csv('/data/ff_factors_monthly.csv', parse_dates=['date']).set_index('date')
monthly = prices.resample('ME').last(); mret = monthly.pct_change()
qpos = meta['quality'] - meta['quality'].min() + 0.5
port = mret.dot(qpos / qpos.sum()).rename('p')              # 质量倾斜组合月收益

d = port.to_frame().join(ff, how='inner').dropna()
y = d['p'].values - d['RF'].values
X = np.column_stack([np.ones(len(d)), d[['MKT','SMB','HML','RMW','CMA']].values])
coef, *_ = np.linalg.lstsq(X, y, rcond=None)
resid = y - X @ coef
r2 = 1 - (resid**2).sum() / ((y - y.mean())**2).sum()
print(f"Alpha(月)={coef[0]*100:+.3f}%   年化={coef[0]*12*100:+.2f}%")
print("Beta:", {k: round(float(v), 2) for k, v in zip(['MKT','SMB','HML','RMW','CMA'], coef[1:])})
print(f"R^2={r2:.2f}  ← 越接近1, 收益越能被五因子解释, 纯Alpha越少")

小结

  • Brinson 归因:把超额收益拆成配置效应(押对板块)+ 选股效应(板块内挑准);
  • 因子归因:回归到 FF 等因子,剥离系统性风险暴露,剩下的截距是纯 Alpha
  • 归因回答的不是「赚了多少」,而是「凭什么赚」——这是区分能力与运气的基础。