3. 单因子构建:从零到一搭建你的第一个有效因子

聊到因子构建,我得先跟你掏个底——这活儿看着简单,做起来全是坑。我刚开始做量化那会儿,以为找个指标算一算就完事了,结果回测曲线漂亮得不像话,实盘一跑直接翻车。后来才明白,因子构建不是算数,是门手艺活。

说白了,因子就是能预测股票未来收益的某个特征。比如市盈率、动量、换手率,这些都是因子。但怎么把这些原始数据变成真正能用的因子?这里头有四个关键步骤:构建原则、构建方法、暴露度计算、中性化处理。咱们一个一个来拆。

核心观点: 因子构建的本质,是把原始数据转化为对收益有预测能力的信号。这个过程需要兼顾逻辑性、稳定性和可解释性。

3.1 因子构建原则:别踩这些坑

我总结了几条铁律,你写因子前最好先过一遍:

  • 逻辑要硬——因子背后得有经济学或行为金融学的解释。比如低波动异象,为什么低波动股票反而收益更高?因为散户喜欢追高,把高波动股票价格推上去了,预期收益自然就低了。这个逻辑说得通。
  • 数据要稳——别用未来数据,别用幸存者偏差。我见过有人用"过去一年涨幅"做因子,结果把退市股票全删了,回测效果当然好,实盘直接傻眼。
  • 分布要正常——极端值要处理,不然一个异常值就能把整个组合带偏。我习惯用MAD(中位数绝对偏差)去极值,比3σ稳健得多。
  • 相关性要低——你辛辛苦苦造了10个因子,结果它们之间相关系数0.9,那跟用一个因子没区别。
我的小技巧: 每次构建新因子,我都会先画个分布图看看。如果数据长尾严重,先做截尾处理,再考虑要不要做Box-Cox变换。

3.2 常见因子构建方法:截面回归 vs 时间序列排序

构建方法主要有两种,我分别说说我的使用心得。

3.2.1 截面回归法

截面回归,说白了就是在每个时间截面上,用股票的特征去回归它的收益。比如你想知道"市净率"这个因子有没有用,就把所有股票的当期收益对市净率做回归,看系数是否显著。

这种方法的好处是能同时控制多个变量,比如你可以在回归中加入市值、行业作为控制变量,这样得到的因子暴露就更干净。但缺点也很明显——对异常值敏感,而且假设线性关系,现实往往没那么简单。

# 截面回归示例(Python伪代码)
import statsmodels.api as sm

# 假设 data 包含:return, pb, market_cap, industry_dummy
X = data[['pb', 'market_cap'] + industry_dummy_cols]
y = data['return']

# 加截距项
X = sm.add_constant(X)

# 做截面回归
model = sm.OLS(y, X).fit()
factor_exposure = model.params['pb']  # 这就是pb因子的暴露度
注意: 截面回归容易过拟合。我一般会做滚动窗口验证,比如用过去12个月的数据训练,预测下个月的收益,这样能看出因子是否稳定。

3.2.2 时间序列排序法

这个方法更直观——把股票按某个特征排序,然后分组看收益差异。比如把股票按市盈率从低到高分成10组,看低市盈率组是不是比高市盈率组收益更高。

我个人更喜欢排序法,原因有三:

  • 不依赖线性假设,能捕捉非线性关系
  • 对异常值不那么敏感,排序只看相对位置
  • 结果直观,好跟老板解释

但排序法也有局限——它只能告诉你"高 vs 低"有没有差异,没法精确量化每个股票的因子暴露度。

# 时间序列排序示例
import pandas as pd

# 每月对股票按pe排序
data['pe_rank'] = data.groupby('month')['pe'].rank(pct=True)

# 构建多空组合
long = data[data['pe_rank'] <= 0.2]   # 低市盈率组
short = data[data['pe_rank'] >= 0.8]  # 高市盈率组

# 计算多空收益差
long_ret = long.groupby('month')['return'].mean()
short_ret = short.groupby('month')['return'].mean()
spread = long_ret - short_ret

3.3 因子暴露度计算:你到底暴露了多少?

因子暴露度,就是每只股票在某个因子上的"得分"。比如你构建了一个"动量因子",那每只股票都有一个动量得分,这个得分就是暴露度。

