4、数据获取与预处理:数据源选择、清洗、对数化与重采样
做统计套利,第一步就是搞数据。这一步要是歪了,后面模型再漂亮也是白搭。我见过太多人把精力全花在策略上,结果数据源选错、缺失值没处理,回测曲线漂亮得像假的一样——嗯,其实就是假的。
今天咱们就把数据获取和预处理这关彻底打通。说白了,就是回答四个问题:数据从哪来?数据脏了怎么办?价格序列怎么处理才符合统计要求?不同频率的数据怎么对齐?
4.1 数据源选择:你的弹药库
选数据源,我个人的习惯是:先看稳定性,再看成本,最后看覆盖范围。市面上主流的有三个:Yahoo Finance、Tushare、Wind。我分别说说它们的脾气。
| 数据源 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Yahoo Finance | 免费、全球覆盖、API简单 | 数据偶尔缺失、延迟高、不稳定 | 快速原型、个人研究 |
| Tushare | 国内数据全、社区活跃、免费额度够用 | 需要注册Token、有调用频率限制 | A股量化、中低频策略 |
| Wind | 数据质量最高、实时性强、机构标配 | 收费贵、接口复杂 | 机构实盘、高频交易 |
我在项目中遇到过最坑的一次:用Yahoo Finance拉美股数据,回测跑了一年,结果发现某只股票在2018年有连续3个月的数据全是重复的。后来查原因,是Yahoo那边数据源切换导致的。所以我的建议是:做回测至少用两个数据源交叉验证。
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秒钟,但能省下几小时的调试时间。
知识体系总览
下面这张图,是我自己总结的数据预处理全流程。你照着这个顺序走,基本不会出大问题。
嗯,数据预处理这块,说难不难,说简单也不简单。关键是要养成习惯:每次拿到数据,先跑一遍清洗流程,再开始建模。我见过太多人跳过这步,结果回测曲线漂亮得像假的一样——后来发现是数据泄露了。
好了,数据准备好了,下一步就可以开始做协整检验了。不过那是下一章的事,咱们先把这章的内容消化掉。