第三章:数据获取与清洗——数据源选择、对齐、缺失值与异常值处理
做跨品种套利策略,数据是地基。地基不稳,模型再漂亮也是空中楼阁。我见过太多人把精力全花在调参上,结果数据没对齐,回测曲线漂亮得像假的一样——嗯,其实就是假的。
这一章,我们聊聊数据怎么拿、怎么洗、怎么对齐。说白了,就是让你手里的数据能真实反映市场。
3.1 数据源选择:Wind、聚宽、Tushare怎么选?
国内做量化,数据源基本就这三家。我三个都用过,各有各的脾气。
| 数据源 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Wind | 数据最全、最准,机构标配 | 贵,一年几万块,个人用户难搞 | 机构实盘、专业研究 |
| 聚宽 | 免费,API友好,回测平台一体 | 数据深度不够,历史数据有限 | 个人策略开发、快速验证 |
| Tushare | 开源,社区活跃,数据种类多 | 需要积分,稳定性一般 | 个人研究、数据挖掘 |
我个人习惯是:策略开发阶段用聚宽或Tushare,快速迭代。到了实盘前,必须用Wind做一次数据校验。为什么?因为免费数据偶尔会有跳空、复权错误,你想想看,套利策略对价差敏感,差一个tick可能就亏钱。
3.2 数据对齐:跨品种套利的命门
跨品种套利,核心是价差。但不同品种的交易时间不一样,比如股指期货和ETF,一个9:30开盘,一个9:15开盘。如果你不对齐,价差里就混进了时间错位的噪声。
怎么做对齐?我一般用时间戳重采样。把两个品种的数据都切成相同的时间片,比如1分钟、5分钟。然后取每个时间片的最后一条记录作为该片的值。
import pandas as pd
# 假设df1是IF主力合约的1分钟数据,df2是沪深300ETF的1分钟数据
# 对齐到整分钟时间戳
df1['time'] = pd.to_datetime(df1['time']).dt.floor('1min')
df2['time'] = pd.to_datetime(df2['time']).dt.floor('1min')
# 按时间合并
aligned = pd.merge(df1, df2, on='time', how='inner', suffixes=('_fut', '_etf'))
# 计算价差
aligned['spread'] = aligned['close_fut'] - aligned['close_etf']
这里有个坑:用inner join还是outer join? 我个人建议用inner,只保留两个品种都有数据的时刻。如果你用outer,缺失的时间点会用NaN填充,后面还得处理,麻烦。
3.3 缺失值处理:别让NaN毁了你的模型
数据对齐后,大概率会有缺失值。原因很多:停牌、涨跌停、数据源抽风。处理缺失值,我一般按这个优先级来:
- 直接删除:如果缺失比例小于5%,直接删掉那几行。简单粗暴,但有效。
- 前向填充:用上一个非缺失值填充。适合价格数据,因为价格在短时间内不会突变。
- 插值:用前后值线性插值。适合成交量、持仓量这类连续数据。
- 模型预测:用其他特征预测缺失值。这招慎用,容易引入偏差。
# 前向填充
df['close'].fillna(method='ffill', inplace=True)
# 线性插值
df['volume'].interpolate(method='linear', inplace=True)
# 删除缺失比例超过20%的列
df.dropna(thresh=len(df)*0.8, axis=1, inplace=True)
你可能会问:为什么不用均值填充?嗯,对于金融时间序列,均值填充会破坏序列的自相关性。我试过一次,模型训练出来,回测收益翻倍,实盘直接亏成狗。后来再也不敢用均值填价格了。
3.4 异常值检测:揪出那些离谱的数据点
异常值在套利策略里特别致命。比如某天价差突然拉大到10个点,你以为是套利机会,结果一查,是数据源把价格写错了。这种假信号会让你亏得莫名其妙。
我常用的异常值检测方法有三种:
- 3σ原则:假设数据服从正态分布,超过均值±3倍标准差的就是异常。简单,但金融数据往往厚尾,容易误判。
- IQR方法:用四分位距。超过Q1-1.5*IQR或Q3+1.5*IQR的算异常。比3σ稳健。
- 滚动窗口法:用过去N个点的均值和标准差判断当前点是否异常。适合处理时间序列的局部异常。
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return (series < lower) | (series > upper)
# 滚动窗口法
def detect_outliers_rolling(series, window=20, n_std=3):
rolling_mean = series.rolling(window).mean()
rolling_std = series.rolling(window).std()
return (series - rolling_mean).abs() > n_std * rolling_std
我个人更推荐滚动窗口法。为什么?因为价差本身会随时间漂移,用全局的3σ会把正常的高波动区间误判为异常。滚动窗口能适应局部变化。
3.5 本章知识体系图
下面这张图概括了数据获取与清洗的完整流程。你可以把它贴在工位旁边,每次做数据预处理时对照着走一遍。
这张图的核心逻辑是:先选源,再对齐,然后处理缺失和异常,最后检查质量。如果质量不过关,别犹豫,回到对齐或清洗步骤重新来。数据这东西,偷懒一时爽,回测火葬场。
好了,数据准备就绪。下一章我们会把这些干净数据喂进机器学习模型,看看能不能挖出稳定的套利信号。不过在那之前,我建议你先把今天的内容动手跑一遍。代码不复杂,但亲手踩一遍坑,比看十遍教程都管用。