数据获取与清洗: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年股灾期间停牌了两个月,复牌后连续跌停。如果没做数据对齐,回测结果会严重失真。
数据对齐的标准做法:
- 先确定一个基准日期序列(比如全市场交易日历)。
- 把所有ETF的数据都对齐到这个序列上。
- 缺失的日期用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())
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. 知识体系总览
下面这张图,概括了数据获取与清洗的完整流程。你可以把它当作一个检查清单:
6. 避坑指南
最后,分享几个我踩过的坑:
- 不要直接用复权后的价格算收益率:前复权价格是调整过的,用它算收益率会出错。正确做法是用原始价格算收益率,或者用后复权价格。
- 注意ETF的停牌规则:有些ETF停牌期间,其他ETF还在交易。如果直接填充,会引入偏差。
- 数据缓存很重要:每次跑策略都重新下载数据,浪费时间。我习惯把清洗后的数据存成HDF5格式,下次直接读取。
- 多ETF对齐时,用交易日历:不要用某个ETF的交易日作为基准,否则会漏掉其他ETF的交易数据。
核心总结:数据获取与清洗,是ETF择时系统的地基。地基不稳,楼盖得再高也得塌。花时间把数据搞干净,后面写策略、做回测,才能事半功倍。