单因子构建与计算:从定义到实战
做量化投资这些年,我最大的体会就是:因子是策略的灵魂。你想想看,没有好的因子,再牛的模型也是白搭。今天我们就来聊聊单因子构建与计算,这是整个因子研究的基础。
说实话,很多新手一上来就追求复杂的因子,结果往往事倍功半。我个人习惯是先把基础打牢,把单因子的构建流程走通,再去想那些花里胡哨的东西。
因子定义与公式化表达
因子是什么?说白了,就是能解释股票收益差异的某种特征。比如市盈率、市净率、动量、波动率等等。每个因子背后都有一个明确的逻辑假设。
举个例子,动量因子。它的逻辑是:过去涨得好的股票,未来一段时间可能继续涨。公式化表达就是:
Momentum = (P_t - P_{t-n}) / P_{t-n}
其中 P_t 是当前价格,P_{t-n} 是 n 天前的价格。就这么简单。
但要注意,因子定义不是随便写的。我在项目中遇到过一个问题:同样的动量因子,用 20 天和用 60 天,效果天差地别。所以定义因子时,一定要明确参数和计算窗口。
核心原则:因子定义要满足三个条件——可计算、可复现、有经济含义。缺一不可。
因子计算实现(Python/Pandas)
有了公式,接下来就是代码实现。我习惯用 Pandas,因为它处理时间序列和截面数据非常方便。
先看一个简单的动量因子计算:
import pandas as pd
import numpy as np
# 假设 df 是包含 'date', 'stock_id', 'close' 的 DataFrame
def calc_momentum(df, window=20):
"""
计算动量因子
"""
df = df.sort_values(['stock_id', 'date'])
df['momentum'] = df.groupby('stock_id')['close'].pct_change(window)
return df
嗯,这里要注意一点:pct_change 默认会向前填充缺失值,但因子计算中我们通常需要对齐到最新日期。我一般会再做一个 shift 操作:
df['momentum'] = df.groupby('stock_id')['close'].pct_change(window).shift(1)
为什么要 shift?因为我们要避免未来信息。你想想看,如果用当天的收益率去预测当天的收益,那不就是作弊吗?
小技巧:计算因子时,建议把中间结果也保存下来。比如计算动量时,同时保存过去 20 天的累计收益率和平均收益率。这样后续分析时能快速定位问题。
因子标准化处理
因子算出来了,但不同因子的量纲不一样。比如市盈率可能是几十倍,而动量可能是百分之几。直接拿来用会出问题。所以我们需要标准化。
常用的标准化方法有三种:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Z-score | (x - μ) / σ | 因子近似正态分布时 |
| 分位数 | rank(x) / N | 因子分布偏态严重时 |
| 排名 | rank(x) | 只需要相对顺序时 |
我个人最常用的是 Z-score,因为它保留了因子的原始分布信息。但要注意,Z-score 对异常值非常敏感。我曾经遇到过一只股票因为停牌后复牌,收益率异常高,导致整个因子的 Z-score 都变形了。
代码实现也很简单:
def zscore_standardize(df, factor_col):
"""
Z-score 标准化
"""
mean = df[factor_col].mean()
std = df[factor_col].std()
df[factor_col + '_zscore'] = (df[factor_col] - mean) / std
return df
def rank_standardize(df, factor_col):
"""
排名标准化
"""
df[factor_col + '_rank'] = df[factor_col].rank(pct=True)
return df
避坑指南:标准化一定要在截面内做,也就是每个时间点单独做。我曾经见过有人把所有时间的数据混在一起标准化,结果完全失去了因子的时序可比性。
因子中性化处理
标准化之后,因子之间可以比较了。但还有一个问题:因子可能受到其他因素的影响。比如市值大的股票,很多因子天然就偏高或偏低。这时候就需要中性化处理。
最常见的两种中性化:市值中性化和行业中性化。
市值中性化,说白了就是把因子中与市值相关的部分剔除掉。做法是用因子对市值做回归,取残差作为中性化后的因子。
import statsmodels.api as sm
def market_neutralize(df, factor_col, market_cap_col):
"""
市值中性化
"""
X = sm.add_constant(df[market_cap_col])
y = df[factor_col]
model = sm.OLS(y, X).fit()
df[factor_col + '_mkt_neutral'] = model.resid
return df
行业中性化更简单,就是减去行业均值。因为不同行业的因子水平天然不同,比如科技股的市盈率普遍比银行股高。
def industry_neutralize(df, factor_col, industry_col):
"""
行业中性化
"""
industry_mean = df.groupby(industry_col)[factor_col].transform('mean')
df[factor_col + '_ind_neutral'] = df[factor_col] - industry_mean
return df
实战经验:我一般会同时做市值和行业中性化。先做行业中性化,再做市值中性化。顺序很重要,因为行业和市值可能有交互效应。
知识体系总览
说了这么多,我们来梳理一下整个流程。下面这张图展示了单因子构建的核心逻辑:
整个流程走下来,你会发现其实并不复杂。但每一步都有细节需要注意。比如因子定义时,参数窗口的选择;计算时,未来信息的避免;标准化时,异常值的处理;中性化时,回归模型的选择。
我记得刚开始做因子研究时,总想着一步到位,结果经常在回测时发现因子失效。后来才明白,基础工作做扎实了,后面的分析才能站得住脚。
我的建议:每次构建新因子,都按照这个流程走一遍。哪怕你觉得某个步骤没必要,也先做一遍看看结果。数据会告诉你真相。
好了,单因子构建与计算就聊到这里。记住,好的因子是反复打磨出来的,不是一蹴而就的。多动手,多思考,你也能做出靠谱的因子。