3、单因子构建基础:因子定义与逻辑、因子计算函数封装、因子值标准化与中性化处理
各位同学,今天咱们聊聊单因子构建。说实话,这是整个量化策略的基石。你因子挖得不好,后面再怎么调参、再怎么风控,都是白搭。我见过太多人一上来就搞复杂模型,结果因子本身逻辑都说不通,那不就是空中楼阁吗?
这一节,我会带着大家从最基础的东西开始。因子定义、计算函数封装、标准化和中性化处理。嗯,每一步都有坑,我会把我在实战中踩过的雷都告诉你。
3.1 因子定义与逻辑:别小看这一步
什么是因子?说白了,因子就是能预测股票未来收益的某种特征。比如市盈率、动量、换手率,这些都是因子。但关键不在于你选什么因子,而在于你怎么定义它。
我个人习惯,定义因子前先问自己三个问题:
- 这个因子有经济含义吗? 比如“过去5日涨幅”,它代表短期动量。这说得通。
- 数据源可靠吗? 你用的行情数据、财务数据,有没有幸存者偏差?
- 计算逻辑是否可复现? 换一个人用同样的数据,能不能算出同样的结果?
举个例子。假设我们要定义一个“日内振幅因子”。逻辑很简单:
# 因子定义:日内振幅 = (最高价 - 最低价) / 前日收盘价
def amplitude_factor(high, low, prev_close):
return (high - low) / prev_close
你看,这个因子逻辑清晰,数据也好拿。但我在项目中遇到过一个问题:如果前日收盘价为0怎么办?比如新股上市第一天。嗯,这里要注意,必须做异常值处理。
3.2 因子计算函数封装:写一次,用一辈子
你想想看,如果你每次计算因子都重新写一遍代码,那效率得多低?我建议把因子计算封装成函数,参数化、模块化。这样不仅方便复用,也便于后续的因子测试和组合。
这里我给出一个通用的因子计算框架:
import pandas as pd
import numpy as np
class FactorCalculator:
def __init__(self, data):
"""
data: DataFrame, 必须包含 columns=['open','high','low','close','volume','returns']
"""
self.data = data.copy()
def calc_momentum(self, window=5):
"""计算动量因子:过去N日收益率"""
self.data['momentum'] = self.data['close'].pct_change(window)
return self.data
def calc_volatility(self, window=20):
"""计算波动率因子:过去N日收益率标准差"""
self.data['volatility'] = self.data['returns'].rolling(window).std()
return self.data
def calc_turnover(self, window=5):
"""计算换手率因子:过去N日平均换手率"""
# 假设 volume 是成交额,close 是收盘价,流通市值需要外部传入
self.data['turnover'] = self.data['volume'] / self.data['market_cap']
self.data['avg_turnover'] = self.data['turnover'].rolling(window).mean()
return self.data
这个类的好处是什么?你只需要传入原始数据,然后调用对应的方法,因子就自动算好了。我建议每个因子都单独写一个方法,这样后期调试也方便。
3.3 因子值标准化:让不同因子站在同一起跑线
因子值标准化,说白了就是把不同量纲的因子拉到同一个尺度上。比如市盈率可能是几十,而换手率可能是百分之零点几。如果不标准化,你没法直接比较。
常用的标准化方法有三种:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Z-score 标准化 | (x - μ) / σ | 因子近似正态分布时 |
| Min-Max 标准化 | (x - min) / (max - min) | 因子值有明确边界时 |
| Rank 标准化 | 排序后映射到 [0,1] | 因子分布偏态严重时 |
我个人最常用的是 Z-score 标准化。为什么?因为它保留了因子值的相对大小关系,而且对异常值有一定的容忍度。但要注意,如果因子值本身有严重的厚尾分布,Z-score 效果会变差。
def zscore_standardize(factor_series):
"""Z-score 标准化"""
mean = factor_series.mean()
std = factor_series.std()
if std == 0:
return factor_series * 0 # 如果标准差为0,全部置0
return (factor_series - mean) / std
def rank_standardize(factor_series):
"""Rank 标准化:排序后映射到 [0,1]"""
rank = factor_series.rank()
return (rank - 1) / (len(rank) - 1)
3.4 因子中性化处理:剔除你不想要的风格
因子中性化,是量化投资里一个非常重要的概念。说白了,就是剔除因子中与某些已知风格(如市值、行业)相关的部分。为什么要这么做?
- 避免风格暴露: 比如你的因子在小市值股票上表现好,但这不是因子本身的能力,而是小市值风格在起作用。
- 提高因子纯度: 中性化后,因子剩下的部分才是真正的“alpha”。
- 便于组合管理: 中性化后的因子更容易与其他因子组合,不会产生多重共线性。
最常用的中性化方法是回归法。具体来说,就是用因子值对市值、行业等变量做线性回归,取残差作为中性化后的因子值。
import statsmodels.api as sm
def neutralize_factor(factor_series, market_cap, industry_dummies):
"""
对因子进行市值和行业中性化处理
factor_series: 因子值
market_cap: 市值(对数化)
industry_dummies: 行业哑变量(One-Hot编码)
"""
# 合并自变量
X = pd.concat([np.log(market_cap), industry_dummies], axis=1)
X = sm.add_constant(X) # 添加截距项
# 回归
model = sm.OLS(factor_series, X).fit()
# 残差即为中性化后的因子
neutralized_factor = model.resid
return neutralized_factor
我在项目中遇到过一个问题:如果行业哑变量太多,会导致多重共线性。比如有30个行业,你放29个哑变量就够了,放30个就会出问题。嗯,这里要注意,一定要去掉一个行业作为基准。
3.5 本章知识体系总览
说了这么多,我画了一张图,帮你把这一节的核心逻辑串起来。你一看就明白了。
你看,整个流程其实就四步:定义、计算、标准化、中性化。每一步都有它的意义,缺一不可。我见过有人跳过中性化直接做回测,结果因子收益全是风格暴露带来的,一换市场环境就失效了。
核心要点回顾:
- 因子定义要有经济含义,数据源要可靠,逻辑要可复现
- 因子计算函数要封装成类或模块,便于复用和测试
- 标准化要在截面上做,常用 Z-score 或 Rank 方法
- 中性化用回归法,剔除市值和行业等风格暴露
- 每一步都要做异常值处理,避免 NaN 或无穷值
好了,这一节的内容就到这里。记住,因子构建是量化策略的根基。根基不牢,地动山摇。下一节我们会讲因子测试与 IC 分析,到时候你会看到,今天做的这些标准化和中性化处理,在 IC 计算中会发挥多大的作用。