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

做量化投资的朋友都知道,因子研究是策略的基石。但光有因子不行,你得知道它到底有没有用。怎么判断?靠的就是单因子测试框架。今天我就把这套框架的核心模块拆开来讲,包括回测框架怎么搭、IC/IR怎么算、分组收益怎么看、多空组合怎么搞。

说实话,我入行头两年踩过不少坑。最惨的一次,一个看起来年化收益15%的因子,实盘跑下来直接亏了3个月。后来复盘才发现,是回测框架里有个不起眼的未来函数bug。嗯,从那以后我对框架的每个细节都格外较真。

一、回测框架搭建:地基要稳

回测框架说白了就是模拟历史交易。你拿着因子信号,在历史数据上跑一遍,看看如果当时按这个信号买卖,能赚多少钱。框架的核心就三个东西:数据、信号、交易逻辑。

数据层:至少需要行情数据(开盘价、收盘价、成交量等)和财务数据(PE、PB、ROE等)。我习惯用日频数据做回测,频率太高容易过拟合,太低又丢失信号。

信号层:因子值就是信号。比如你算出一个“低波动因子”,那么每个月末按波动率排序,选波动最小的30只股票,这就是信号。

交易层:什么时候买、什么时候卖、手续费怎么扣、涨跌停怎么办。这些细节决定了回测的逼真程度。

下面是一个极简的回测框架代码骨架:

import pandas as pd
import numpy as np

def simple_backtest(factor_df, price_df, period=20):
    """
    factor_df: 因子值,index为日期,columns为股票代码
    price_df: 价格数据,同上
    period: 调仓周期(交易日)
    """
    # 每月末调仓
    rebalance_dates = factor_df.index[::period]
    
    # 存储每日收益
    daily_returns = []
    
    for i in range(len(rebalance_dates)-1):
        start = rebalance_dates[i]
        end = rebalance_dates[i+1]
        
        # 获取当期因子值
        factor_slice = factor_df.loc[start]
        
        # 选前20%的股票
        n_stocks = int(len(factor_slice.dropna()) * 0.2)
        selected = factor_slice.nlargest(n_stocks).index
        
        # 计算持有期收益
        ret = price_df.loc[end] / price_df.loc[start] - 1
        portfolio_ret = ret[selected].mean()
        
        daily_returns.append(portfolio_ret)
    
    return pd.Series(daily_returns, index=rebalance_dates[:-1])

这个框架虽然简单,但核心逻辑都有了。实际项目中,你还要处理停牌、退市、新股等特殊情况。我曾经因为没处理停牌,回测收益虚高了8个百分点,教训深刻。

避坑指南:回测中最常见的坑是“未来函数”。比如用当天的收盘价计算因子,然后当天买入。这在现实中根本做不到。正确的做法是用前一天的因子值,预测后一天的收益。

二、IC/IR分析:因子的预测能力有多强

IC(信息系数)衡量的是因子值和未来收益之间的相关性。简单说,因子值越高,未来收益是不是也越高?如果是,IC就是正的。

IC的计算方式有两种:

  • Spearman秩相关系数:对因子值和收益分别排序,算排序后的相关性。这是最常用的,因为它对异常值不敏感。
  • Pearson相关系数:直接算因子值和收益的线性相关性。容易受极端值影响,我一般不用。

IR(信息比率)是IC的均值除以IC的标准差。它衡量的是因子预测能力的稳定性。IR越高,说明因子不仅有效,而且稳定有效。

来看代码实现:

def calc_ic_ir(factor_df, ret_df):
    """
    计算每日IC和IR
    factor_df: 因子值
    ret_df: 未来一期收益
    """
    ic_series = []
    
    for date in factor_df.index:
        f = factor_df.loc[date].dropna()
        r = ret_df.loc[date].dropna()
        
        # 取交集
        common = f.index.intersection(r.index)
        if len(common) < 30:  # 样本太少不计算
            continue
        
        f_common = f[common]
        r_common = r[common]
        
        # Spearman秩相关
        ic = f_common.corr(r_common, method='spearman')
        ic_series.append(ic)
    
    ic_series = pd.Series(ic_series, index=factor_df.index[:len(ic_series)])
    
    # IR = IC均值 / IC标准差
    ir = ic_series.mean() / ic_series.std()
    
    return ic_series, ir

我个人习惯把IC的时序图画出来,看看它是不是稳定在0附近波动。如果IC忽正忽负,说明因子不稳定,IR会很低。一般IR大于0.5就算不错了,大于1.0就是很优秀的因子。

实战经验:我在做A股因子研究时发现,很多因子的IC在牛熊市里表现完全不同。比如动量因子在牛市里IC很高,熊市里直接变负。所以只看全样本IC是不够的,要分市场状态看。

