3、数据获取与预处理:数据源选择、数据清洗、数据对齐与重采样
做统计套利,数据就是你的命根子。这话一点都不夸张。
我见过太多人,策略逻辑写得天花乱坠,结果一跑回测,全是噪音。为什么?数据源没选对,或者预处理没做好。说白了,垃圾进垃圾出。今天咱们就把这块硬骨头啃下来。
3.1 数据源选择:Yahoo Finance vs 本地CSV
我个人习惯,做快速原型验证的时候,首选 Yahoo Finance。免费、方便、覆盖面广。但你要真拿它做生产环境,我劝你三思。
两种数据源怎么选?我列个对比表,你一看就明白:
| 对比维度 | Yahoo Finance | 本地CSV |
|---|---|---|
| 获取速度 | 依赖网络,有延迟 | 秒级加载 |
| 数据完整性 | 偶有缺失,节假日不统一 | 可自行校验,可控 |
| 历史深度 | 一般支持10-20年 | 取决于你存了多少 |
| 适用场景 | 快速验证、小规模回测 | 生产环境、高频策略 |
我的建议是:先用Yahoo Finance做快速迭代,策略稳定后,再切换到本地CSV做精细回测。这样既快又稳。
3.2 数据清洗:缺失值与异常值
拿到数据后,第一件事不是跑策略,而是洗数据。你想想看,如果数据里有个空值,你的协整检验直接报错,那多尴尬。
3.2.1 缺失值处理
缺失值一般有两种:一种是某一天完全没有数据(比如节假日),另一种是某一天部分字段缺失。
我常用的处理方式:
- 向前填充(ffill):用上一个交易日的数据填充。适合价格数据,因为价格不会突变。
- 线性插值:适合成交量等波动较大的指标。
- 直接删除:如果缺失比例超过5%,我建议直接删掉那段时间的数据。
3.2.2 异常值检测
异常值怎么找?我一般用两种方法:
- 3σ原则:超过均值±3倍标准差的数据,标记为异常。
- IQR方法:四分位距法,更稳健,不受极端值影响。
举个例子,某只股票突然一天涨了50%,但大盘没动。这大概率是数据错误,而不是真实行情。我曾经在回测中没处理这种异常值,结果策略信号全乱了,白白浪费了两天时间排查。
3.3 数据对齐与重采样
做统计套利,最核心的就是价差。价差怎么算?两个品种的价格相减。但前提是——时间必须对齐。
你想想看,如果A股是9:30开盘,港股是9:00开盘,那9:15这个时间点,A股还没数据,港股已经有了。直接算价差?那肯定不对。
3.3.1 时间对齐
我常用的对齐策略:
- 按分钟对齐:取两个品种都有数据的时间点。缺失的用前值填充。
- 按日对齐:只取两个品种都交易的日期。节假日不一致的,直接跳过。
这里有个坑:不同交易所的休市时间不同。比如美国有感恩节,中国有春节。如果你做跨市场套利,一定要手动维护一个交易日历。
3.3.2 重采样
重采样,说白了就是把高频数据降成低频,或者把低频数据升成高频。
我一般这样做:
- 日线数据:用收盘价,不需要重采样。
- 分钟数据:如果需要做日内套利,我会把1分钟数据重采样成5分钟或15分钟。为什么?因为1分钟数据噪音太大,协整检验容易误判。
resample().agg() 很容易实现。
3.4 知识体系框架图
下面这张图,是我自己总结的数据预处理流程。你照着做,基本不会出大问题:
3.5 代码实战:一个完整的预处理流程
光说不练假把式。我给你一个可以直接跑的代码片段,把上面讲的内容串起来:
import pandas as pd
import numpy as np
from pandas_datareader import data as pdr
import yfinance as yf
# 1. 获取数据
yf.pdr_override()
tickers = ['AAPL', 'MSFT']
data = pdr.get_data_yahoo(tickers, start='2020-01-01', end='2023-12-31')
# 2. 提取收盘价
close_prices = data['Close']
# 3. 缺失值处理:向前填充
close_prices = close_prices.fillna(method='ffill')
# 4. 异常值检测:3σ原则
def detect_outliers(series):
mean = series.mean()
std = series.std()
return (series < mean - 3*std) | (series > mean + 3*std)
outliers = close_prices.apply(detect_outliers)
# 将异常值替换为前值
close_prices[outliers] = close_prices.shift(1)[outliers]
# 5. 数据对齐:只保留两个品种都有数据的日期
close_prices = close_prices.dropna()
# 6. 重采样:如果需要分钟数据,可以这样
# close_prices.resample('5T').last()
print(f"最终数据形状: {close_prices.shape}")
print(f"缺失值数量: {close_prices.isnull().sum().sum()}")
fillna(method='ffill') 在数据开头有缺失时会失效。所以我会在填充前先检查前几行数据。
嗯,数据预处理这块,说白了就是「慢工出细活」。你花80%的时间洗数据,剩下20%的时间跑策略,这才是正确的比例。别急着上模型,先把数据搞干净。
公众号:蓝海数据掘金营,微信deep3321