数据获取与清洗:ETF择时系统的第一步

做量化交易,尤其是ETF择时,有个铁律:垃圾进,垃圾出。你策略再牛,模型再花哨,数据源有问题,结果全是废的。

我个人习惯,花在数据清洗上的时间,至少占整个项目周期的30%。别嫌多,这钱省不了。今天我们就聊聊,怎么把ETF的历史数据搞到手,再把它收拾得服服帖帖。

1. 获取ETF历史行情数据

数据源的选择,说白了就两条路:免费的和付费的。

免费数据源

  • Tushare:国内用得最多的免费接口之一。数据全,但需要注册拿token。我早期做回测时全靠它。
  • AKShare:开源免费,接口丰富,更新快。适合个人研究。
  • Yahoo Finance:做海外ETF必备。但国内访问有时不稳定,你懂的。

付费数据源

  • Wind:机构标配。数据质量高,但贵。我在私募时用的就是Wind,确实省心。
  • 聚宽/米筐:提供量化平台的同时也卖数据,性价比不错。

我个人建议,初期先用免费数据跑通流程。等策略稳定了,再考虑付费数据。

下面是一个用Tushare获取沪深300 ETF(代码:510300)历史数据的示例:

import tushare as ts
import pandas as pd

# 设置token(需要去tushare官网注册)
ts.set_token('your_token_here')
pro = ts.pro_api()

# 获取510300从2020-01-01到2023-12-31的日线数据
df = pro.fund_daily(
    ts_code='510300.SH',
    start_date='20200101',
    end_date='20231231',
    fields='trade_date,open,high,low,close,vol'
)

# 排序并重置索引
df = df.sort_values('trade_date').reset_index(drop=True)
print(df.head())

嗯,这里要注意:fund_daily接口返回的是未复权数据。后面我们会专门处理复权。

2. 数据清洗与对齐

数据拿到手,第一件事不是跑策略,而是体检

常见问题

  • 缺失值:某天停牌,数据直接跳空。
  • 异常值:价格突然暴涨暴跌,可能是数据录入错误。
  • 重复值:同一交易日出现多条记录。
  • 时间对齐:不同ETF的交易日期可能不一致。

我曾经遇到过一个坑:某只ETF在2015年股灾期间停牌了两个月,复牌后连续跌停。如果没做数据对齐,回测结果会严重失真。

数据对齐的标准做法

  1. 先确定一个基准日期序列(比如全市场交易日历)。
  2. 把所有ETF的数据都对齐到这个序列上。
  3. 缺失的日期用NaN填充,或者用前值填充。
# 生成全市场交易日历(这里用沪深300的交易日作为基准)
benchmark = pro.index_daily(ts_code='000300.SH', start_date='20200101', end_date='20231231')
benchmark_dates = benchmark['trade_date'].tolist()

# 将ETF数据对齐到基准日期
df_aligned = df.set_index('trade_date').reindex(benchmark_dates)
print(df_aligned.head(10))

3. 复权处理

复权,是ETF择时系统里最容易忽略、但影响最大的环节。

为什么需要复权?

ETF会分红、拆份额。如果不复权,价格序列会出现跳空。你想想看,一个分红除息日,价格突然掉了一块,你的技术指标全乱了。

两种复权方式

复权类型 说明 适用场景
前复权 调整历史价格,使价格序列连续 技术分析、择时策略
后复权 调整当前价格,保持历史价格不变 计算真实收益率

我个人习惯,做择时策略时用前复权。因为技术指标(比如均线、MACD)对价格连续性要求高。后复权虽然能反映真实收益,但历史价格会被扭曲,指标会失真。

复权实现

Tushare的fund_daily接口不直接提供复权因子。但我们可以用adj_factor接口获取复权因子,自己计算。

# 获取复权因子
adj = pro.fund_adj(ts_code='510300.SH', start_date='20200101', end_date='20231231')
adj = adj[['trade_date', 'adj_factor']]

# 合并到原始数据
df_adj = df.merge(adj, on='trade_date', how='left')

# 前复权计算
df_adj['close_adj'] = df_adj['close'] * df_adj['adj_factor'] / df_adj['adj_factor'].iloc[-1]
df_adj['open_adj'] = df_adj['open'] * df_adj['adj_factor'] / df_adj['adj_factor'].iloc[-1]
df_adj['high_adj'] = df_adj['high'] * df_adj['adj_factor'] / df_adj['adj_factor'].iloc[-1]
df_adj['low_adj'] = df_adj['low'] * df_adj['adj_factor'] / df_adj['adj_factor'].iloc[-1]

print(df_adj[['trade_date', 'close', 'close_adj']].head())
小技巧:复权因子通常是累乘的。如果你发现复权后的价格和原始价格差很多,别慌,说明这只ETF分红次数多,复权影响大。

4. 缺失值处理

缺失值,说白了就是数据里的窟窿。怎么补?

常见处理方法

  • 删除法:直接删掉缺失的行。简单粗暴,但会损失数据。
  • 前向填充:用上一个有效值填充。适合停牌场景。
  • 插值法:用前后值线性插值。适合短期缺失。
  • 模型填充:用其他相关ETF的数据预测缺失值。适合长期缺失。

我一般这样选:

  • 如果缺失天数 ≤ 5天,用前向填充。
  • 如果缺失天数 > 5天,用插值法。
  • 如果缺失天数 > 20天,直接删除该时间段。
# 前向填充
df_filled = df_aligned.fillna(method='ffill')

# 线性插值
df_interpolated = df_aligned.interpolate(method='linear')

# 删除缺失超过20%的股票
threshold = len(df_aligned) * 0.2
df_clean = df_aligned.dropna(thresh=threshold)
警告:千万不要用均值填充金融时间序列!价格有趋势,均值填充会引入未来信息,造成过拟合。我曾经见过有人这么干,回测曲线漂亮得不行,实盘直接崩了。

5. 知识体系总览

下面这张图,概括了数据获取与清洗的完整流程。你可以把它当作一个检查清单:

ETF数据获取与清洗流程 数据获取 Tushare / AKShare / Wind 数据清洗 去重 / 异常值 / 对齐 复权处理 前复权 / 后复权 缺失值处理 填充 / 插值 / 删除 数据对齐 基准日期 / 多ETF对齐 输出清洗后数据 CSV / HDF5 / 数据库 数据获取 → 清洗 → 复权 → 缺失值处理 → 对齐 → 输出 每个环节都可能影响最终策略表现,不可跳过 关键检查点 ① 数据源可靠性 ② 复权方式选择 ③ 缺失值处理策略 ④ 多ETF时间对齐 ⑤ 异常值检测 ⑥ 数据完整性验证

6. 避坑指南

最后,分享几个我踩过的坑:

  • 不要直接用复权后的价格算收益率:前复权价格是调整过的,用它算收益率会出错。正确做法是用原始价格算收益率,或者用后复权价格。
  • 注意ETF的停牌规则:有些ETF停牌期间,其他ETF还在交易。如果直接填充,会引入偏差。
  • 数据缓存很重要:每次跑策略都重新下载数据,浪费时间。我习惯把清洗后的数据存成HDF5格式,下次直接读取。
  • 多ETF对齐时,用交易日历:不要用某个ETF的交易日作为基准,否则会漏掉其他ETF的交易数据。

核心总结:数据获取与清洗,是ETF择时系统的地基。地基不稳,楼盖得再高也得塌。花时间把数据搞干净,后面写策略、做回测,才能事半功倍。

专注资料整理