三、分组收益分析:因子能区分好坏股票吗

分组收益分析是最直观的因子检验方法。你把股票按因子值从大到小分成10组(或5组),然后看每组未来的平均收益。如果第1组(因子值最大)的收益明显高于第10组(因子值最小),说明因子有效。

分组分析能回答三个问题:

  1. 单调性:收益是不是从第1组到第10组单调递减?如果是,说明因子有很好的区分度。
  2. 区分度:第1组和第10组的收益差距有多大?差距越大,因子越强。
  3. 稳定性:这种分组效果在不同时间段是否一致?

代码实现如下:

def group_analysis(factor_df, ret_df, n_groups=10):
    """
    分组收益分析
    返回每组在每个调仓期的收益
    """
    results = {}
    
    for date in factor_df.index:
        f = factor_df.loc[date].dropna()
        r = ret_df.loc[date].dropna()
        
        common = f.index.intersection(r.index)
        if len(common) < n_groups * 10:
            continue
        
        f_common = f[common]
        r_common = r[common]
        
        # 按因子值分组
        f_sorted = f_common.sort_values()
        group_size = len(f_sorted) // n_groups
        
        for g in range(n_groups):
            start = g * group_size
            end = (g+1) * group_size if g < n_groups-1 else len(f_sorted)
            group_stocks = f_sorted.index[start:end]
            
            group_ret = r_common[group_stocks].mean()
            
            if g not in results:
                results[g] = []
            results[g].append(group_ret)
    
    # 转成DataFrame
    result_df = pd.DataFrame(results)
    result_df.index = factor_df.index[:len(result_df)]
    
    return result_df

你想想看,如果第1组年化收益20%,第10组年化收益-5%,多空组合(做多第1组、做空第10组)的年化收益就是25%。这就是我们下一步要讲的多空组合。

小技巧:分组数不是越多越好。A股3000多只股票,分10组每组还有300只,统计意义够。如果分20组,每组只有150只,个别股票的影响就大了。

四、多空组合收益计算:真正的alpha来源

多空组合是因子测试的终极检验。它做多因子值最高的组,做空因子值最低的组,把市场风险对冲掉,剩下的就是因子的纯alpha收益。

计算多空组合收益很简单:

def long_short_return(group_returns, top_pct=0.1, bottom_pct=0.1):
    """
    计算多空组合收益
    group_returns: 分组收益DataFrame,每列是一组
    top_pct: 做多的组比例(前10%)
    bottom_pct: 做空的组比例(后10%)
    """
    n_groups = group_returns.shape[1]
    top_n = int(n_groups * top_pct)
    bottom_n = int(n_groups * bottom_pct)
    
    # 做多前top_n组,做空后bottom_n组
    long_ret = group_returns.iloc[:, :top_n].mean(axis=1)
    short_ret = group_returns.iloc[:, -bottom_n:].mean(axis=1)
    
    ls_ret = long_ret - short_ret
    
    return ls_ret

多空组合的年化收益、夏普比率、最大回撤,是衡量因子质量的核心指标。我一般要求多空组合的年化收益至少5%以上,夏普比率大于0.5,最大回撤不超过15%。

这里有个细节要注意:多空组合的收益计算要考虑做空成本。A股融券成本不低,而且很多股票根本融不到。所以实际做多空组合时,我通常用“多空收益差”来评估因子,而不是真的去做空。

核心观点:多空组合收益 = 因子带来的纯alpha。如果多空组合收益高且稳定,说明这个因子确实能预测股票收益,而不是靠运气或市场beta。

五、知识体系总览

说了这么多,我把单因子测试框架的核心逻辑画成了一张图,方便你理解各个模块之间的关系:

单因子测试框架核心逻辑 数据层 行情数据 + 财务数据 信号层 因子值计算 + 信号生成 交易层 买卖逻辑 + 费用 回测引擎 IC/IR分析 预测能力评估 分组收益分析 区分度检验 多空组合 纯alpha提取 因子有效性综合评估

从这张图可以看得很清楚:数据层、信号层、交易层构成了回测引擎的基础。回测跑完之后,我们通过IC/IR分析、分组收益分析、多空组合收益计算三个维度来评估因子。三者相互印证,缺一不可。

我个人习惯是先看分组收益的单调性,如果单调性好,再看IC/IR是否稳定,最后用多空组合算算实际能赚多少钱。这个顺序能帮你快速过滤掉大部分无效因子。

好了,单因子测试框架的核心内容就这些。框架搭好了,后面的事情就顺了。记住一句话:回测是工具,不是目的。真正重要的是你从回测中理解因子的行为逻辑。