第三章:数据获取与清洗——数据源选择、对齐、缺失值与异常值处理

做跨品种套利策略,数据是地基。地基不稳,模型再漂亮也是空中楼阁。我见过太多人把精力全花在调参上,结果数据没对齐,回测曲线漂亮得像假的一样——嗯,其实就是假的。

这一章,我们聊聊数据怎么拿、怎么洗、怎么对齐。说白了,就是让你手里的数据能真实反映市场。

3.1 数据源选择:Wind、聚宽、Tushare怎么选?

国内做量化,数据源基本就这三家。我三个都用过,各有各的脾气。

数据源 优点 缺点 适合场景
Wind 数据最全、最准,机构标配 贵,一年几万块,个人用户难搞 机构实盘、专业研究
聚宽 免费,API友好,回测平台一体 数据深度不够,历史数据有限 个人策略开发、快速验证
Tushare 开源,社区活跃,数据种类多 需要积分,稳定性一般 个人研究、数据挖掘

我个人习惯是:策略开发阶段用聚宽或Tushare,快速迭代。到了实盘前,必须用Wind做一次数据校验。为什么?因为免费数据偶尔会有跳空、复权错误,你想想看,套利策略对价差敏感,差一个tick可能就亏钱。

我的小技巧: 如果你用Tushare,建议注册后攒够200积分,这样能拿到日线以上的高频数据。我曾经为了省积分用低权限接口,结果数据少了三天,回测曲线直接变形。

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填充,后面还得处理,麻烦。

注意: 不同交易所的节假日可能不同。比如港股通标的,A股休市港股开市,这时候价差数据会断档。我曾经因为这个原因,回测里出现了一堆异常价差点,排查了两天才发现是日历没对齐。

3.3 缺失值处理:别让NaN毁了你的模型

数据对齐后,大概率会有缺失值。原因很多:停牌、涨跌停、数据源抽风。处理缺失值,我一般按这个优先级来:

  1. 直接删除:如果缺失比例小于5%,直接删掉那几行。简单粗暴,但有效。
  2. 前向填充:用上一个非缺失值填充。适合价格数据,因为价格在短时间内不会突变。
  3. 插值:用前后值线性插值。适合成交量、持仓量这类连续数据。
  4. 模型预测:用其他特征预测缺失值。这招慎用,容易引入偏差。
# 前向填充
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σ检测螺纹钢和热卷的价差,结果把2016年供给侧改革期间的正常价差波动全标成了异常,删掉了大量有效数据。后来改用滚动窗口,才保留了那段宝贵的行情数据。

3.5 本章知识体系图

下面这张图概括了数据获取与清洗的完整流程。你可以把它贴在工位旁边,每次做数据预处理时对照着走一遍。

数据获取与清洗流程 数据源选择 Wind / 聚宽 / Tushare 数据对齐 时间戳重采样 / 合并 缺失值处理 删除/填充/插值 异常值检测 3σ / IQR / 滚动窗口 标记异常 数据质量检查 完整性 / 一致性 / 准确性 数据不合格,返回重新清洗 ✅ 干净、对齐的价差数据 可用于策略建模与回测 注:虚线表示数据质量不合格时的回退路径

这张图的核心逻辑是:先选源,再对齐,然后处理缺失和异常,最后检查质量。如果质量不过关,别犹豫,回到对齐或清洗步骤重新来。数据这东西,偷懒一时爽,回测火葬场。

我的习惯: 每次清洗完数据,我都会随机抽10个时间点,手动去Wind或交易所官网核对价格。虽然麻烦,但能发现很多自动化检测漏掉的bug。尤其是复权因子、合约换月这些细节,机器容易搞错。

好了,数据准备就绪。下一章我们会把这些干净数据喂进机器学习模型,看看能不能挖出稳定的套利信号。不过在那之前,我建议你先把今天的内容动手跑一遍。代码不复杂,但亲手踩一遍坑,比看十遍教程都管用。

专注资料整理