3. 数据准备与清洗:如何获取、清洗和预处理股票/期货数据

做因子回测,最怕什么?

不是策略亏钱,而是数据有坑。我见过太多人,因子算出来漂亮得很,一上实盘就崩。查到最后,发现是数据里藏了几个脏点。嗯,今天我们就聊聊数据准备这件事。

3.1 数据获取:从哪里来,拿什么用

数据源的选择,直接决定了你后续工作的质量。我个人习惯把数据源分成三类:

类型 代表 优点 缺点
免费公开 Tushare、AKShare、Yahoo Finance 零成本,上手快 稳定性差,字段不全
商业数据 Wind、Choice、Bloomberg 质量高,字段丰富 贵,年费几万起
自建采集 爬虫抓取交易所公告 定制化强 维护成本高

我在项目中遇到过最头疼的事:用免费数据跑回测,夏普比率做到2.8,换到商业数据一测,直接掉到1.2。为什么?免费数据里缺失了分红除权的调整,复权价格算错了。

警告: 千万别迷信免费数据。做严肃的因子研究,至少准备两套数据源做交叉验证。

3.2 数据清洗:脏数据是因子杀手

数据拿到手,第一件事不是算因子,而是洗数据。我总结了一套「四步清洗法」:

3.2.1 缺失值处理

股票停牌、期货换月,都会产生缺失值。处理方法要看场景:

  • 时间序列因子:用前向填充(ffill),假设停牌期间价格不变
  • 截面因子:用行业均值填充,或者直接剔除
  • 极端情况:超过连续5天缺失,我建议直接标记为无效
# 我个人常用的缺失值处理逻辑
import pandas as pd

def clean_missing(df, method='ffill', max_gap=5):
    """
    df: 多级索引 DataFrame (date, asset)
    method: ffill / drop / fill_mean
    max_gap: 最大连续缺失天数
    """
    if method == 'ffill':
        # 前向填充,但限制最大连续缺失
        df = df.groupby(level='asset').apply(
            lambda x: x.fillna(method='ffill', limit=max_gap)
        )
    elif method == 'drop':
        df = df.dropna()
    return df

3.2.2 异常值检测

说白了,就是找出那些「离谱」的数据点。比如某只股票一天涨了50%,但当天大盘没动,也没有重大利好——这八成是数据错误。

我常用的方法:

  • MAD法:中位数绝对偏差,比3σ更稳健
  • 分位数法:剔除上下1%的极端值
  • 行业对比法:同行业同一天的收益率偏离超过5个标准差,标记为异常
小技巧: 我曾经用MAD法发现过一个有趣的现象——某只小盘股连续3天出现-20%的收益率,查到最后是数据商把「除权除息」当成了「价格下跌」来处理。这种坑,你不洗数据根本发现不了。

3.2.3 复权处理

做因子回测,必须用复权价格。为什么?你想想看,一只股票分红10元,价格从100跌到90,你的因子计算如果基于原始价格,就会误判为「下跌10%」,但实际持有者并没有亏损。

复权有两种方式:

类型 逻辑 适用场景
前复权 调整历史价格,保持当前价格不变 回测时常用,方便看历史走势
后复权 调整当前价格,保持历史价格不变 计算真实收益率时用

我个人习惯用后复权算因子,前复权做可视化。两者结合,基本能避开复权带来的坑。

3.2.4 幸存者偏差剔除

这是新手最容易忽略的问题。你拿今天还在交易的股票做回测,当然表现好——因为退市的、暴跌的都被你自动过滤掉了。

正确的做法:

  • 回测时使用「当时存在」的股票池
  • 包含已退市、被ST的股票
  • 定期更新成分股列表
核心观点: 数据清洗不是一次性的工作,而是贯穿整个因子研究流程的持续动作。每当你发现一个异常结果,第一反应应该是「检查数据」,而不是「调整参数」。

3.3 数据预处理:为因子计算铺路

数据洗干净了,接下来就是预处理。这一步做好了,因子计算会顺畅很多。

3.3.1 对齐与重采样

不同数据源的频率可能不一样:日线、分钟线、Tick级。做因子研究,我建议统一到日频。为什么?

  • 日频数据最稳定,受微观结构噪声影响小
  • 大部分学术研究都基于日频,便于对比
  • 计算成本低,迭代速度快
# 日频重采样示例
def resample_to_daily(df, rule='D', agg_method='last'):
    """
    将高频数据重采样为日频
    rule: 'D' 日频, 'W' 周频, 'M' 月频
    agg_method: 'last' 收盘价, 'mean' 均价
    """
    return df.resample(rule).agg(agg_method)

3.3.2 行业分类标准化

不同数据源的行业分类可能不一样。有的用申万一级,有的用GICS,有的用中信。做截面因子时,行业中性化处理必须统一分类标准。

我建议的做法:

  • 建立行业映射表,把不同分类映射到统一标准
  • 至少保留两级分类(行业+子行业)
  • 定期更新行业分类(每年至少一次)

3.3.3 数据切片与存储

数据量大了之后,每次全量加载会非常慢。我习惯按年份切片存储:

# 按年切片存储
def slice_and_save(df, path_template='./data/{year}.parquet'):
    df['year'] = df.index.year
    for year, group in df.groupby('year'):
        group.to_parquet(path_template.format(year=year))

这样做的好处是:回测时只需要加载对应年份的数据,内存占用小,速度快。

3.4 数据质量检查清单

每次拿到新数据,我都会过一遍这个清单:

  1. 时间范围检查:数据覆盖的时间段是否完整?有没有跳空?
  2. 股票数量检查:每个时间截面的股票数量是否合理?有没有突然减少?
  3. 价格合理性检查:有没有负价格?有没有异常涨跌停?
  4. 成交量检查:有没有零成交量的交易日?
  5. 财务数据检查:营收、利润等指标有没有缺失?有没有异常值?
注意: 数据质量检查不是一次性工作。每次新增数据、每次更新数据源,都要重新跑一遍检查流程。我曾经因为偷懒没检查,用了一整年的错误数据做研究,浪费了三个月时间。

3.5 数据清洗流程总览

下面这张图,是我多年实践总结出来的数据清洗流程。你可以把它贴在工位上,每次做数据都对照着走一遍。

数据清洗与预处理流程图 数据获取 缺失值处理 异常值检测 复权处理 幸存者偏差剔除 干净数据输出 免费/商业/自建 ffill/drop/均值填充 MAD/分位数/行业对比 前复权/后复权 包含退市/ST股票

这张图看起来简单,但每一步都有很多细节。我在实际项目中,光是「异常值检测」这一步,就踩过不下10个坑。比如:

  • 新股上市首日不设涨跌幅,容易被误判为异常
  • ST股连续跌停,成交量极低,容易被误判为缺失
  • 期货换月时的价格跳空,需要特殊处理

这些细节,只有在实战中才会遇到。书本上不会告诉你,数据商也不会提醒你。

最后说一句: 数据准备占整个因子研究工作量的60%以上。别嫌麻烦,也别想走捷径。数据质量决定了你研究的可信度。数据干净了,因子计算、回测、分析才有意义。

专注资料整理