第四节:分组回测方法——分位数分组、多空组合构建、净值曲线绘制、收益统计指标

好,咱们进入正题。因子有效性检验里,分组回测是最直观、最常用的方法。说白了,就是看看按因子值把股票分成几组后,高分组和低分组的表现到底差多少。我做了这么多年量化,这步几乎没跳过。

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(最小组),看看这个组合的收益曲线。如果多空收益显著为正,说明因子有区分度。

构建步骤

  1. 每个调仓日,计算 Q5 和 Q1 的等权平均收益。
  2. 多空收益 = Q5 收益 - Q1 收益。
  3. 累计多空收益 = 每日多空收益的累乘。
避坑指南:我曾经在计算多空收益时,直接用 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 本章知识体系

下面这张图,是我自己总结的分组回测流程,你一看就明白:

分组回测核心流程 步骤1:数据准备 因子值 + 收益率 步骤2:分位数分组 Q1 ~ Q5 等分 步骤3:多空组合 做多Q5,做空Q1 步骤4:净值曲线 分组 + 多空曲线 步骤5:统计指标 夏普、回撤、胜率 步骤6:结论 因子有效 or 无效 关键检查点:单调性、显著性、稳定性

这张图把整个流程串起来了。从数据准备到最终结论,每一步都环环相扣。你想想看,如果哪一步出了问题,后面的结果就全废了。

核心要点总结

  • 分组回测是因子检验的基石,必须做
  • 多空组合收益是判断因子有效性的关键
  • 净值曲线要单调,统计指标要全面
  • 别忘了交易成本和样本量问题

好了,分组回测这部分就讲到这里。记住,回测只是第一步,真正的考验在实盘。下一节我们会聊到因子失效的应对方案,到时候你就知道,回测做得再好,市场也会教你做人。

专注资料整理