计算暴露度有两种常见做法:

  1. 直接法——标准化后的原始值就是暴露度。比如把市盈率做Z-score标准化,得到的值就是暴露度。简单粗暴,但容易受极端值影响。
  2. 回归法——用时间序列回归得到因子载荷。比如用股票收益对因子收益做回归,回归系数就是暴露度。这种方法更精确,但需要较长的历史数据。

我一般这样处理:先用直接法算一个初步暴露度,然后用回归法做验证。如果两者差异很大,说明数据可能有问题,需要回头检查。

# 直接法计算因子暴露度
from scipy import stats

# 对因子值做横截面标准化
def calc_exposure(factor_values):
    # 去极值(MAD方法)
    median = factor_values.median()
    mad = (factor_values - median).abs().median()
    factor_values = factor_values.clip(median - 5*mad, median + 5*mad)
    
    # Z-score标准化
    exposure = stats.zscore(factor_values)
    return exposure

3.4 因子中性化处理:把干扰项剔除干净

这是最容易踩坑的一步。你想想看,如果某个因子跟市值高度相关,那它到底是因子本身有效,还是因为买小盘股赚了钱?中性化就是为了回答这个问题。

3.4.1 市值中性化

市值是A股最强的风格因子,没有之一。我做过统计,A股中超过60%的因子都跟市值有显著相关性。所以市值中性化几乎是必做的一步。

做法很简单:把因子暴露度对市值做回归,取残差作为中性化后的暴露度。

# 市值中性化
def market_neutralize(exposure, market_cap):
    # 对市值取对数,更符合正态分布
    log_mkt = np.log(market_cap)
    
    # 回归取残差
    X = sm.add_constant(log_mkt)
    model = sm.OLS(exposure, X).fit()
    neutral_exposure = model.resid
    
    return neutral_exposure
避坑指南: 我曾经犯过一个错误——先中性化再标准化。结果中性化后的残差均值不为0,标准化后反而引入了偏差。正确的顺序是:先去极值,再中性化,最后标准化。

3.4.2 行业中性化

行业中性化道理一样——不同行业的股票天然有差异。比如银行股PE普遍低,消费股PE普遍高,如果不做行业中性化,你可能会误以为"低PE因子"有效,其实只是因为你重仓了银行股。

行业中性化通常用虚拟变量回归:

# 行业中性化
def industry_neutralize(exposure, industry_dummies):
    # industry_dummies 是one-hot编码的行业哑变量
    X = sm.add_constant(industry_dummies)
    model = sm.OLS(exposure, X).fit()
    neutral_exposure = model.resid
    
    return neutral_exposure

实际操作中,我一般会把市值和行业一起中性化,一步到位:

# 市值+行业联合中性化
def joint_neutralize(exposure, market_cap, industry_dummies):
    log_mkt = np.log(market_cap)
    X = pd.concat([pd.Series(log_mkt, name='log_mkt'), industry_dummies], axis=1)
    X = sm.add_constant(X)
    
    model = sm.OLS(exposure, X).fit()
    neutral_exposure = model.resid
    
    return neutral_exposure
注意: 行业分类不宜过细,也不宜过粗。我一般用申万一级行业(28个),再细的话样本量不够,回归结果不稳定。另外,有些行业股票太少(比如综合类),我会把它们合并到"其他"类别里。

3.5 本章知识体系总览

说了这么多,我画了张图帮你梳理一下整个因子构建的流程。你一看就明白了:

单因子构建完整流程 原始数据 构建原则:逻辑硬、数据稳、分布正、相关性低 构建方法:截面回归法 vs 时间序列排序法 暴露度计算:直接法 / 回归法 中性化处理:市值中性化 + 行业中性化 每一步都环环相扣,跳过任何一步都可能让因子失效

嗯,以上就是单因子构建的全部核心内容。从原则到方法,从暴露度到中性化,每一步都有讲究。我刚开始做的时候也踩过不少坑,但只要你按照这个流程走下来,至少能保证你的因子是"干净"的。

记住一句话:因子构建不是终点,而是起点。一个干净的因子,才能为后续的组合优化打下坚实基础。


专注资料整理