4、因子计算引擎:因子定义规范、向量化计算、因子去极值、因子标准化、中性化处理
好,咱们进入正题。因子计算引擎,说白了就是整个回测系统的心脏。你前面数据清洗得再漂亮,因子算不对,一切都是白搭。我见过太多人在这块翻车了——因子定义不规范,算出来的结果自己都不敢信。
今天我把这块掰开揉碎了讲。四个核心环节:因子定义规范、向量化计算、去极值、标准化和中性化。嗯,咱们一个一个来。
4.1 因子定义规范:先把规矩立好
我个人习惯,写因子之前先定规范。没有规矩不成方圆,尤其在团队协作里,你写一个因子,别人看不懂,那就废了。
一个标准的因子定义,至少包含以下字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| 因子名称 | 英文标识,建议用下划线分隔 | pe_ttm |
| 因子中文名 | 方便沟通 | 滚动市盈率 |
| 计算逻辑 | 伪代码或公式 | 总市值 / 近12个月净利润 |
| 依赖数据 | 需要哪些原始字段 | close, net_profit |
| 输出范围 | 理论取值范围 | (-∞, +∞) |
| 处理方式 | 是否需要去极值/标准化 | 是 |
举个例子,我定义一个动量因子:
# 因子定义示例
factor_name: 'momentum_20d'
chinese_name: '20日动量'
formula: 'close / shift(close, 20) - 1'
dependencies: ['close']
output_range: [-1, +∞)
need_winsorize: True
need_standardize: True
need_neutralize: False
为什么要这么细致?因为因子计算最怕歧义。我曾经在项目里遇到一个同事,他算的"市盈率"和我算的差了10倍——后来发现他用的是总股本,我用的是流通股本。这种坑,规范能帮你避开一大半。
4.2 向量化计算:别写循环,求你了
很多新手写因子,喜欢用for循环。股票3000只,时间2000天,你一个循环下去,电脑能跑冒烟。
正确的做法是向量化计算。用pandas或numpy的数组运算,一次搞定所有数据。
来看个对比:
# 错误示范:循环计算
def calc_momentum_loop(df):
result = []
for i in range(20, len(df)):
ret = df['close'].iloc[i] / df['close'].iloc[i-20] - 1
result.append(ret)
return result
# 正确示范:向量化计算
def calc_momentum_vectorized(df):
return df['close'] / df['close'].shift(20) - 1
看到了吗?向量化版本就一行代码。而且速度差距是数量级的——3000只股票2000天数据,循环可能要跑几分钟,向量化几秒钟搞定。
为什么会这样?因为pandas底层用的是C语言和numpy,向量化操作直接调用这些高效实现。你写个Python循环,每步都要解释器处理,自然慢。
4.3 因子去极值:别让异常值毁了你的因子
因子算出来了,但你会发现有些值特别离谱。比如某只股票因为停牌,20日收益算出来是1000%。这种极端值如果不处理,后面标准化、回归分析全都会被带偏。
常用的去极值方法有三种:
- MAD法(中位数绝对偏差):稳健,不受极端值影响
- 百分位法:直接砍掉头尾一定比例
- 标准差法:超过n倍标准差的值截断
我个人最常用的是MAD法,因为它对异常值不敏感。来看代码:
def winsorize_mad(series, n=5):
"""
MAD法去极值
n: 阈值倍数,通常取3-5
"""
median = series.median()
mad = (series - median).abs().median()
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
我曾经遇到过一个案例:某因子在2015年股灾期间,因为几只股票连续跌停,因子值直接崩到-999%。如果不做去极值,整个回测结果都是错的。做了MAD处理后,这些异常值被限制在合理范围内,回测结果才恢复正常。
4.4 因子标准化:让不同因子站在同一起跑线
因子去完极值,接下来要标准化。为什么?因为不同因子的量纲不一样。比如市盈率可能是几十,换手率可能是百分之几,你直接拿去做回归,量纲大的因子天然占优势。
标准化的目标是把因子值映射到均值为0、标准差为1的分布上。最常用的就是Z-score标准化:
def standardize_zscore(series):
"""
Z-score标准化
"""
return (series - series.mean()) / series.std()
标准化之后,所有因子的均值都是0,标准差都是1。这样你在做多因子组合时,每个因子的权重才有意义。
嗯,这里有个细节要注意:标准化也是在截面上做的。每天对所有股票的因子值做标准化,而不是对某只股票的历史数据做。原因很简单——我们关心的是股票在当天的相对位置,而不是它自己的历史位置。
4.5 中性化处理:剔除你不想要的风格
中性化是因子处理里最容易被忽视的一步,但也是最重要的一步之一。
什么叫中性化?说白了,就是剔除因子中与某些风格变量相关的部分。最常见的做法是对市值和行业做中性化。
举个例子:你发现小市值股票涨得好,但你真正想研究的因子是"动量"。如果不做市值中性化,动量因子里可能混入了市值效应——小市值股票动量强,大市值股票动量弱,你分不清收益是来自动量还是来自市值。
中性化的做法很简单:用因子值对市值和行业做回归,取残差作为新的因子值。
def neutralize_factor(factor, market_cap, industry_dummies):
"""
因子中性化处理
factor: 待处理的因子值
market_cap: 市值(取对数)
industry_dummies: 行业哑变量
"""
import statsmodels.api as sm
X = pd.concat([market_cap, industry_dummies], axis=1)
X = sm.add_constant(X)
model = sm.OLS(factor, X).fit()
residual = factor - model.predict(X)
return residual
这样处理之后,因子值中与市值和行业相关的部分被剥离了,剩下的就是纯粹的因子效应。
我自己的经验是:大部分因子都应该至少做行业中性化。因为行业之间的差异太大了,银行股的市盈率和科技股的市盈率完全没有可比性。不做行业中性化,你算出来的因子可能只是行业暴露的代理变量。
4.6 完整流程:从原始数据到干净因子
好了,四个步骤都讲完了。咱们串起来看一个完整的因子处理流程:
def factor_pipeline(raw_factor, market_cap, industry_dummies):
"""
因子处理完整流水线
"""
# 1. 去极值
clean_factor = winsorize_mad(raw_factor, n=5)
# 2. 标准化
std_factor = standardize_zscore(clean_factor)
# 3. 中性化(可选)
if market_cap is not None and industry_dummies is not None:
final_factor = neutralize_factor(std_factor, market_cap, industry_dummies)
else:
final_factor = std_factor
return final_factor
这个流水线,每天对截面数据跑一遍。日积月累,你就得到了一个干净、可用、没有偏误的因子序列。
最后,我用一张图来总结本章的知识体系:
这张图把整个流程串起来了。从原始因子数据开始,经过去极值、标准化、中性化三步处理,最终得到干净可用的因子。每一步都有明确的数学含义和业务目的。
好了,因子计算引擎的核心内容就这些。记住:规范定义是基础,向量化计算是效率,去极值和标准化是常规操作,中性化是进阶技巧。把这四步吃透了,你的因子质量就能超过90%的量化团队。