4. 单因子分析:因子计算与实现、因子IC分析、因子分组回测、因子收益与夏普比率
好,我们进入单因子分析这个核心环节。说实话,很多做量化的人,策略亏钱往往不是因为模型不够复杂,而是连最基础的因子分析都没做扎实。我个人习惯是,拿到一个新因子,先把它拆成四个步骤来审视:因子怎么算、IC怎么样、分组回测表现如何、最终收益和风险比划不划得来。
4.1 因子计算与实现
因子计算,说白了就是把你的投资逻辑变成一串数字。比如你相信“低市盈率”的股票能涨,那你就得把市盈率算出来,然后对齐到每个股票、每个时间点。
我见过不少新手,因子算出来之后直接拿去跑回测,结果发现信号全是未来函数。嗯,这里要注意:因子计算最怕的就是“未来信息泄露”。
举个简单的例子,假设我们要计算一个“过去5日动量因子”:
import pandas as pd
import numpy as np
def calc_momentum_factor(price_df, window=5):
"""
计算过去N日的动量因子
price_df: DataFrame, index为日期, columns为股票代码
"""
# 计算收益率
ret = price_df.pct_change()
# 滚动求和,注意shift(1)防止未来数据
momentum = ret.rolling(window=window).sum().shift(1)
return momentum
# 使用示例
# prices = pd.read_csv('daily_prices.csv', index_col=0, parse_dates=True)
# factor = calc_momentum_factor(prices, window=5)
看到那个 .shift(1) 了吗?这就是避坑的关键。我曾经有一次忘记加这个shift,结果回测年化收益直接翻倍,当时还高兴了半天,后来发现是用了当天的数据去预测当天的涨跌——这不就是作弊嘛。
4.2 因子IC分析(信息系数)
因子算好了,怎么知道它有没有预测能力?IC(Information Coefficient)就是干这个的。IC衡量的是因子值与未来收益之间的相关性。
常用的IC有两种:
- Pearson IC: 线性相关系数,假设因子和收益是线性关系
- Spearman Rank IC: 秩相关系数,更稳健,对异常值不敏感
我个人更偏爱Rank IC。为什么?因为因子值本身的大小可能受极端值影响,但排序关系相对稳定。你想想看,一个因子如果能把股票从好到坏排个序,那它的价值就已经很大了。
def calc_rank_ic(factor_series, forward_ret_series):
"""
计算截面Rank IC
factor_series: 某一天的因子值(所有股票)
forward_ret_series: 对应的未来N日收益
"""
# 计算秩相关系数
ic = factor_series.rank().corr(forward_ret_series.rank())
return ic
# 计算时间序列IC
def calc_ic_ts(factor_df, ret_df):
"""
计算每日IC,返回IC时间序列
"""
dates = factor_df.index
ic_list = []
for date in dates:
ic = calc_rank_ic(factor_df.loc[date], ret_df.loc[date])
ic_list.append(ic)
return pd.Series(ic_list, index=dates)
IC算出来之后,我通常会看几个指标:
- IC均值: 大于0.02就算不错了,大于0.05就是好因子
- IC标准差: 越小越好,说明因子稳定
- IR(信息比率): IC均值 / IC标准差,大于0.5算合格
4.3 因子分组回测
IC分析告诉你因子有没有预测能力,但分组回测告诉你的是——这个能力能不能转化成真金白银。
分组回测的逻辑很简单:每个月(或每天)按因子值把股票分成N组,然后看每组未来的收益表现。如果第1组(因子值最大)的收益显著高于第N组(因子值最小),那这个因子就有区分度。
def group_backtest(factor_df, ret_df, n_groups=5):
"""
因子分组回测
返回每组的时间序列收益
"""
dates = factor_df.index
group_returns = {i: [] for i in range(1, n_groups+1)}
for date in dates:
# 获取当天的因子值和未来收益
factors = factor_df.loc[date].dropna()
returns = ret_df.loc[date]
# 按因子值分组
grouped = pd.qcut(factors, n_groups, labels=False) + 1
# 计算每组平均收益
for group in range(1, n_groups+1):
stocks_in_group = grouped[grouped == group].index
group_ret = returns[stocks_in_group].mean()
group_returns[group].append(group_ret)
return pd.DataFrame(group_returns, index=dates)
分组回测的结果,我一般会画成折线图。理想的情况是:各组收益曲线从高到低整齐排列,而且第1组和第N组之间有明显差距。
4.4 因子收益与夏普比率
最后一步,也是最实际的一步:这个因子到底能赚多少钱?风险有多大?
我们通常用“多空组合”来评估因子收益。具体做法是:做多第1组(因子值最大的组),做空第N组(因子值最小的组),然后看这个组合的收益表现。
def calc_factor_return(group_returns_df):
"""
计算多空组合收益
假设第1组是多头,第N组是空头
"""
long_ret = group_returns_df.iloc[:, 0] # 第1组
short_ret = group_returns_df.iloc[:, -1] # 最后一组
factor_ret = long_ret - short_ret
return factor_ret
def calc_sharpe(return_series, rf=0.0):
"""
计算夏普比率
"""
excess_ret = return_series.mean() - rf
std = return_series.std()
sharpe = excess_ret / std * np.sqrt(252) # 年化
return sharpe
夏普比率大于1,说明这个因子不错;大于2,那就是相当优秀了。但我要提醒你,回测中的夏普比率往往偏高,实盘能打个六折就不错了。
我记得有一次,我开发了一个因子,回测夏普高达3.5,当时兴奋得不行。结果实盘跑了三个月,夏普直接掉到0.8。后来复盘发现,是因为回测时没考虑交易成本和冲击成本。所以,因子收益分析一定要加上交易成本假设,不然就是自欺欺人。
- 因子计算:注意未来函数,做好去极值和标准化
- IC分析:用Rank IC更稳健,关注IR而非单纯IC均值
- 分组回测:看各组收益是否单调排列,这是因子有效性的直观证据
- 收益与夏普:多空组合收益是核心,夏普比率要打折扣看
好了,单因子分析这一套流程走下来,你基本就能判断一个因子是“宝贝”还是“垃圾”了。别嫌麻烦,这一步做扎实了,后面的多因子组合才能站得住脚。