第四章:因子构建与计算
因子构建,说白了就是把你脑子里的投资逻辑,变成一串可计算的数字。我做了这么多年量化,见过太多人拿着漂亮的理论模型,结果一算因子全是噪音。今天咱们就聊聊,怎么把因子从想法落地到代码。
4.1 因子计算公式:从逻辑到数学
每个因子背后都有一个故事。比如动量因子,逻辑是“过去涨的股票未来还会涨”。那怎么算?最简单的就是过去N天的收益率。
# 动量因子:过去20天累计收益率
def momentum_factor(close_prices, window=20):
return close_prices / close_prices.shift(window) - 1
我个人习惯,写因子公式前先问自己三个问题:
- 这个因子是截面比较,还是时间序列比较? 截面比较看相对强弱,时间序列看自身变化。
- 数据频率是什么? 日频、周频还是月频?不同频率噪音水平差很多。
- 有没有行业或市值偏见? 很多因子天然偏向小盘股,后面我们会处理。
举个例子,我做过一个“毛利率变化因子”。公式很简单:
# 毛利率变化 = 本期毛利率 - 上期毛利率
gross_margin_change = (revenue - cost) / revenue - prev_gross_margin
但注意,这里有个坑。毛利率变化在行业间不可比——科技公司毛利率80%,零售公司才20%。直接拿原始值做截面比较,结果全是行业效应。嗯,这就是为什么我们需要标准化和中性化。
4.2 标准化处理:让因子站在同一起跑线
标准化,就是把不同量纲的因子拉到同一个尺度。我最常用的两种方法:Z-score和分位数。
4.2.1 Z-score标准化
Z-score的公式很简单:
Z = (X - μ) / σ
其中μ是均值,σ是标准差。处理后的因子均值为0,标准差为1。
我在项目中遇到过一个问题:因子值有极端异常点。比如某只股票因为数据错误,因子值跑到100个标准差之外。这时候Z-score会被严重扭曲。怎么办?
# 带极端值处理的Z-score
def robust_zscore(factor, cap=5):
median = factor.median()
mad = (factor - median).abs().median()
# 用MAD替代标准差,更稳健
z = (factor - median) / (mad * 1.4826)
# 截断极端值
return z.clip(-cap, cap)
4.2.2 分位数标准化
分位数标准化,说白了就是把因子值映射到0到1之间。我比较喜欢用rank分位数:
# 分位数标准化
def rank_quantile(factor, n_quantiles=5):
rank = factor.rank()
return (rank - 1) / (len(factor) - 1)
你想想看,分位数标准化的好处是什么?它对异常值不敏感。不管你的因子值多大,排名第1就是1,排名最后就是0。但代价是丢失了因子值之间的间距信息。
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Z-score | 保留间距信息 | 对异常值敏感 | 因子值分布较正态 |
| 分位数 | 稳健,不受异常值影响 | 丢失间距信息 | 因子有极端值 |
4.3 中性化处理:剥离干扰因素
中性化,是因子构建中最容易被忽视的一步。我见过太多人,因子跑出来IC很高,结果一分析,全是行业和市值在起作用。
4.3.1 市值中性化
市值中性化的逻辑很简单:把因子中对市值的暴露剥离掉。方法是用因子值对市值做回归,取残差。
# 市值中性化:回归取残差
import statsmodels.api as sm
def market_cap_neutralize(factor, market_cap):
X = sm.add_constant(np.log(market_cap))
model = sm.OLS(factor, X).fit()
residual = model.resid
return residual
为什么要取对数市值?因为市值分布是右偏的,取对数后更接近正态分布。这是我踩过坑才学到的——直接用原始市值做回归,小市值股票会被过度调整。
4.3.2 行业中性化
行业中性化更直接:在每个行业内,把因子值减去行业均值。
# 行业中性化:组内去均值
def industry_neutralize(factor, industry):
return factor - factor.groupby(industry).transform('mean')
我个人习惯,做行业中性化时用申万一级行业。但要注意,有些行业股票太少(比如综合行业),中性化后因子值可能不稳定。我曾经遇到过,某个行业只有3只股票,中性化后因子值完全失去了区分度。
4.4 综合处理流程:一个完整的例子
好了,咱们把上面这些串起来。假设我们要构建一个“市盈率倒数因子”(EP因子):
# 完整因子处理流程
def factor_pipeline(data):
# 1. 原始因子计算
data['EP'] = data['earnings'] / data['market_cap']
# 2. 极端值处理(MAD截断)
median = data['EP'].median()
mad = (data['EP'] - median).abs().median()
data['EP_clipped'] = data['EP'].clip(
lower=median - 5 * mad * 1.4826,
upper=median + 5 * mad * 1.4826
)
# 3. 市值中性化
X = sm.add_constant(np.log(data['market_cap']))
model = sm.OLS(data['EP_clipped'], X).fit()
data['EP_neutral'] = model.resid
# 4. 行业中性化
data['EP_final'] = data['EP_neutral'] - \
data.groupby('industry')['EP_neutral'].transform('mean')
# 5. Z-score标准化
data['EP_zscore'] = (data['EP_final'] - data['EP_final'].mean()) / \
data['EP_final'].std()
return data
这个流程,我建议你每次构建因子都走一遍。顺序很重要:先处理极端值,再做中性化,最后标准化。为什么?因为极端值会影响回归系数的估计,所以要先处理掉。
4.5 本章知识体系
下面这张图,是我自己总结的因子构建流程。每次做新因子,我都会对着这张图检查一遍:
这张图我贴在公司工位上好几年了。每次构建新因子,我都会对着它走一遍流程。你可能会觉得麻烦,但相信我,这一步省了,后面回测出问题更麻烦。
好了,这一章的内容就到这里。因子构建是量化投资的基石,花时间把这一步做扎实,后面的组合优化才能站得住脚。
公众号:蓝海数据掘金营,微信deep3321