3. 因子构建基础:数据获取与清洗、因子计算逻辑、因子标准化处理
各位同学好,我是老张。今天咱们聊聊因子构建的底层功夫。说实话,很多做量化的人一上来就搞复杂模型,结果数据源就有问题,那后面全是白搭。我个人习惯,花在数据清洗上的时间,至少占整个因子开发周期的40%。
核心观点:因子构建不是写代码,是处理脏数据。数据干净了,因子就成功了一半。
3.1 数据获取与清洗——地基不牢,地动山摇
数据获取这块,我踩过的坑太多了。早期我用过某免费数据源,结果某天突然发现因子回测收益高得离谱——后来一查,是数据里混入了未来信息。嗯,这教训值十万块。
我个人常用的数据源有几种:
- Wind / Choice:机构常用,数据质量高,但贵
- Tushare / AKShare:开源免费,适合个人研究
- 自建数据库:大团队必备,我习惯用InfluxDB存时序数据
数据清洗这块,我总结了一个「三查」原则:
- 查缺失:停牌、退市、新股,这些都会导致数据缺失。我一般用前向填充,但要注意——如果连续缺失超过5天,直接剔除。
- 查异常:比如某股票一天涨了1000%,这明显是数据错误。我习惯用3倍标准差法做截断。
- 查对齐:不同股票的交易日期可能不一样,必须统一对齐到交易日历。
我的小技巧:数据清洗完,一定要做一次「反向验证」。比如拿复权价格算一下收益率,跟原始收益率对比,差太多就是有问题。
3.2 因子计算逻辑——从原始数据到有效信号
因子计算,说白了就是把原始数据变成有预测能力的信号。我见过太多人直接拿原始指标当因子,比如直接用「市盈率」——这其实不对,因为不同行业的市盈率天然不同,你得做行业中性化处理。
举个例子,咱们算一个经典的「动量因子」:
import pandas as pd
import numpy as np
def calc_momentum_factor(price_df, window=20):
"""
计算动量因子:过去N日累计收益率
"""
# 计算每日收益率
ret = price_df.pct_change()
# 滚动求和
momentum = ret.rolling(window=window).sum()
# 剔除最近1日,避免微观结构噪声
momentum = momentum.shift(1)
return momentum
这里有个细节要注意——为什么要shift(1)?因为我们要避免未来信息。我曾经犯过这个错,回测时因子跟当天收益率相关,结果实盘一塌糊涂。
常见的因子类型我列了个表:
| 因子类型 | 典型因子 | 计算逻辑 | 注意事项 |
|---|---|---|---|
| 动量因子 | 过去N日收益率 | 滚动求和/求平均 | 需剔除最近1日 |
| 价值因子 | BP、EP | 账面价值/市值 | 需做行业中性化 |
| 质量因子 | ROE、毛利率 | 财务指标计算 | 注意财报发布日期 |
| 波动率因子 | 过去N日波动率 | 收益率标准差 | 建议用对数收益率 |
避坑指南:我曾经在计算波动率因子时,直接用简单收益率算标准差。后来发现,对数收益率才是正确的选择——因为对数收益率具有可加性,而且更符合正态分布假设。
3.3 因子标准化处理——让不同因子站在同一起跑线
因子标准化,说白了就是把不同量纲的因子拉到同一个尺度上。你想想看,动量因子的值可能是-0.5到0.5,而市盈率可能是10到100,直接比较肯定不行。
我常用的标准化方法有三种:
- Z-score标准化:减去均值,除以标准差。这是最常用的方法,但要注意——如果因子分布有厚尾,效果会打折扣。
- 排序标准化:把因子值排序,然后映射到0-1之间。这个方法对异常值不敏感,我比较推荐。
- 分位数标准化:把因子值映射到标准正态分布的分位数上。这个方法能保证因子分布接近正态,但计算量稍大。
代码实现如下:
def standardize_factor(factor_df, method='zscore'):
"""
因子标准化处理
"""
if method == 'zscore':
# Z-score标准化
mean = factor_df.mean()
std = factor_df.std()
standardized = (factor_df - mean) / std
elif method == 'rank':
# 排序标准化
rank = factor_df.rank()
standardized = rank / len(factor_df)
elif method == 'quantile':
# 分位数标准化
from scipy.stats import norm
rank = factor_df.rank()
standardized = norm.ppf(rank / (len(factor_df) + 1))
# 截断极端值
standardized = standardized.clip(-3, 3)
return standardized
我的经验:标准化之后一定要做截断。我一般截断到±3倍标准差,这样能避免极端值对后续组合构建的干扰。记住,因子极端值往往不是信号,而是噪声。
3.4 本章知识体系总览
下面这张图是我自己画的,把本章的核心逻辑串起来了。你仔细看看,数据清洗、因子计算、标准化处理,这三步是环环相扣的。
嗯,以上就是本章的全部内容。数据清洗、因子计算、标准化处理,这三步看起来简单,但真正做好需要大量实践。我建议你拿真实数据跑一遍,遇到问题再回来翻翻这节课。