第1章:多因子交叉特征——从加减乘除到条件因子

大家好,我是老张。今天咱们聊聊因子挖掘里最基础也最实用的技巧——多因子交叉特征。

说实话,我刚开始做量化那会儿,也迷信过复杂的机器学习模型。后来发现,真正赚钱的策略,往往就靠几个精心构造的交叉特征。嗯,这章我就把压箱底的经验掏出来。

1.1 因子间的加减乘除组合

先问个问题:为什么要把因子组合起来?

单个因子就像一把螺丝刀,能拧螺丝但修不了车。组合因子就像工具箱,能应对复杂市场。我个人习惯,拿到新数据先做三件事:加、减、乘、除。

加法与减法

加法适合捕捉「共振」信号。比如动量因子加上成交量因子,能过滤掉无量上涨的假突破。

减法更常用。我记得有个项目,用「短期动量减去长期动量」构造了一个反转因子,效果比单独用任何一个都好。

# 加减法示例
import pandas as pd
import numpy as np

# 假设已有因子数据
df['momentum_short'] = df['close'].pct_change(5)
df['momentum_long'] = df['close'].pct_change(20)

# 差值因子:短期相对长期的超额动量
df['momentum_diff'] = df['momentum_short'] - df['momentum_long']

# 和值因子:综合动量强度
df['momentum_sum'] = df['momentum_short'] + df['momentum_long']

乘法与除法

乘法能放大信号。比如「换手率 × 波动率」可以识别出异常活跃的股票。

除法更讲究。我做过一个避坑指南:分母不能为0。曾经有个实习生直接用原始数据做除法,结果一堆inf,回测曲线直接崩了。

⚠️ 避坑指南: 做除法前一定要加epsilon(极小值),或者用np.where处理零值。我曾经因为没处理这个,回测结果虚高,实盘直接打脸。
# 乘除法示例
epsilon = 1e-8

# 乘法:波动率调整后的动量
df['volatility'] = df['close'].pct_change().rolling(20).std()
df['momentum_vol_adj'] = df['momentum_short'] * df['volatility']

# 除法:换手率标准化(加epsilon防除零)
df['turnover'] = df['volume'] / df['shares_outstanding']
df['turnover_norm'] = df['turnover'] / (df['turnover'].rolling(60).mean() + epsilon)

1.2 条件因子:高动量且低波动

条件因子说白了就是「如果...那么...」的逻辑。我最常用的场景是:高动量且低波动

为什么?高动量说明趋势强,低波动说明风险可控。两者结合,胜率能提高不少。

# 条件因子示例
# 定义条件:动量排名前30%,波动排名后30%
momentum_rank = df['momentum_short'].rank(pct=True)
volatility_rank = df['volatility'].rank(pct=True)

# 条件因子:高动量(前30%)且低波动(后30%)
df['high_mom_low_vol'] = np.where(
    (momentum_rank > 0.7) & (volatility_rank < 0.3),
    1,  # 满足条件
    0   # 不满足
)
💡 个人经验: 条件因子的阈值不要拍脑袋。我习惯用滚动分位数,比如过去60天的80%分位。这样能自适应市场环境变化。

1.3 因子正交化处理

因子之间往往有相关性。比如动量因子和换手率因子,经常高度相关。如果不处理,模型会学偏。

正交化的核心思想:把相关因子变成独立因子。我最常用的是施密特正交化。

# 施密特正交化示例
from sklearn.linear_model import LinearRegression

def orthogonalize(factor_a, factor_b):
    """
    将factor_b对factor_a做正交化
    返回:与factor_a正交的factor_b_residual
    """
    model = LinearRegression()
    model.fit(factor_a.values.reshape(-1, 1), factor_b)
    predicted = model.predict(factor_a.values.reshape(-1, 1))
    residual = factor_b - predicted
    return residual

# 使用:将换手率对动量做正交化
df['turnover_orth'] = orthogonalize(df['momentum_short'], df['turnover'])

你想想看,正交化后的因子,每个都携带独立信息。做多因子模型时,不会出现「两个因子其实是一回事」的尴尬。

1.4 因子标准化与中性化

标准化和中性化,是因子处理的两大基石。我见过太多人跳过这步,结果模型一塌糊涂。

标准化:让因子可比

不同因子的量纲不同。比如动量可能是0.05,波动率可能是0.3。直接放一起,模型会偏向数值大的因子。

# Z-score标准化
def zscore_standardize(series):
    return (series - series.mean()) / series.std()

# 或者用MAD(中位数绝对偏差),更稳健
def mad_standardize(series):
    median = series.median()
    mad = np.median(np.abs(series - median))
    return (series - median) / (mad + epsilon)

df['momentum_z'] = zscore_standardize(df['momentum_short'])
df['volatility_z'] = mad_standardize(df['volatility'])
🔑 关键点: 标准化要在截面(同一时间点)上做,而不是在时序上。否则会引入未来信息,造成过拟合。

中性化:剔除干扰因素

中性化是剔除因子中「不想要」的成分。最常见的是市值中性化和行业中性化。

举个例子:小市值股票天然波动大。如果你不做市值中性化,波动率因子其实包含了市值信息。

# 市值中性化示例
def market_cap_neutralize(factor, market_cap):
    """
    对因子做市值中性化
    返回:剔除市值影响后的残差
    """
    model = LinearRegression()
    model.fit(np.log(market_cap).values.reshape(-1, 1), factor)
    predicted = model.predict(np.log(market_cap).values.reshape(-1, 1))
    residual = factor - predicted
    return residual

# 使用
df['volatility_neutral'] = market_cap_neutralize(
    df['volatility'], 
    df['market_cap']
)

我曾经犯过一个错:先标准化再中性化,结果顺序搞反了。标准化应该在最后一步做,因为中性化会改变因子的分布。

知识体系总览

下面这张图,是我自己总结的交叉特征处理流程。你照着这个顺序做,基本不会出错。

多因子交叉特征处理流程 原始因子数据 因子预处理 标准化 中性化 正交化 交叉特征构造(加减乘除/条件) 最终因子库

这张图展示了完整的处理链条:原始因子 → 预处理(标准化/中性化/正交化)→ 交叉特征构造 → 最终因子库。每一步都不可或缺。

实战中的避坑总结

最后,我把自己踩过的坑整理成一张表,你写代码时对照着检查:

操作 常见错误 正确做法
除法 分母为0导致inf 加epsilon或np.where处理
条件因子 固定阈值不适应市场 用滚动分位数动态设定
正交化 忘记检查正交性 计算相关系数矩阵验证
标准化 在时序上做标准化 必须在截面上做
中性化 先标准化再中性化 先中性化,最后标准化

嗯,这章的内容就这些。记住一句话:好的交叉特征,胜过十个平庸的单因子。下一章咱们聊时间序列特征,到时候见。


公众号:蓝海数据掘金营,微信 deep3321