跳到主要内容

分层回测(Quantile Portfolios)

直觉

IC 只告诉你因子「平均」有没有效。分层回测更直观:按因子把股票分成 5 档(分位),分别算每档的收益,看是不是单调——最高档真的赚得比最低档多吗?最高档减最低档的多空组合,就是这个因子能捕获的「纯因子收益」。

方法

每个时段按因子 ffNN 只股票排序、均分 QQ 档(如 5 档):

Rt(q)=1GqiGqri,t,q=1,,QR^{(q)}_t = \frac{1}{|G_q|}\sum_{i\in G_q} r_{i,t},\qquad q=1,\dots,Q

多空组合 = 最高档 − 最低档:

RtLS=Rt(Q)Rt(1)R^{\text{LS}}_t = R^{(Q)}_t - R^{(1)}_t
「人话」解释:为什么强调「单调」?

一个好因子,五档收益应该从低到高递增(或递减),越极端差距越大——这叫单调。 如果只是「最高档比最低档高」、中间却乱跳,说明因子噪声大、不稳定。 多空组合剥离了市场涨跌,纯粹赚「排序」的钱,是因子收益最干净的度量。

可运行案例:质量因子五分位分层

sp500_metadata.csvquality 是设计好的横截面因子——按它分 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) 替代质量,并改成每月重新分组)——你会看到动量分层的形状和质量不同(动量更「尖」、回撤更大)。

小结

  • 分层回测:按因子排序均分 QQ 档,看每档收益是否单调
  • 多空组合(最高 − 最低)= 因子的纯收益,剥离了市场 Beta;
  • 单调性比单点差距更能说明因子质量。