分层回测(Quantile Portfolios)
直觉
IC 只告诉你因子「平均」有没有效。分层回测更直观:按因子把股票分成 5 档(分位),分别算每档的收益,看是不是单调——最高档真的赚得比最低档多吗?最高档减最低档的多空组合,就是这个因子能捕获的「纯因子收益」。
方法
每个时段按因子 把 只股票排序、均分 档(如 5 档):
多空组合 = 最高档 − 最低档:
「人话」解释:为什么强调「单调」?
一个好因子,五档收益应该从低到高递增(或递减),越极端差距越大——这叫单调。 如果只是「最高档比最低档高」、中间却乱跳,说明因子噪声大、不稳定。 多空组合剥离了市场涨跌,纯粹赚「排序」的钱,是因子收益最干净的度量。
可运行案例:质量因子五分位分层
sp500_metadata.csv 的 quality 是设计好的横截面因子——按它分 5 档,预期看到单调价差。按月回测,并报告多空组合指标。
import quant
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').loc[prices.columns]
quality = meta['quality']
# 按质量分 5 组(每组 10 只), 恒定分组
grp = (quality.rank(method='first') - 1) // 10 # 0..4
monthly = prices.resample('ME').last()
mret = monthly.pct_change()
# 每组等权月收益
qret = pd.DataFrame({g: mret[grp[grp == g].index].mean(axis=1) for g in range(5)})
ls = (qret[4] - qret[0]).fillna(0.0)
ls_eq = (1 + ls).cumprod()
ann = (1 + qret.fillna(0)).prod() ** (12 / len(qret)) - 1
print("各组年化收益(应为单调):")
for g in range(5):
print(f" Q{g+1} ({'低' if g==0 else '高' if g==4 else ' '}): {ann[g]:+.2%}")
print("\n多空(Q5-Q1) 指标:")
print(quant.performance_summary(ls_eq, ls, freq=12).round(3))
cum = (1 + qret.fillna(0)).cumprod()
plt.figure(figsize=(9, 3.8))
for g in range(5):
plt.plot(cum[g], lw=1.2, label=f'Q{g+1}')
plt.plot(ls_eq, color='black', ls='--', lw=1.6, label='Q5-Q1 多空')
plt.title('质量因子五分位分层回测'); plt.ylabel('净值'); plt.legend(fontsize=8)
plt.tight_layout(); plt.show()
动手改一改
把分组因子从 quality 换成动量(用 np.log(monthly/monthly.shift(6)).shift(1) 替代质量,并改成每月重新分组)——你会看到动量分层的形状和质量不同(动量更「尖」、回撤更大)。
小结
- 分层回测:按因子排序均分 档,看每档收益是否单调;
- 多空组合(最高 − 最低)= 因子的纯收益,剥离了市场 Beta;
- 单调性比单点差距更能说明因子质量。