4、数据获取与预处理:数据源选择、清洗、对数化与重采样

做统计套利,第一步就是搞数据。这一步要是歪了,后面模型再漂亮也是白搭。我见过太多人把精力全花在策略上,结果数据源选错、缺失值没处理,回测曲线漂亮得像假的一样——嗯,其实就是假的。

今天咱们就把数据获取和预处理这关彻底打通。说白了,就是回答四个问题:数据从哪来?数据脏了怎么办?价格序列怎么处理才符合统计要求?不同频率的数据怎么对齐?

4.1 数据源选择:你的弹药库

选数据源,我个人的习惯是:先看稳定性,再看成本,最后看覆盖范围。市面上主流的有三个:Yahoo Finance、Tushare、Wind。我分别说说它们的脾气。

数据源 优点 缺点 适用场景
Yahoo Finance 免费、全球覆盖、API简单 数据偶尔缺失、延迟高、不稳定 快速原型、个人研究
Tushare 国内数据全、社区活跃、免费额度够用 需要注册Token、有调用频率限制 A股量化、中低频策略
Wind 数据质量最高、实时性强、机构标配 收费贵、接口复杂 机构实盘、高频交易

我在项目中遇到过最坑的一次:用Yahoo Finance拉美股数据,回测跑了一年,结果发现某只股票在2018年有连续3个月的数据全是重复的。后来查原因,是Yahoo那边数据源切换导致的。所以我的建议是:做回测至少用两个数据源交叉验证

我的小技巧: 如果你用Tushare,记得把token存成环境变量,别硬编码在代码里。我曾经把token直接写在Jupyter Notebook里,然后不小心上传到了GitHub……嗯,第二天就被别人刷爆了额度。

4.2 数据清洗:把脏东西筛出去

数据拿到手,第一件事不是建模,是洗数据。你想想看,如果原始数据里就有缺失值、异常值,模型学到的全是噪音。

4.2.1 缺失值处理

缺失值分两种:随机缺失系统性缺失。随机缺失好办,比如某天停牌导致数据为空。系统性缺失就麻烦了,比如某只股票退市后所有数据都没了。

我的处理原则很简单:

  • 连续缺失不超过3个交易日:用前向填充(ffill)或线性插值
  • 连续缺失超过5个交易日:直接剔除该时间段
  • 某只股票缺失超过20%:放弃这只股票
import pandas as pd
import numpy as np

# 假设df是包含多只股票价格的数据框
# 前向填充缺失值(最多填充3天)
df.fillna(method='ffill', limit=3, inplace=True)

# 检查每只股票的缺失比例
missing_ratio = df.isnull().sum() / len(df)
stocks_to_drop = missing_ratio[missing_ratio > 0.2].index
df.drop(columns=stocks_to_drop, inplace=True)
注意: 千万别用均值填充价格序列!价格是有时间序列自相关性的,均值填充会破坏这种结构。我曾经见过有人用均值填充后,协整检验结果全变了——说白了就是假信号。

4.2.2 异常值处理

异常值怎么找?我一般用两种方法:

  • 3σ原则:超出均值±3倍标准差的值视为异常
  • IQR方法:超出Q1-1.5*IQR或Q3+1.5*IQR的值视为异常

我个人更推荐IQR方法,因为价格序列往往不是正态分布,3σ原则容易误判。举个例子,某只股票突然因为财报利好跳空高开10%,这在3σ里可能是异常,但在统计套利里,这种跳空恰恰是我们要捕捉的信号。

def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    return (series < lower_bound) | (series > upper_bound)

# 标记异常值,但不直接删除,先观察
outliers = df.apply(detect_outliers_iqr)
print(f"异常值数量: {outliers.sum().sum()}")

4.3 价格序列的对数化处理

为什么要对数化?说白了,就是让数据更「乖」。价格序列有两个特点:非平稳异方差。对数化能解决一部分问题。

具体来说:

  • 对数收益率:ln(P_t / P_{t-1}),比简单收益率更接近正态分布
  • 对数价格:ln(P_t),能消除指数增长趋势,让序列更平稳

我在做配对交易时,习惯用对数价格做协整检验。为什么?因为协整检验要求序列是同阶单整的,而对数化后的价格序列往往更容易满足这个条件。

# 对数价格
df_log = np.log(df)

# 对数收益率
df_log_return = df_log.diff().dropna()
核心要点: 对数化不是万能的。如果价格序列本身有负值(比如某些衍生品),就不能直接取对数。这时候可以考虑用百分比收益率替代。

4.4 同步性与重采样

做统计套利,最怕的就是数据不同步。你想想看,A股和港股交易时间不一样,美股还有盘前盘后。如果直接用收盘价,那时间戳根本对不上。

我的做法是:统一重采样到同一个时间频率

4.4.1 重采样方法

  • 日频数据:用收盘价,直接取每天最后一个有效价格
  • 小时频数据:用最后5分钟的均价,避免收盘价操纵
  • 分钟频数据:用最后1笔成交价,注意剔除集合竞价时段
# 将多个股票的数据重采样到日频
df_resampled = df.resample('D').last()

# 如果某天没有数据,用前一天的收盘价填充
df_resampled.fillna(method='ffill', inplace=True)

4.4.2 跨市场同步

如果你做的是跨市场套利(比如A+H股),那就要特别注意时区对齐。我一般会把所有时间统一转换成UTC,然后再重采样。

# 假设df_a是A股数据(北京时间),df_h是港股数据(香港时间)
# 统一转换为UTC
df_a.index = df_a.index.tz_localize('Asia/Shanghai').tz_convert('UTC')
df_h.index = df_h.index.tz_localize('Asia/Hong_Kong').tz_convert('UTC')

# 然后重采样到日频
df_a_daily = df_a.resample('D').last()
df_h_daily = df_h.resample('D').last()
避坑指南: 我曾经在做美股和欧股的配对交易时,没注意夏令时切换,结果有两天数据完全错位。后来我加了一个检查:每次重采样后,手动检查时间戳是否对齐。就一行代码:print(df.index[:5]),花不了10秒钟,但能省下几小时的调试时间。

知识体系总览

下面这张图,是我自己总结的数据预处理全流程。你照着这个顺序走,基本不会出大问题。

数据获取与预处理全流程 数据源选择 Yahoo / Tushare / Wind 数据清洗 缺失值 / 异常值处理 对数化处理 ln(P) / ln(Pt/Pt-1) 同步性与重采样 统一频率 / 时区对齐 / 前向填充 输出:干净数据 用于协整检验 / 策略回测 预处理检查清单 ✅ 数据源是否稳定?至少两个源交叉验证 ✅ 缺失值是否按规则处理?不要用均值填充 ✅ 异常值是否标记?不要直接删除 ✅ 时间戳是否对齐?检查夏令时和时区

嗯,数据预处理这块,说难不难,说简单也不简单。关键是要养成习惯:每次拿到数据,先跑一遍清洗流程,再开始建模。我见过太多人跳过这步,结果回测曲线漂亮得像假的一样——后来发现是数据泄露了。

好了,数据准备好了,下一步就可以开始做协整检验了。不过那是下一章的事,咱们先把这章的内容消化掉。


专注资料整理