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

好,咱们进入正题。单因子测试,是整个因子投资体系的基石。说白了,就是你要先证明你手里这把“刀”是锋利的,才能拿去上战场。我见过太多人,因子逻辑讲得天花乱坠,一跑回测就露馅。所以,这一章咱们把地基打牢。

4.1 因子定义与计算:从想法到数字

一个因子,本质上就是一个数学公式。它把股票的各种原始数据(价格、成交量、财务数据)加工成一个有预测能力的数字。

因子的三大要素:

  • 逻辑性:为什么这个因子能预测收益?你得有个故事。比如“低波动异象”,逻辑是散户喜欢追高,导致高波动股票被高估。
  • 可计算性:数据要能拿到,计算要能复现。别搞个需要手工整理的数据,那没法量化。
  • 单调性:因子值越大,未来收益应该越高(或越低),而不是忽高忽低。

举个例子,一个经典的“反转因子”:

# 因子定义:过去20个交易日的累计收益率
# 因子值越大,代表过去涨得越多,预期未来会下跌(反转)
import pandas as pd
import numpy as np

def calc_reversal_factor(close_price, window=20):
    """
    close_price: DataFrame, 行是日期,列是股票代码
    """
    # 计算过去window天的累计收益率
    ret = close_price.pct_change(window)
    # 这里我习惯取负值,让因子值越大,预期收益越高
    factor = -ret
    return factor

# 使用示例
# factor_df = calc_reversal_factor(df_close, 20)
我的小习惯:因子计算时,我通常会做一步“去极值”和“标准化”。不然几个极端值会把整个截面搞偏。我常用MAD(中位数绝对偏差)法去极值,然后用Z-score标准化。

4.2 分组回测框架:把股票装进篮子里

因子算出来了,怎么验证它有效?最直观的方法就是分组回测。把股票按因子值从大到小排序,分成10组(Decile)或5组(Quintile),然后看每组未来的收益表现。

我画了个流程图,帮你理解这个框架:

分组回测框架流程图 输入:因子值 + 收益数据 步骤1:按因子值排序,分成10组 步骤2:计算每组未来N日收益 输出:分组收益曲线 + 统计指标

框架不复杂,但细节决定成败。我分享一段核心代码:

def group_backtest(factor_df, ret_df, groups=10):
    """
    分组回测核心函数
    factor_df: 因子值,行日期,列股票
    ret_df: 未来N日收益,行日期,列股票
    """
    # 对齐日期和股票
    common_dates = factor_df.index.intersection(ret_df.index)
    common_stocks = factor_df.columns.intersection(ret_df.columns)
    
    factor_df = factor_df.loc[common_dates, common_stocks]
    ret_df = ret_df.loc[common_dates, common_stocks]
    
    # 逐日分组
    group_returns = []
    for date in common_dates:
        # 获取当天的因子值和收益
        factor_today = factor_df.loc[date].dropna()
        ret_today = ret_df.loc[date]
        
        # 按因子值排序,分成groups组
        sorted_factors = factor_today.sort_values()
        group_size = len(sorted_factors) // groups
        
        for g in range(groups):
            start_idx = g * group_size
            end_idx = (g + 1) * group_size if g < groups - 1 else len(sorted_factors)
            group_stocks = sorted_factors.index[start_idx:end_idx]
            
            # 计算该组等权收益
            group_ret = ret_today[group_stocks].mean()
            group_returns.append({
                'date': date,
                'group': g + 1,
                'return': group_ret
            })
    
    result = pd.DataFrame(group_returns)
    return result
注意:分组时一定要用“截面排序”,也就是每天独立排序。我曾经见过有人用历史分位数来分组,那会引入前视偏差,结果虚高。记住,每天都是一个新的开始。

4.3 多空组合收益分析:真正的Alpha在哪?

分组回测看的是单调性。但真正衡量因子选股能力的,是多空组合收益。也就是:做多因子值最高的组,做空因子值最低的组,看这个组合能赚多少钱。

为什么这个指标重要?因为它剥离了市场整体涨跌的影响。你想想看,如果市场涨了20%,你分组全都涨,那说明不了什么。但多空组合的收益,才是因子真正的Alpha。

计算多空收益很简单:

# 假设group_returns是分组回测的结果
# 计算多空组合收益
long_short = group_returns[group_returns['group'] == groups].copy()
long_short['return'] = long_short['return'] - group_returns[group_returns['group'] == 1]['return'].values
long_short['cumulative'] = (1 + long_short['return']).cumprod()

# 年化收益率
annual_ret = long_short['cumulative'].iloc[-1] ** (252 / len(long_short)) - 1
# 年化波动率
annual_vol = long_short['return'].std() * np.sqrt(252)
# 夏普比率
sharpe = annual_ret / annual_vol

print(f"多空组合年化收益: {annual_ret:.2%}")
print(f"多空组合夏普比率: {sharpe:.2f}")

我个人习惯,多空组合的夏普比率至少要大于1,我才会考虑把这个因子纳入模型。低于0.5的,基本就是噪音。

4.4 因子IC/IR分析:从收益到相关性

分组回测有个缺点:它把连续变量离散化了。有时候分组收益单调,但组内差异很大。这时候就需要IC(信息系数)和IR(信息比率)来帮忙。

IC:因子值与未来收益的截面相关系数。常用Spearman秩相关系数,因为它对异常值不敏感。

IR:IC的均值除以IC的标准差。衡量因子预测能力的稳定性。

计算代码:

def calc_ic_ir(factor_df, ret_df):
    """
    计算每日IC和IR
    """
    common_dates = factor_df.index.intersection(ret_df.index)
    ic_series = []
    
    for date in common_dates:
        factor_today = factor_df.loc[date].dropna()
        ret_today = ret_df.loc[date]
        
        # 取共同股票
        common = factor_today.index.intersection(ret_today.index)
        if len(common) < 30:  # 股票太少,IC不稳定
            continue
            
        factor_values = factor_today[common]
        ret_values = ret_today[common]
        
        # 计算Spearman相关系数
        from scipy.stats import spearmanr
        ic, _ = spearmanr(factor_values, ret_values)
        ic_series.append(ic)
    
    ic_series = pd.Series(ic_series, index=common_dates[:len(ic_series)])
    
    # 计算IR
    ic_mean = ic_series.mean()
    ic_std = ic_series.std()
    ir = ic_mean / ic_std if ic_std != 0 else 0
    
    return ic_series, ic_mean, ir

# 使用示例
# ic_series, ic_mean, ir = calc_ic_ir(factor_df, ret_df)
# print(f"IC均值: {ic_mean:.4f}, IR: {ir:.2f}")
实战经验:IC均值在0.02以上就算不错了,0.05以上就是很好的因子。IR大于0.5算及格,大于1算优秀。但要注意,IC的稳定性比绝对值更重要。我见过IC均值0.08但忽正忽负的因子,实盘根本没法用。

最后,我把这四个维度的评价标准整理成一张表,方便你对照:

评价维度 优秀 及格 不合格
分组单调性 严格单调递增/递减 基本单调,有1-2组偏离 杂乱无章,无规律
多空夏普 > 1.5 0.8 - 1.5 < 0.5
IC均值 > 0.05 0.02 - 0.05 < 0.02
IR > 1.0 0.5 - 1.0 < 0.3

嗯,到这里,单因子测试的完整流程就讲完了。从因子定义,到分组回测,再到多空分析和IC/IR分析,每一步都有坑。我当年刚开始做的时候,光数据对齐就折腾了两天。但只要你把框架搭好,后面就是流水线作业了。

专注资料整理