第四节:分组回测方法——分位数分组、多空组合构建、净值曲线绘制、收益统计指标
好,咱们进入正题。因子有效性检验里,分组回测是最直观、最常用的方法。说白了,就是看看按因子值把股票分成几组后,高分组和低分组的表现到底差多少。我做了这么多年量化,这步几乎没跳过。
4.1 分位数分组:把股票装进不同的篮子
分位数分组,核心思想很简单:每个调仓日,把所有股票按因子值从小到大排序,然后等分成 N 组。比如分成 5 组,就是 Q1(最小)到 Q5(最大)。
分组方式有讲究:
- 等分位数:每组股票数量相等。我最常用的是 5 组或 10 组。5 组够用,10 组更精细。
- 固定阈值:比如只取前 20% 和后 20%。适合因子值分布不均匀的情况。
- 市值加权 vs 等权:我个人习惯用等权,因为能更纯粹地反映因子选股能力。市值加权容易受大票干扰。
我的经验:分组数不是越多越好。分组太多,每组样本量太少,统计结果不稳定。我一般建议 5 组起步,如果样本量够大(比如全 A 股),可以试试 10 组。
来看一段 Python 代码,实现月度调仓的分位数分组:
import pandas as pd
import numpy as np
def quantile_group(df, factor_col, n_groups=5):
"""
分位数分组函数
df: 包含因子值和收益率的 DataFrame,索引为日期,列为股票代码
factor_col: 因子列名
n_groups: 分组数
"""
# 按日期分组,每个日期内排序
df['rank'] = df.groupby('date')[factor_col].rank(method='first')
df['group'] = df.groupby('date')['rank'].transform(
lambda x: pd.qcut(x, n_groups, labels=False) + 1
)
return df
# 使用示例
# 假设 data 包含 'date', 'stock', 'factor', 'return'
data = quantile_group(data, 'factor', 5)
print(data.head())
嗯,这里要注意:pd.qcut 在因子值重复时会报错,所以我用了 rank(method='first') 先处理一下。我曾经在这个坑里摔过,回测结果莫名其妙多了一组空值。
4.2 多空组合构建:多空收益差才是硬道理
分组回测的精华,在于多空组合。也就是做多 Q5(最大组),做空 Q1(最小组),看看这个组合的收益曲线。如果多空收益显著为正,说明因子有区分度。
构建步骤:
- 每个调仓日,计算 Q5 和 Q1 的等权平均收益。
- 多空收益 = Q5 收益 - Q1 收益。
- 累计多空收益 = 每日多空收益的累乘。
避坑指南:我曾经在计算多空收益时,直接用 Q5 减去 Q1 的净值,结果发现曲线一直向上。后来才发现,我忘了考虑交易成本。做空成本通常比做多高,尤其是 A 股融券成本。建议在回测中至少扣除 0.1% 的单边交易成本。
代码实现:
def long_short_portfolio(df, group_col='group', return_col='return'):
"""
构建多空组合
"""
# 计算每组每日平均收益
group_returns = df.groupby(['date', group_col])[return_col].mean().unstack()
# 多空收益:Q5 - Q1
ls_returns = group_returns[group_returns.columns[-1]] - group_returns[group_returns.columns[0]]
# 累计净值
ls_cum = (1 + ls_returns).cumprod()
return ls_returns, ls_cum
# 使用
ls_ret, ls_cum = long_short_portfolio(data)
print(ls_cum.tail())
4.3 净值曲线绘制:一张图胜过千言万语
净值曲线,是检验因子有效性的第一道防线。我每次拿到新因子,第一件事就是画图。曲线应该单调递增,且多空曲线稳步向上。
画图要点:
- 分组净值曲线:Q1 到 Q5 的净值曲线应该呈单调排列。Q1 在最下面,Q5 在最上面。
- 多空净值曲线:应该平稳向上,回撤不能太大。
- 基准对比:加上沪深 300 或中证 500 作为基准,看看因子是否有超额收益。
我用 matplotlib 画图,代码很简单:
import matplotlib.pyplot as plt
def plot_group_net_worth(group_returns, ls_cum):
"""
绘制分组净值曲线和多空净值曲线
group_returns: 每组每日收益,DataFrame,列为组号
ls_cum: 多空累计净值
"""
# 计算每组累计净值
cum_returns = (1 + group_returns).cumprod()
plt.figure(figsize=(12, 6))
# 子图1:分组净值
plt.subplot(1, 2, 1)
for col in cum_returns.columns:
plt.plot(cum_returns.index, cum_returns[col], label=f'Q{col}')
plt.title('分组净值曲线')
plt.legend()
plt.grid(True)
# 子图2:多空净值
plt.subplot(1, 2, 2)
plt.plot(ls_cum.index, ls_cum, color='red', linewidth=2)
plt.title('多空组合净值')
plt.grid(True)
plt.tight_layout()
plt.show()
# 调用
plot_group_net_worth(group_returns, ls_cum)
我的习惯:画图时,我会把净值曲线用对数坐标显示。因为对数坐标能更清楚地看到长期增长趋势,尤其是回测周期超过 3 年时。你想想看,线性坐标下,后期的小波动根本看不出来。
4.4 收益统计指标:用数字说话
光看图不够,还得有量化指标。我常用的统计指标有这些:
| 指标 | 含义 | 计算方式 | 理想值 |
|---|---|---|---|
| 年化收益率 | 多空组合的年化收益 | 累计收益^(252/天数) - 1 | > 5% |
| 年化波动率 | 收益的波动程度 | 日收益标准差 * sqrt(252) | 越低越好 |
| 夏普比率 | 风险调整后收益 | 年化收益 / 年化波动率 | > 1.0 |
| 最大回撤 | 从峰值到谷底的最大跌幅 | 累计净值的最大回撤 | < 15% |
| 胜率 | 多空收益为正的天数占比 | 正收益天数 / 总天数 | > 55% |
| IC均值 | 因子与未来收益的秩相关系数 | Spearman 相关系数 | 绝对值 > 0.02 |
代码实现这些指标:
def compute_performance_metrics(ls_returns, ls_cum):
"""
计算多空组合的收益统计指标
"""
# 年化收益率
total_days = len(ls_returns)
annual_return = ls_cum.iloc[-1] ** (252 / total_days) - 1
# 年化波动率
annual_vol = ls_returns.std() * np.sqrt(252)
# 夏普比率
sharpe = annual_return / annual_vol if annual_vol != 0 else 0
# 最大回撤
rolling_max = ls_cum.expanding().max()
drawdown = (ls_cum - rolling_max) / rolling_max
max_drawdown = drawdown.min()
# 胜率
win_rate = (ls_returns > 0).mean()
return {
'年化收益率': f'{annual_return:.2%}',
'年化波动率': f'{annual_vol:.2%}',
'夏普比率': f'{sharpe:.2f}',
'最大回撤': f'{max_drawdown:.2%}',
'胜率': f'{win_rate:.2%}'
}
# 调用
metrics = compute_performance_metrics(ls_ret, ls_cum)
for k, v in metrics.items():
print(f'{k}: {v}')
注意:夏普比率不是万能的。我见过夏普 2.0 的因子,但最大回撤 30%,这种因子你敢用吗?所以一定要综合看多个指标。我个人习惯先看最大回撤,再看夏普。
4.5 本章知识体系
下面这张图,是我自己总结的分组回测流程,你一看就明白:
这张图把整个流程串起来了。从数据准备到最终结论,每一步都环环相扣。你想想看,如果哪一步出了问题,后面的结果就全废了。
核心要点总结:
- 分组回测是因子检验的基石,必须做
- 多空组合收益是判断因子有效性的关键
- 净值曲线要单调,统计指标要全面
- 别忘了交易成本和样本量问题
好了,分组回测这部分就讲到这里。记住,回测只是第一步,真正的考验在实盘。下一节我们会聊到因子失效的应对方案,到时候你就知道,回测做得再好,市场也会教你做人。