第一章:单因子特征构造——基础统计特征与时间序列变换
做量化这几年,我越来越觉得因子挖掘就像是在跟市场玩一场「找规律」的游戏。你挖到的因子越干净,你的模型就越能听懂市场在说什么。今天咱们就从最基础的单因子特征构造开始聊。
说白了,单因子特征就是从一个原始数据序列里,通过各种数学变换,提取出能反映市场状态的信息。我个人习惯把这类特征分成两大类:截面统计特征和时序变换特征。下面我们一个一个来看。
核心思路:原始价格/成交量数据 → 数学变换 → 有预测力的因子
一、基础统计特征:均值、方差、偏度、峰度
先说说最简单的。均值、方差这些统计量,大家上学时都学过。但在量化里,它们不是用来描述整个数据集的,而是用来描述一个局部窗口内的数据行为。
举个例子。你拿过去20天的收益率算一个均值,这个均值其实就是「动量因子」的雏形。我刚开始做因子研究时,以为越复杂的因子越好,结果回测一跑,还不如一个简单的20日均线。你想想看,有时候最简单的反而是最有效的。
我的经验:偏度和峰度这两个特征,很多人会忽略。但我发现,在极端行情来临前,收益率的偏度往往会先出现异常。比如2015年股灾前,很多股票的日收益率偏度突然转负。这个信号比单纯看波动率要早几天。
import pandas as pd
import numpy as np
# 假设 df 包含股票日收益率,列名为 'return'
def calc_basic_stats(df, window=20):
"""计算基础统计特征"""
result = pd.DataFrame(index=df.index)
# 滚动均值 - 其实就是动量
result['rolling_mean'] = df['return'].rolling(window).mean()
# 滚动方差 - 波动率的一种度量
result['rolling_var'] = df['return'].rolling(window).var()
# 滚动偏度 - 分布不对称性
result['rolling_skew'] = df['return'].rolling(window).skew()
# 滚动峰度 - 尾部风险
result['rolling_kurt'] = df['return'].rolling(window).kurt()
return result
注意:滚动窗口的大小选择很关键。窗口太小(比如5天),噪声太大;窗口太大(比如120天),信号太滞后。我个人习惯先用20天做基准,然后根据因子IC值去调优。
二、滚动窗口特征:让因子「活」起来
滚动窗口特征,说白了就是给每个时间点都算一个「局部统计量」。这样做的好处是,因子能及时反映市场状态的变化。
我记得有一次做CTA策略,用全样本的均值做信号,结果策略在趋势行情里表现很好,一到震荡市就亏成狗。后来改成滚动20日均值,震荡市里信号自动变弱,策略的夏普比率直接翻了一倍。
滚动窗口特征有几个关键参数需要调:
| 参数 | 说明 | 常见取值 |
|---|---|---|
| 窗口大小 | 计算统计量时使用的数据长度 | 5, 10, 20, 60, 120 |
| 步长 | 每次滑动的时间间隔 | 1(日频数据) |
| 最小周期数 | 窗口内最少需要多少有效数据 | 窗口大小的80% |
# 多窗口滚动特征
def multi_window_features(df, windows=[5, 10, 20, 60]):
"""生成多窗口滚动特征"""
result = pd.DataFrame(index=df.index)
for w in windows:
# 滚动均值
result[f'mean_{w}d'] = df['return'].rolling(w).mean()
# 滚动标准差
result[f'std_{w}d'] = df['return'].rolling(w).std()
# 滚动最大值 - 反映近期高点
result[f'max_{w}d'] = df['return'].rolling(w).max()
# 滚动最小值 - 反映近期低点
result[f'min_{w}d'] = df['return'].rolling(w).min()
return result
避坑指南:我曾经犯过一个错误——用未来数据算滚动特征。比如在t时刻,用了t+1到t+20的数据算均值。这在回测里看起来效果很好,但实盘一跑就崩。记住,滚动窗口一定要用过去的数据,不能偷看未来。
三、时序滞后特征:捕捉记忆效应
市场是有记忆的。今天的价格变化,往往会影响明天的交易行为。时序滞后特征就是用来捕捉这种「记忆效应」的。
最简单的做法就是把今天的收益率作为明天的特征。但这里有个问题:滞后多少期合适?
我个人的经验是:
- 滞后1期:捕捉隔夜效应,很多股票有反转现象
- 滞后2-5期:捕捉短期动量或反转
- 滞后10-20期:捕捉中期趋势
def lag_features(df, lags=[1, 2, 3, 5, 10, 20]):
"""生成时序滞后特征"""
result = pd.DataFrame(index=df.index)
for lag in lags:
result[f'return_lag_{lag}'] = df['return'].shift(lag)
return result
注意:滞后特征会导致数据缺失。比如滞后5期,前5行数据就变成了NaN。处理方式有两种:要么直接丢弃,要么用0填充。我个人建议丢弃,因为填充会引入偏差。
四、差分与对数收益率:让数据更「干净」
价格序列通常是非平稳的,直接拿来用会有很多问题。差分和对数收益率就是用来解决这个问题的。
差分:一阶差分就是今天的价格减去昨天的价格。它把价格序列变成了收益序列,去掉了趋势项。
对数收益率:ln(P_t / P_{t-1})。相比简单收益率,对数收益率有几个好处:
- 时间可加性:多期对数收益率可以直接相加
- 分布更接近正态分布
- 数值范围更稳定,不容易出现极端值
def transform_to_returns(price_series):
"""将价格序列转换为收益率特征"""
# 简单收益率
simple_return = price_series.pct_change()
# 对数收益率
log_return = np.log(price_series / price_series.shift(1))
# 一阶差分
diff_1 = price_series.diff(1)
# 二阶差分 - 捕捉加速度
diff_2 = price_series.diff(2)
return pd.DataFrame({
'simple_return': simple_return,
'log_return': log_return,
'diff_1': diff_1,
'diff_2': diff_2
})
我的习惯:在因子挖掘中,我几乎只用对数收益率。原因很简单——它让后续的统计建模更稳定。有一次我用简单收益率做回归,结果残差方差一直在变,换成对数收益率后,问题就解决了。
实战组合:把这些特征串起来
好了,上面讲了四种基础特征构造方法。在实际项目中,我通常会把它们组合起来使用。下面是一个完整的特征工程流水线:
def build_factor_pipeline(df, price_col='close', return_col='return'):
"""完整的单因子特征构造流水线"""
# 1. 先计算收益率
df[return_col] = np.log(df[price_col] / df[price_col].shift(1))
# 2. 基础统计特征
stats = calc_basic_stats(df[[return_col]], window=20)
# 3. 多窗口滚动特征
rolling = multi_window_features(df[[return_col]],
windows=[5, 10, 20, 60])
# 4. 滞后特征
lags = lag_features(df[[return_col]], lags=[1, 2, 3, 5, 10])
# 5. 合并所有特征
all_features = pd.concat([stats, rolling, lags], axis=1)
# 6. 删除缺失值
all_features = all_features.dropna()
return all_features
嗯,到这里,单因子特征构造的基础部分就讲完了。这些方法虽然看起来简单,但它们是整个因子挖掘体系的基石。我见过很多新手一上来就搞深度学习、搞复杂模型,结果连最基本的滚动均值都没算对。你想想看,地基没打好,楼盖得再高也是危房。
下一节我们会聊到多因子组合和特征筛选,到时候这些基础特征就会派上大用场了。
公众号:蓝海资料掘金营,微信deep3321