单因子测试框架:IC/IR分析、分层回测法、因子收益率计算

做过多因子模型的朋友都知道,选因子就像相亲——看着顺眼不行,得真能过日子。单因子测试就是帮我们把关的环节。我个人习惯把这部分拆成三个维度:预测能力(IC/IR)、区分能力(分层回测)、收益贡献(Fama-MacBeth回归)。咱们一个一个聊。

IC/IR分析:因子的预测能力到底行不行

IC,全称是Information Coefficient,信息系数。说白了就是因子值和未来收益之间的相关性。我刚开始做量化时,总觉得IC越高越好,后来发现没那么简单。

IC的计算方式有两种:

  • Spearman秩相关系数:看排序一致性,对极端值不敏感
  • Pearson相关系数:看线性关系,但容易被异常值带偏

我个人更推荐Spearman。为什么?因为因子值本身可能分布很偏,但排序关系往往更稳定。我在项目中遇到过一只因子,Pearson IC只有0.02,但Spearman IC有0.08——后来分层回测发现,它确实能区分好坏股票,只是线性关系不强。

IR呢?IR = mean(IC) / std(IC)。它衡量的是IC的稳定性。你想想看,一个因子IC忽高忽低,你敢重仓用吗?

实战经验:我一般要求月度IC均值 > 0.02,IR > 0.5。低于这个门槛的因子,基本可以放弃了。

代码实现其实不复杂:

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

def calc_ic(factor_series, return_series, method='spearman'):
    """计算截面IC"""
    if method == 'spearman':
        ic, p_value = spearmanr(factor_series, return_series)
    else:
        ic = factor_series.corr(return_series)
    return ic

def calc_ir(ic_series):
    """计算IR"""
    return ic_series.mean() / ic_series.std()

# 月度滚动计算
monthly_ic = []
for date in unique_dates:
    cross_data = data[data['date'] == date]
    ic = calc_ic(cross_data['factor'], cross_data['next_return'])
    monthly_ic.append(ic)

ir = calc_ir(pd.Series(monthly_ic))

避坑指南:我曾经犯过一个错误——直接用未来收益和当期因子算IC,没考虑行业中性化。结果IC虚高,实盘一跑就崩。记得先做行业市值中性化处理。

分层回测法:因子能不能区分好坏股票

IC告诉你因子和收益有没有关系,但分层回测告诉你——这种关系能不能用来赚钱。

做法很简单:每个月把所有股票按因子值从大到小排序,分成10组(或者5组)。然后看第1组(因子最大)和第10组(因子最小)的收益差异。

我习惯用这个框架:

  1. 每月末按因子值排序,分10层
  2. 每层等权或市值加权构建组合
  3. 持有到下个月末,记录收益
  4. 滚动重复,计算每层的累计净值

好的因子应该呈现单调性——第1组收益最高,第10组最低,中间依次排列。如果出现第5组收益最高,那这个因子就有问题。

注意:分层回测最怕幸存者偏差。我曾经用了一个退市股票没剔除的数据集,结果因子表现好得离谱——后来发现是因为垃圾股退市了,剩下的都是好股票。一定要做退市处理。

代码示例:

def layer_backtest(data, factor_col, return_col, n_layers=10):
    """分层回测"""
    results = []
    
    for date in data['date'].unique():
        cross_data = data[data['date'] == date].copy()
        cross_data['rank'] = cross_data[factor_col].rank(pct=True)
        cross_data['layer'] = pd.cut(cross_data['rank'], 
                                     bins=n_layers, 
                                     labels=range(n_layers))
        
        layer_returns = cross_data.groupby('layer')[return_col].mean()
        results.append(layer_returns)
    
    result_df = pd.DataFrame(results)
    return result_df.mean()  # 返回每层平均收益

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

终于到了最硬核的部分。Fama-MacBeth回归,简称FM回归。它解决了一个核心问题:因子到底能带来多少超额收益?

FM回归分两步走:

  • 第一步:每个时间截面做一次横截面回归。比如每个月,用当期因子值对下期收益做回归。
  • 第二步:把各期的回归系数取均值,做t检验。

为什么这么做?因为传统的时间序列回归假设残差独立同分布,但股票收益的残差是相关的——同行业的股票往往一起涨跌。FM回归通过截面回归+时间序列平均,解决了这个问题。

我当年学FM回归时,最困惑的是为什么要做两步。后来想明白了:第一步是看因子在每个时点上的解释力,第二步是看这种解释力是否稳定。

import statsmodels.api as sm

def fama_macbeth(data, factor_cols, return_col, date_col='date'):
    """Fama-MacBeth回归"""
    results = []
    
    for date in data[date_col].unique():
        cross_data = data[data[date_col] == date].dropna()
        if len(cross_data) < 30:
            continue
            
        X = cross_data[factor_cols]
        X = sm.add_constant(X)
        y = cross_data[return_col]
        
        model = sm.OLS(y, X).fit()
        results.append(model.params)
    
    # 第二步:时间序列平均
    coef_df = pd.DataFrame(results)
    final_results = {}
    
    for col in coef_df.columns:
        mean_coef = coef_df[col].mean()
        t_stat = coef_df[col].mean() / (coef_df[col].std() / np.sqrt(len(coef_df)))
        final_results[col] = {'系数': mean_coef, 't值': t_stat}
    
    return final_results

关键点:t值的绝对值大于2,通常认为因子显著。但要注意——如果因子数量多,多重共线性会干扰结果。我一般先做VIF检验,剔除高度相关的因子。

三种方法的联动关系

这三种方法不是孤立的。我通常这样组合使用:

  • 先用IC/IR做初筛,剔除预测能力差的因子
  • 再用分层回测验证单调性,确保因子能区分好坏
  • 最后用FM回归量化因子收益率,确认统计显著性

只有三个条件都满足的因子,我才会纳入最终的多因子模型。少一个都不行——这是我在实盘里摔过跟头换来的教训。

个人习惯:我会把每个因子的IC序列、分层收益曲线、FM回归系数画在一张图上。一眼就能看出这个因子靠不靠谱。数据可视化比看数字直观多了。

嗯,单因子测试这部分内容就这些。记住一个原则:因子不是越多越好,而是越精越好。一个经过严格测试的优质因子,胜过十个凑数的垃圾因子。