单因子测试框架:IC分析、分组回测、多空组合收益、因子收益率计算

单因子测试,说白了就是给每个候选因子做一次「体检」。

我做了这么多年量化,见过太多人拿着一个看起来不错的因子直接上实盘,结果亏得底裤都不剩。为什么?因为没有经过严格的单因子测试。

这一章,我们就来拆解这个体检流程。它包含四个核心模块:IC分析分组回测多空组合收益因子收益率计算

核心观点:单因子测试不是为了证明因子有效,而是为了找出它什么时候失效。

单因子测试框架 IC分析 Rank IC / 截面IC IC序列 / ICIR 分组回测 5组 / 10组 单调性检验 多空组合 多头 - 空头 对冲收益 因子收益率 Fama-MacBeth 回归法 流程:因子数据 → IC分析 → 分组回测 → 多空组合 → 因子收益率 每个环节都在回答一个问题:这个因子到底能不能赚钱? ⚠️ 常见坑:只看IC均值,不看IC序列的稳定性 IC高但波动大 → 因子不稳定,容易翻车

1. IC分析:因子的预测能力有多强?

IC,全称是Information Coefficient,信息系数。它衡量的是因子值与未来收益之间的相关性。

我个人习惯用Rank IC,也就是秩相关系数。为什么?因为它对极端值不敏感。你想想看,A股市场经常有妖股,一个涨停就把普通相关系数拉偏了,Rank IC就不会。

小技巧:IC的绝对值在0.03以上就算有预测能力了。别追求0.1以上,那种因子要么是过拟合,要么是数据出错了。

具体计算很简单。每个月末,我们计算因子值与下个月收益率的Spearman相关系数。然后得到一个IC序列。

import pandas as pd
import numpy as np
from scipy.stats import spearmanr

def calc_rank_ic(factor_series, return_series):
    """
    计算截面Rank IC
    factor_series: 某期因子值
    return_series: 对应下期收益率
    """
    # 去掉缺失值
    mask = factor_series.notna() & return_series.notna()
    f = factor_series[mask]
    r = return_series[mask]
    
    if len(f) < 30:
        return np.nan
    
    ic, p_value = spearmanr(f, r)
    return ic

# 月度IC计算示例
monthly_ic = []
for date in factor_data.index.get_level_values('date').unique():
    factor_slice = factor_data.xs(date, level='date')['factor']
    return_slice = return_data.xs(date, level='date')['ret']
    ic = calc_rank_ic(factor_slice, return_slice)
    monthly_ic.append({'date': date, 'ic': ic})

ic_df = pd.DataFrame(monthly_ic)
print(f"IC均值: {ic_df['ic'].mean():.4f}")
print(f"IC标准差: {ic_df['ic'].std():.4f}")
print(f"ICIR: {ic_df['ic'].mean() / ic_df['ic'].std():.4f}")

这里有个关键指标——ICIR,也就是IC的均值除以标准差。它衡量的是IC的稳定性。ICIR大于0.5算合格,大于1.0算优秀。

我曾经踩过的坑:有一次我测了一个因子,IC均值0.06,看起来不错。但ICIR只有0.2,说明IC波动极大。后来回测发现,这个因子在牛市中有效,熊市中完全失效。如果只看IC均值,就会被误导。

2. 分组回测:因子能不能区分好坏股票?

IC分析告诉你因子有没有预测能力,但没告诉你它能不能用来选股。分组回测就是干这个的。

做法很简单:每个月把所有股票按因子值从大到小排序,分成5组或10组。然后计算每组下个月的等权或市值加权收益。

def group_backtest(factor_df, return_df, n_groups=5):
    """
    分组回测
    factor_df: 因子值,index=[date, stock]
    return_df: 收益率,index=[date, stock]
    """
    results = []
    
    for date in factor_df.index.get_level_values('date').unique():
        # 获取当期因子
        f = factor_df.xs(date, level='date')['factor']
        # 获取下期收益
        next_date = get_next_trading_date(date)
        r = return_df.xs(next_date, level='date')['ret']
        
        # 合并
        combined = pd.DataFrame({'factor': f, 'ret': r}).dropna()
        
        # 分组
        combined['group'] = pd.qcut(combined['factor'], n_groups, 
                                    labels=range(n_groups))
        
        # 计算每组平均收益
        group_ret = combined.groupby('group')['ret'].mean()
        group_ret['date'] = date
        results.append(group_ret)
    
    result_df = pd.DataFrame(results).set_index('date')
    return result_df

