4、时序归因分析:滚动窗口归因、事件驱动归因、状态切换归因
时序归因,说白了就是回答一个问题:“策略的收益,到底是哪个时间段、哪个事件、哪种市场状态贡献的?”
我刚开始做归因时,犯过一个低级错误——直接用全样本跑一次归因,结果发现某个因子贡献了80%的收益。后来复盘才发现,那80%全集中在某三个月里,其他时间这个因子根本就是负贡献。嗯,从那以后,我再也不敢忽略时序维度了。
这一章,我们聊三种实战中最常用的时序归因方法:滚动窗口、事件驱动、状态切换。它们解决的是不同场景下的归因问题,但核心逻辑是一致的——把时间切开,看局部。
4.1 滚动窗口归因:看因子贡献的稳定性
滚动窗口归因,是我个人最常用的诊断手段。它的思路很简单:固定一个窗口长度(比如60个交易日),每次滑动一步,重新做一次归因。这样你就能看到每个因子的贡献率随时间的变化曲线。
为什么要这么做?因为全样本归因会掩盖一个事实——因子的表现可能是不稳定的。上半年是Alpha,下半年可能就是Beta了。
我在项目中遇到过这样一个案例:一个动量因子策略,全样本夏普比1.8,看起来很不错。但滚动窗口归因一跑,发现2023年Q3之后,动量因子的贡献率从60%直接掉到了10%以下。后来发现是市场风格切换了。如果不做滚动归因,你根本不知道这个因子已经“失效”了。
代码实现上,我习惯这样写:
import pandas as pd
import numpy as np
from statsmodels.api import OLS, add_constant
def rolling_factor_attribution(returns, factors, window=60):
"""
滚动窗口因子归因
returns: 策略日收益率序列
factors: 因子收益率DataFrame
window: 滚动窗口长度
"""
n = len(returns)
result = []
for i in range(window, n):
# 取窗口数据
y = returns.iloc[i-window:i]
X = add_constant(factors.iloc[i-window:i])
# OLS回归
model = OLS(y, X).fit()
# 记录因子贡献
contrib = model.params[1:] * factors.iloc[i-1:i].values[0]
result.append({
'date': returns.index[i],
**{f'contrib_{col}': contrib[j] for j, col in enumerate(factors.columns)}
})
return pd.DataFrame(result).set_index('date')
你想想看,滚动窗口归因最大的价值是什么?是帮你识别因子的“保质期”。一个因子能稳定贡献多久?什么时候开始衰减?这些信息对调仓决策至关重要。
4.2 事件驱动归因:抓关键节点的归因
事件驱动归因,解决的是另一个问题:“某个重大事件发生后,策略的收益来源发生了什么变化?”
举个例子。美联储加息、财报发布、政策出台——这些事件前后,市场结构可能发生剧变。全样本归因会把事件前后的表现混在一起,你看不出事件的影响。
我建议的做法是:以事件日为锚点,取前后各一段窗口(比如前后各30天),分别做归因,然后对比。这叫“事件前后对比归因”。
我曾经用这个方法诊断过一个CTA策略。策略在2022年俄乌冲突后突然回撤了15%。事件驱动归因一分析,发现冲突前策略主要靠趋势因子赚钱,冲突后趋势因子贡献率从70%掉到了-20%,而波动率因子贡献率从10%飙到了50%。说白了,市场从趋势市变成了震荡市,策略没跟上节奏。
代码实现上,我习惯这样写:
def event_driven_attribution(returns, factors, event_dates, window=30):
"""
事件驱动归因
returns: 策略日收益率序列
factors: 因子收益率DataFrame
event_dates: 事件日期列表
window: 事件前后窗口长度
"""
results = []
for event_date in event_dates:
# 取事件前后窗口
pre_start = event_date - pd.Timedelta(days=window)
post_end = event_date + pd.Timedelta(days=window)
pre_returns = returns.loc[pre_start:event_date]
pre_factors = factors.loc[pre_start:event_date]
post_returns = returns.loc[event_date:post_end]
post_factors = factors.loc[event_date:post_end]
# 分别做归因
pre_model = OLS(pre_returns, add_constant(pre_factors)).fit()
post_model = OLS(post_returns, add_constant(post_factors)).fit()
# 计算贡献率变化
contrib_change = post_model.params[1:] - pre_model.params[1:]
results.append({
'event_date': event_date,
**{f'change_{col}': contrib_change[i] for i, col in enumerate(factors.columns)}
})
return pd.DataFrame(results).set_index('event_date')
事件驱动归因还有一个变体——事件累积归因。就是把多个同类事件(比如所有加息日)的归因结果做平均,看这类事件的“平均影响”。这个方法在宏观策略中特别有用。
4.3 状态切换归因:按市场状态切分
状态切换归因,是我认为最接近“因果推断”的方法。它的思路是:先识别市场状态(牛市/熊市、高波动/低波动、趋势市/震荡市),然后按状态分别做归因。
为什么要这么做?因为同一个因子在不同市场状态下的表现可能截然不同。比如低波动因子在熊市里是避风港,在牛市里可能就是拖油瓶。
我个人习惯用HMM(隐马尔可夫模型)来做状态识别。当然,你也可以用更简单的方法——比如用波动率分位数、趋势强度指标来划分状态。
举个例子。我用HMM把市场分成三个状态:低波动趋势市、高波动震荡市、危机模式。然后分别跑归因。结果发现:
| 市场状态 | 动量因子贡献 | 价值因子贡献 | 波动率因子贡献 |
|---|---|---|---|
| 低波动趋势市 | 65% | 15% | 20% |
| 高波动震荡市 | -10% | 40% | 70% |
| 危机模式 | -30% | 60% | 70% |
你看,动量因子在趋势市里是王者,到了震荡市和危机模式就变成了负贡献。这个信息对策略的“状态自适应”设计非常关键。
代码实现上,我习惯用HMM:
from hmmlearn import hmm
def state_switching_attribution(returns, factors, n_states=3):
"""
状态切换归因
returns: 策略日收益率序列
factors: 因子收益率DataFrame
n_states: 状态数量
"""
# 用HMM识别市场状态
model = hmm.GaussianHMM(n_components=n_states, covariance_type="full")
model.fit(factors.values)
states = model.predict(factors.values)
# 分状态归因
results = {}
for state in range(n_states):
mask = (states == state)
state_returns = returns[mask]
state_factors = factors[mask]
if len(state_returns) < 20: # 样本太少就跳过
continue
reg = OLS(state_returns, add_constant(state_factors)).fit()
results[f'state_{state}'] = {
'contributions': reg.params[1:],
'n_samples': len(state_returns),
'r_squared': reg.rsquared
}
return results, states
状态切换归因还有一个进阶用法——状态转移归因。就是看策略在状态切换时的表现。比如从趋势市切换到震荡市时,策略的收益来源如何变化?这个分析能帮你设计“状态自适应”的调仓逻辑。
4.4 三种方法的对比与选择
说了这么多,你可能会问:这三种方法到底什么时候用?我整理了一个对比表:
| 方法 | 适用场景 | 核心问题 | 输出形式 |
|---|---|---|---|
| 滚动窗口归因 | 因子稳定性诊断 | 因子贡献随时间如何变化? | 时间序列图 |
| 事件驱动归因 | 重大事件影响分析 | 事件前后因子贡献有何变化? | 对比表格 |
| 状态切换归因 | 市场状态适应性分析 | 不同状态下因子贡献有何差异? | 状态-贡献矩阵 |
我个人习惯的组合是:先用滚动窗口归因做“体检”,发现异常时间段;然后用事件驱动归因定位具体事件;最后用状态切换归因解释根本原因。三步走,基本能把策略的时序行为说清楚。
嗯,这一章的内容就到这里。时序归因的核心就一句话:不要只看整体,要切开看局部。 你想想看,一个策略的收益来源如果随时间变化很大,那全样本归因的结果就是“平均的谎言”。只有把时间维度加进来,归因才有真正的诊断价值。