数据获取与清洗:打好回测的地基
做统计套利,数据就是你的原材料。原材料不行,再好的策略也白搭。我见过太多人一上来就写策略代码,结果数据里全是坑,回测曲线漂亮得像假的,实盘一跑就露馅。今天咱们就把数据这块彻底捋清楚。
核心要点:数据质量决定回测可信度。花80%时间处理数据,只花20%时间写策略,这是量化老手的共识。
4.1 用yfinance获取股票数据
yfinance这个库,说白了就是雅虎财经的非官方Python接口。我最早用的时候还叫yahoo-finance,后来雅虎把API关了,社区又搞出了yfinance。嗯,这里要注意:它虽然方便,但生产环境别太依赖,偶尔会抽风。
先装一下:
pip install yfinance
获取数据其实就一行代码的事:
import yfinance as yf
# 获取单只股票
aapl = yf.download('AAPL', start='2020-01-01', end='2023-12-31')
# 获取多只股票
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')
你想想看,就这么简单?其实背后yfinance做了不少事:自动处理股票分割、股息调整、时区转换。我个人习惯是拿到数据后先看一眼shape和info,确认数据量对不对。
我的小技巧:yfinance默认返回的是调整后的价格(Adjusted Close),这对回测来说更合理。如果你需要原始价格,加参数 auto_adjust=False。
多只股票返回的是MultiIndex DataFrame,列名是(价格类型, 股票代码)的元组。我第一次用的时候被这个结构搞懵了,后来发现用 data['Close'] 就能拿到所有股票的收盘价,方便得很。
4.2 数据清洗:处理缺失值和异常值
数据拿到手,第一件事就是检查质量。我在项目中遇到过一只股票,某天突然跌了90%,后来发现是数据源的问题,不是真的暴跌。要是没清洗直接跑策略,那回测结果就完全失真了。
4.2.1 缺失值处理
先看看缺失情况:
# 检查缺失值
print(data.isnull().sum())
# 查看缺失比例
missing_ratio = data.isnull().sum() / len(data)
print(missing_ratio[missing_ratio > 0.05]) # 缺失超过5%的列
处理缺失值,我一般按这个优先级来:
- 前向填充(ffill):用上一个交易日的数据填充。适合非开盘日导致的缺失。
- 线性插值:用前后数据线性推算。适合短期缺失。
- 直接删除:如果某只股票缺失太多(比如超过20%),干脆别用了。
# 前向填充
data_ffill = data.ffill()
# 线性插值
data_interp = data.interpolate(method='linear')
# 删除缺失过多的股票
valid_tickers = [t for t in tickers
if data[t]['Close'].isnull().sum() / len(data) < 0.2]
避坑指南:我曾经用ffill处理了连续一周的缺失数据,结果那周正好是停牌,复牌后直接跳空。ffill出来的价格完全不对。所以遇到连续缺失,建议先查一下是不是有停牌、退市等特殊情况。
4.2.2 异常值检测
异常值怎么找?我常用两种方法:
- Z-score方法:假设收益率服从正态分布,超过3个标准差就算异常。
- IQR方法:用四分位距,不受极端值影响,更稳健。
import numpy as np
def detect_outliers_zscore(series, threshold=3):
z_scores = np.abs((series - series.mean()) / series.std())
return z_scores > threshold
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
return (series < Q1 - 1.5 * IQR) | (series > Q3 + 1.5 * IQR)
# 对收益率做异常检测
returns = data['Close'].pct_change().dropna()
outliers = detect_outliers_iqr(returns)
发现异常值后怎么办?别急着删。先看看是不是真实的市场事件,比如财报发布后的跳空。如果是数据错误,用前后均值替换;如果是真实事件,保留但做好标记。
4.3 数据对齐与重采样
统计套利的核心是配对交易,两只股票的数据必须严格对齐。你想想看,如果A股用收盘价,B股用开盘价,那算出来的相关性全是错的。
4.3.1 数据对齐
对齐其实就一句话:确保所有股票的时间索引一致。
# 取所有股票的共同交易日
common_dates = data['Close'].dropna().index
# 对齐数据
aligned_data = data.loc[common_dates]
这里有个坑:不同交易所的节假日不一样。比如A股春节休市,美股正常交易。做跨市场套利时,对齐就特别麻烦。我一般会先取并集,再用ffill填充非共同交易日。
4.3.2 重采样
有时候我们需要把日线数据转成周线或月线。重采样就是干这个的:
# 日线转周线(取每周最后一个交易日)
weekly_data = data['Close'].resample('W').last()
# 日线转月线
monthly_data = data['Close'].resample('M').last()
# 自定义重采样:取每周均值
weekly_mean = data['Close'].resample('W').mean()
重采样时要注意:用last还是mean,取决于你的策略逻辑。如果是趋势跟踪,用last更合理;如果是均值回归,用mean可能更平滑。
我的经验:做统计套利时,我更喜欢用日线数据。周线和月线会丢失太多细节,尤其是协整关系的检验,对数据频率很敏感。
4.4 数据存储:HDF5 vs CSV
数据清洗完了,得存起来。存成什么格式?我踩过不少坑。
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSV | 人类可读,通用性强 | 占用空间大,读写慢 | 小数据集、调试阶段 |
| HDF5 | 压缩率高,读写快,支持复杂结构 | 二进制格式,不易直接查看 | 大数据集、生产环境 |
| Parquet | 列式存储,压缩比HDF5还好 | 生态不如HDF5成熟 | 超大数据集 |
我个人习惯:开发阶段用CSV,方便调试;上线后用HDF5,性能好。存HDF5的代码长这样:
# 存为HDF5
data.to_hdf('stock_data.h5', key='close_prices', mode='w')
# 读取HDF5
data_loaded = pd.read_hdf('stock_data.h5', key='close_prices')
# 存为CSV
data.to_csv('stock_data.csv')
# 读取CSV
data_csv = pd.read_csv('stock_data.csv', index_col=0, parse_dates=True)
避坑指南:我曾经把100只股票5年的日线数据存成CSV,结果文件有200多MB,每次读取要等十几秒。后来换成HDF5,文件压缩到50MB,读取不到1秒。数据量大了,格式选择真的很关键。
知识体系总览
下面这张图把数据获取与清洗的整个流程串起来了,你可以对照着看:
这张图把整个流程串起来了。从yfinance获取数据开始,经过清洗、对齐、重采样,最后存成合适的格式。每一步都有坑,但踩过之后你就知道该怎么避了。
数据这块就聊到这儿。记住一句话:数据质量决定了你的回测到底是在做量化,还是在做自欺欺人。把基础打牢,后面的策略实现才能站得住脚。