# 运行回测
group_returns = group_backtest(factor_df, return_df, n_groups=5)
# 计算累计收益
cum_returns = (1 + group_returns).cumprod()

好的因子,分组收益应该是单调的。也就是说,第1组收益最高,第5组收益最低,中间依次排列。

判断标准:

  • 单调性明显:优秀因子
  • 部分单调:可用,但需注意
  • 完全无序:放弃吧

3. 多空组合收益:对冲掉市场风险

分组回测有个问题:它包含了市场整体的涨跌。比如牛市里所有组都涨,熊市里所有组都跌,你很难判断因子本身的能力。

多空组合就是解决这个问题的。我们做多第1组(因子值最大的组),做空第5组(因子值最小的组)。这样市场涨跌的影响就被对冲掉了。

def long_short_portfolio(group_returns):
    """
    计算多空组合收益
    group_returns: 分组回测结果,列名为0,1,2,3,4
    """
    # 做多第0组,做空第4组
    ls_returns = group_returns[0] - group_returns[4]
    
    # 统计指标
    annual_return = ls_returns.mean() * 252
    annual_vol = ls_returns.std() * np.sqrt(252)
    sharpe = annual_return / annual_vol
    
    return ls_returns, sharpe

ls_ret, sharpe = long_short_portfolio(group_returns)
print(f"多空组合年化收益: {annual_return:.2%}")
print(f"多空组合夏普比: {sharpe:.2f}")

多空组合的夏普比如果超过1.0,说明这个因子有很强的选股能力。我见过最好的因子夏普比能到2.0以上,但那种因子通常很快就会被市场发现,然后失效。

注意:多空组合在实盘中很难完全复制,因为做空有成本,而且很多小股票融不到券。但作为因子评价工具,它非常有效。

4. 因子收益率计算:Fama-MacBeth回归

最后一个环节,也是最学术化的一个——计算因子收益率。

这里用的是Fama-MacBeth两步法。第一步,每期做截面回归,得到因子载荷;第二步,对因子载荷的时间序列做统计推断。

import statsmodels.api as sm

def fama_macbeth(factor_df, return_df, control_factors=None):
    """
    Fama-MacBeth回归
    factor_df: 待测因子
    return_df: 收益率
    control_factors: 控制变量,如市值、估值等
    """
    # 第一步:截面回归
    betas = []
    for date in factor_df.index.get_level_values('date').unique():
        # 准备数据
        f = factor_df.xs(date, level='date')
        r = return_df.xs(date, level='date')
        
        X = sm.add_constant(f)
        if control_factors is not None:
            c = control_factors.xs(date, level='date')
            X = pd.concat([X, c], axis=1)
        
        # 回归
        model = sm.OLS(r, X, missing='drop')
        result = model.fit()
        betas.append(result.params)
    
    beta_df = pd.DataFrame(betas)
    
    # 第二步:时间序列统计
    factor_return = beta_df['factor'].mean()
    t_stat = beta_df['factor'].mean() / beta_df['factor'].std() * np.sqrt(len(beta_df))
    
    return factor_return, t_stat, beta_df

# 运行
factor_ret, t_stat, _ = fama_macbeth(factor_df, return_df)
print(f"因子收益率: {factor_ret:.4%}")
print(f"t统计量: {t_stat:.2f}")

t统计量的绝对值大于2,说明因子收益率显著不为零。我个人习惯看t值大于3才放心,因为A股的数据质量参差不齐,t值2可能只是运气。

我曾经犯过的错:刚开始做因子测试时,我直接跑Fama-MacBeth,没加控制变量。结果发现一个因子t值高达5,兴奋得不行。后来加了市值和估值作为控制变量,t值直接掉到1.2。原来这个因子只是市值因子的代理变量。

总结:四个环节缺一不可

单因子测试不是走流程,而是帮你理解因子的本质。

  • IC分析:看因子有没有预测能力
  • 分组回测:看因子能不能区分股票
  • 多空组合:看因子能不能独立赚钱
  • 因子收益率:看因子是不是真的显著

这四个环节做完,一个因子能不能用,你心里就有数了。

我的建议:每个因子至少做3年的回测,覆盖不同的市场环境。牛市、熊市、震荡市都跑一遍,你才能知道这个因子到底靠不靠谱。

嗯,单因子测试就讲到这里。下一章我们会聊多因子组合,那才是真正上强度的部分。


专注资料整理