3. 绩效归因分析:收益分解、风险因子暴露分析、Alpha衰减检测
策略跑了一段时间,净值曲线看着还行。但你真的知道它赚的是谁的钱吗?
我见过太多团队,策略回测漂亮,实盘一上就变形。为什么?因为没人做归因分析。说白了,你连策略的「钱从哪来」都没搞清楚,怎么敢上实盘?
这一章,我们就来拆解绩效归因的三个核心模块:收益分解、风险因子暴露、Alpha衰减检测。这三板斧砍下去,你的策略基本就「裸奔」了——好坏一目了然。
核心观点: 绩效归因不是事后诸葛亮,而是策略健康度的「体检报告」。不做归因的策略,就像闭着眼睛开车。
3.1 收益分解:把利润「拆开」看
收益分解,就是把策略的总收益拆成几个部分。我个人习惯先拆成三块:市场收益、风格收益、选股收益。
举个例子。你跑了一个沪深300的增强策略,年化收益15%。同期沪深300涨了8%。那多出来的7%是哪来的?
- 市场收益(Beta): 8%,大盘涨的,跟你没关系。
- 风格收益: 比如你超配了成长股,成长因子涨了3%,这部分归风格。
- 选股收益(Alpha): 剩下的4%,才是你真正的选股能力。
我在项目中遇到过一种情况:策略收益很高,但一拆开,发现全是风格收益。后来风格切换,策略直接崩了。所以,别把Beta当Alpha,这是新手最容易犯的错误。
实用技巧: 用 Brinson 归因模型做收益分解。它把超额收益拆成「资产配置收益」和「选股收益」两部分。代码实现很简单,用 pandas 分组聚合就能算。
Brinson 归因的 Python 实现
import pandas as pd
import numpy as np
def brinson_attribution(portfolio_weights, benchmark_weights,
portfolio_returns, benchmark_returns):
"""
Brinson 归因:拆解超额收益
"""
# 资产配置收益
allocation = (portfolio_weights - benchmark_weights) * benchmark_returns
# 选股收益
selection = benchmark_weights * (portfolio_returns - benchmark_returns)
# 交互收益
interaction = (portfolio_weights - benchmark_weights) * \
(portfolio_returns - benchmark_returns)
return {
'allocation': allocation.sum(),
'selection': selection.sum(),
'interaction': interaction.sum(),
'excess_return': (allocation + selection + interaction).sum()
}
# 使用示例
result = brinson_attribution(
portfolio_weights=pd.Series([0.3, 0.4, 0.3]),
benchmark_weights=pd.Series([0.4, 0.3, 0.3]),
portfolio_returns=pd.Series([0.05, 0.03, 0.04]),
benchmark_returns=pd.Series([0.04, 0.02, 0.03])
)
print(result)
注意: 交互收益很多人会忽略。它代表「配置和选股的协同效应」。如果交互收益很大,说明你的配置和选股策略之间有很强的相关性,需要进一步分析。
3.2 风险因子暴露分析:你的策略「长什么样」?
收益分解告诉你赚了多少。风险因子暴露告诉你——你的策略到底在承担什么风险。
我习惯用 Barra 模型来做因子暴露分析。说白了,就是把你的持仓收益率,对一系列风险因子做回归。常见的因子包括:
| 因子类别 | 具体因子 | 解释 |
|---|---|---|
| 市场因子 | MKT | 大盘涨跌的暴露度(Beta) |
| 风格因子 | Size, Value, Momentum | 大小盘、价值成长、动量反转 |
| 行业因子 | 申万一级行业 | 行业集中度风险 |
| 波动率因子 | Volatility | 低波/高波暴露 |
怎么做?用滚动回归。比如每个月跑一次回归,看因子载荷的变化。如果某个因子的暴露度突然飙升,那就要小心了——策略可能发生了「风格漂移」。
避坑指南: 我曾经见过一个策略,因子暴露度在半年内从0.2涨到0.8。团队以为是Alpha变强了,其实是偷偷加载了动量因子。后来动量反转,策略回撤30%。所以,因子暴露要设监控阈值,超过就报警。
因子暴露分析的代码示例
import statsmodels.api as sm
def factor_exposure_analysis(returns, factors, window=60):
"""
滚动窗口因子暴露分析
returns: 策略日收益率序列
factors: 因子收益率DataFrame
window: 滚动窗口长度(月)
"""
exposures = []
for i in range(window, len(returns)):
y = returns.iloc[i-window:i]
X = factors.iloc[i-window:i]
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
exposures.append(model.params)
return pd.DataFrame(exposures, index=returns.index[window:])
# 假设有策略收益率和因子数据
# exposures_df = factor_exposure_analysis(strategy_returns, factor_returns)
# 检查因子暴露是否稳定
3.3 Alpha衰减检测:你的策略「保质期」多长?
这是我最看重的部分。很多策略刚上线时表现很好,但过几个月就不行了。为什么?因为Alpha在衰减。
Alpha衰减,说白了就是策略的预测能力在下降。检测方法有很多,我常用的是IC(信息系数)序列分析。
- IC: 预测值与实际收益的秩相关系数。IC越高,预测越准。
- IC衰减曲线: 把不同持有期的IC画出来,看衰减速度。
- 半衰期: IC衰减到一半所需的时间。半衰期越短,策略失效越快。
举个例子。你有一个因子,日频IC是0.05。但到了第5天,IC就降到0.02了。这说明你的因子「保质期」只有5天。如果持仓周期是10天,那后面5天基本在吃土。
我的经验: 高频策略的半衰期通常只有1-3天。中低频策略可以到20-60天。如果半衰期突然缩短,说明市场结构变了,或者策略被「薅羊毛」了。
IC衰减检测代码
from scipy.stats import spearmanr
def ic_decay_analysis(factor_values, forward_returns, max_lag=20):
"""
IC衰减曲线分析
factor_values: 因子值序列
forward_returns: 未来N期收益DataFrame
max_lag: 最大滞后天数
"""
ic_series = []
for lag in range(1, max_lag + 1):
ic, _ = spearmanr(factor_values, forward_returns[f'ret_{lag}d'])
ic_series.append(ic)
# 计算半衰期
half_life = None
for i, ic in enumerate(ic_series):
if ic < ic_series[0] / 2:
half_life = i + 1
break
return {
'ic_curve': ic_series,
'half_life': half_life,
'decay_rate': (ic_series[0] - ic_series[-1]) / max_lag
}
# 使用示例
# result = ic_decay_analysis(factor, forward_ret, max_lag=20)
# print(f"半衰期: {result['half_life']}天")
重要提醒: IC衰减检测要定期做。我建议至少每月跑一次。如果发现半衰期连续3个月缩短,就要考虑策略迭代或停用了。别等到回撤了才后悔。
3.4 三合一:构建策略健康度仪表盘
三个模块单独看都有用,但合在一起威力更大。我个人习惯做一个策略健康度仪表盘,包含以下指标:
| 维度 | 指标 | 健康阈值 | 监控频率 |
|---|---|---|---|
| 收益分解 | Alpha占比 | > 60% | 月度 |
| 因子暴露 | 暴露度波动率 | < 0.3 | 周度 |
| Alpha衰减 | 半衰期 | > 10天 | 月度 |
| 综合 | 信息比率 | > 1.5 | 月度 |
嗯,这里要注意:阈值不是死的。不同策略类型、不同市场环境,阈值要动态调整。比如高频策略的半衰期阈值肯定比中低频低。
好了,绩效归因这块就讲到这里。你想想看,如果连自己的策略赚的是谁的钱、承担了什么风险、Alpha还能撑多久都不知道,那跟赌博有什么区别?