数据获取与清洗:打好回测的地基

做统计套利,数据就是你的原材料。原材料不行,再好的策略也白搭。我见过太多人一上来就写策略代码,结果数据里全是坑,回测曲线漂亮得像假的,实盘一跑就露馅。今天咱们就把数据这块彻底捋清楚。

核心要点:数据质量决定回测可信度。花80%时间处理数据,只花20%时间写策略,这是量化老手的共识。

4.1 用yfinance获取股票数据

yfinance这个库,说白了就是雅虎财经的非官方Python接口。我最早用的时候还叫yahoo-finance,后来雅虎把API关了,社区又搞出了yfinance。嗯,这里要注意:它虽然方便,但生产环境别太依赖,偶尔会抽风。

先装一下:

pip install yfinance

获取数据其实就一行代码的事:

import yfinance as yf

# 获取单只股票
aapl = yf.download('AAPL', start='2020-01-01', end='2023-12-31')

# 获取多只股票
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')

你想想看,就这么简单?其实背后yfinance做了不少事:自动处理股票分割、股息调整、时区转换。我个人习惯是拿到数据后先看一眼shape和info,确认数据量对不对。

我的小技巧:yfinance默认返回的是调整后的价格(Adjusted Close),这对回测来说更合理。如果你需要原始价格,加参数 auto_adjust=False

多只股票返回的是MultiIndex DataFrame,列名是(价格类型, 股票代码)的元组。我第一次用的时候被这个结构搞懵了,后来发现用 data['Close'] 就能拿到所有股票的收盘价,方便得很。

4.2 数据清洗:处理缺失值和异常值

数据拿到手,第一件事就是检查质量。我在项目中遇到过一只股票,某天突然跌了90%,后来发现是数据源的问题,不是真的暴跌。要是没清洗直接跑策略,那回测结果就完全失真了。

4.2.1 缺失值处理

先看看缺失情况:

# 检查缺失值
print(data.isnull().sum())

# 查看缺失比例
missing_ratio = data.isnull().sum() / len(data)
print(missing_ratio[missing_ratio > 0.05])  # 缺失超过5%的列

处理缺失值,我一般按这个优先级来:

  1. 前向填充(ffill):用上一个交易日的数据填充。适合非开盘日导致的缺失。
  2. 线性插值:用前后数据线性推算。适合短期缺失。
  3. 直接删除:如果某只股票缺失太多(比如超过20%),干脆别用了。
# 前向填充
data_ffill = data.ffill()

# 线性插值
data_interp = data.interpolate(method='linear')

# 删除缺失过多的股票
valid_tickers = [t for t in tickers 
                 if data[t]['Close'].isnull().sum() / len(data) < 0.2]

避坑指南:我曾经用ffill处理了连续一周的缺失数据,结果那周正好是停牌,复牌后直接跳空。ffill出来的价格完全不对。所以遇到连续缺失,建议先查一下是不是有停牌、退市等特殊情况。

4.2.2 异常值检测

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

  • Z-score方法:假设收益率服从正态分布,超过3个标准差就算异常。
  • IQR方法:用四分位距,不受极端值影响,更稳健。
import numpy as np

def detect_outliers_zscore(series, threshold=3):
    z_scores = np.abs((series - series.mean()) / series.std())
    return z_scores > threshold

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

# 对收益率做异常检测
returns = data['Close'].pct_change().dropna()
outliers = detect_outliers_iqr(returns)

发现异常值后怎么办?别急着删。先看看是不是真实的市场事件,比如财报发布后的跳空。如果是数据错误,用前后均值替换;如果是真实事件,保留但做好标记。

4.3 数据对齐与重采样

统计套利的核心是配对交易,两只股票的数据必须严格对齐。你想想看,如果A股用收盘价,B股用开盘价,那算出来的相关性全是错的。

4.3.1 数据对齐

对齐其实就一句话:确保所有股票的时间索引一致。

# 取所有股票的共同交易日
common_dates = data['Close'].dropna().index

# 对齐数据
aligned_data = data.loc[common_dates]

这里有个坑:不同交易所的节假日不一样。比如A股春节休市,美股正常交易。做跨市场套利时,对齐就特别麻烦。我一般会先取并集,再用ffill填充非共同交易日。

4.3.2 重采样

有时候我们需要把日线数据转成周线或月线。重采样就是干这个的:

# 日线转周线(取每周最后一个交易日)
weekly_data = data['Close'].resample('W').last()

# 日线转月线
monthly_data = data['Close'].resample('M').last()

# 自定义重采样:取每周均值
weekly_mean = data['Close'].resample('W').mean()

重采样时要注意:用last还是mean,取决于你的策略逻辑。如果是趋势跟踪,用last更合理;如果是均值回归,用mean可能更平滑。

我的经验:做统计套利时,我更喜欢用日线数据。周线和月线会丢失太多细节,尤其是协整关系的检验,对数据频率很敏感。

4.4 数据存储:HDF5 vs CSV

数据清洗完了,得存起来。存成什么格式?我踩过不少坑。

格式 优点 缺点 适用场景
CSV 人类可读,通用性强 占用空间大,读写慢 小数据集、调试阶段
HDF5 压缩率高,读写快,支持复杂结构 二进制格式,不易直接查看 大数据集、生产环境
Parquet 列式存储,压缩比HDF5还好 生态不如HDF5成熟 超大数据集

我个人习惯:开发阶段用CSV,方便调试;上线后用HDF5,性能好。存HDF5的代码长这样:

# 存为HDF5
data.to_hdf('stock_data.h5', key='close_prices', mode='w')

# 读取HDF5
data_loaded = pd.read_hdf('stock_data.h5', key='close_prices')

# 存为CSV
data.to_csv('stock_data.csv')

# 读取CSV
data_csv = pd.read_csv('stock_data.csv', index_col=0, parse_dates=True)

避坑指南:我曾经把100只股票5年的日线数据存成CSV,结果文件有200多MB,每次读取要等十几秒。后来换成HDF5,文件压缩到50MB,读取不到1秒。数据量大了,格式选择真的很关键。

知识体系总览

下面这张图把数据获取与清洗的整个流程串起来了,你可以对照着看:

数据获取与清洗流程 数据获取 yfinance.download() 数据清洗 缺失值 · 异常值 对齐与重采样 resample · align 数据存储 HDF5 / CSV 回测准备 协整检验 · 策略参数 数据获取细节 • 单只/多只股票下载 • 自动调整价格 • 指定时间范围 • MultiIndex处理 • 时区处理 数据清洗细节 • 缺失值:ffill/interpolate • 异常值:Z-score/IQR • 停牌/退市处理 • 数据质量检查 • 收益率计算 存储与对齐细节 • 共同交易日对齐 • 日/周/月重采样 • HDF5压缩存储 • CSV调试用 • 跨市场对齐

这张图把整个流程串起来了。从yfinance获取数据开始,经过清洗、对齐、重采样,最后存成合适的格式。每一步都有坑,但踩过之后你就知道该怎么避了。

数据这块就聊到这儿。记住一句话:数据质量决定了你的回测到底是在做量化,还是在做自欺欺人。把基础打牢,后面的策略实现才能站得住脚。

专注资料整理