数据获取与预处理:让数据说话的第一步
做量化回测,最怕什么?
我个人觉得,最怕的就是数据本身有问题。你策略再牛,模型再花哨,数据是脏的,结果就是垃圾进垃圾出。今天咱们就来聊聊,怎么用 yfinance 把数据搞到手,再把它收拾得干干净净。
2.1 为什么数据预处理这么重要?
我刚开始做量化的时候,踩过一个坑。当时回测一个价差回归策略,跑出来年化收益高得吓人,差点以为自己发现了圣杯。结果仔细一查,发现是因为两只股票的交易时间没对齐——一个在纽约,一个在伦敦,数据里混着大量缺失值,策略其实是在「预测空气」。
说白了,数据预处理就是给你的策略打好地基。地基不稳,楼盖得再高也得塌。
核心原则: 数据质量 > 策略复杂度。一个简单的策略配上干净的数据,往往比一个复杂的策略配上脏数据更靠谱。
2.2 用 yfinance 获取数据
yfinance 是个好东西,免费、简单、数据源还算靠谱。我个人习惯用它来做快速原型验证,正式生产环境可能会换别的数据源,但教学和实验用它足够了。
先安装:
pip install yfinance
然后获取数据,代码其实就几行:
import yfinance as yf
import pandas as pd
# 定义股票/ETF列表
tickers = ['SPY', 'TLT', 'GLD']
# 下载数据
data = yf.download(
tickers,
start='2020-01-01',
end='2023-12-31',
auto_adjust=True, # 自动调整价格(拆股、分红)
progress=False
)
# 只看收盘价
close_prices = data['Close']
print(close_prices.head())
这里有个细节要注意——auto_adjust=True。我建议你一直开着这个参数。为什么?因为股票会拆股、会分红,如果不调整,价格序列会出现断崖式跳变,你的回测结果会失真。我曾经见过有人没开这个参数,回测出来一个「分红套利策略」,其实全是数据假象。
小技巧: 如果你只需要收盘价,直接取 data['Close'] 就行。但如果你需要 OHLC(开盘、最高、最低、收盘),那就保留完整数据。我个人习惯把收盘价单独拎出来,其他数据按需取用。
2.3 处理缺失值
数据拿到手,第一件事就是检查缺失值。你想想看,不同市场的交易时间不一样,节假日也不一样,数据里出现 NaN 是家常便饭。
# 检查缺失值
print(close_prices.isnull().sum())
# 看看缺失值长什么样
print(close_prices[close_prices.isnull().any(axis=1)].head())
处理缺失值,我一般分三步走:
- 先看缺失比例——如果某只股票缺失超过 20%,我建议直接放弃,别硬补。
- 再看缺失模式——是随机缺失,还是集中在某个时间段?如果是节假日导致的,那很正常。
- 最后选填充方法——常用的有向前填充、插值、或者直接删除。
我个人最常用的是向前填充(ffill),因为金融数据有很强的惯性——今天的价格不知道,用昨天的凑合一下,逻辑上说得通。
# 向前填充
close_prices_filled = close_prices.ffill()
# 如果开头还有缺失,向后填充
close_prices_filled = close_prices_filled.bfill()
# 最后检查还有没有缺失
print(close_prices_filled.isnull().sum().sum())
注意: 千万别用均值填充金融数据!价格序列不是平稳的,均值填充会引入严重的偏差。我曾经见过有人用均值填充股价,结果回测出来的波动率直接砍半,策略看起来稳如老狗,实盘一跑就崩。
2.4 对齐时间戳
这一步是价差策略的核心。价差策略本质上是做两只或多只资产之间的相对价值交易,如果时间戳没对齐,你算出来的价差就是错的。
举个例子:A 股和美股,交易时间差 12 个小时。如果你直接把两边的收盘价拿来算价差,那相当于拿今天的 A 股收盘价和昨天的美股收盘价比——这能对吗?
对齐时间戳,我一般这么做:
# 只保留所有资产都有数据的日期
aligned_data = close_prices_filled.dropna()
# 或者用更灵活的方式:取交集
common_dates = close_prices_filled.index[
close_prices_filled.notna().all(axis=1)
]
aligned_data = close_prices_filled.loc[common_dates]
print(f"原始数据天数: {len(close_prices_filled)}")
print(f"对齐后数据天数: {len(aligned_data)}")
你会发现,对齐之后数据量少了不少。别心疼,这是好事——你排除了那些「假数据」。
经验之谈: 对于价差策略,我建议至少保留 500 个以上的对齐交易日,这样统计上才有意义。如果数据太少,你算出来的协整关系可能只是巧合。
2.5 完整的数据预处理流程
把上面几步串起来,就是一个完整的预处理流程。我把它封装成一个函数,方便复用:
def prepare_price_data(tickers, start, end):
"""
获取并预处理价格数据
"""
# 1. 获取数据
data = yf.download(
tickers, start=start, end=end,
auto_adjust=True, progress=False
)
prices = data['Close']
# 2. 处理缺失值
prices = prices.ffill().bfill()
# 3. 检查缺失比例
missing_ratio = prices.isnull().sum() / len(prices)
bad_tickers = missing_ratio[missing_ratio > 0.2].index.tolist()
if bad_tickers:
print(f"警告:以下资产缺失过多,已移除:{bad_tickers}")
prices = prices.drop(columns=bad_tickers)
# 4. 对齐时间戳
prices = prices.dropna()
return prices
# 使用示例
clean_prices = prepare_price_data(
tickers=['SPY', 'TLT', 'GLD', 'QQQ'],
start='2018-01-01',
end='2023-12-31'
)
print(clean_prices.head())
2.6 本章知识体系总览
下面这张图,帮你把整个数据获取与预处理的流程串起来。我画这张图的时候,特意把「检查缺失比例」这个步骤放大了——因为这是最容易忽略、也最容易出问题的地方。
2.7 避坑指南
最后,分享几个我踩过的坑,希望能帮你省点时间:
- 别信默认参数——
yfinance的默认参数不一定适合你的场景。比如auto_adjust默认是False,我建议你手动改成True。 - 注意时区——
yfinance返回的时间戳是 UTC 的,如果你做的是 A 股策略,记得转成北京时间。我吃过这个亏,回测里好好的,实盘一跑发现时间对不上。 - 数据量别贪多——我见过有人一口气下载 20 年的分钟级数据,结果电脑直接卡死。先想清楚你需要什么频率、什么时间范围,按需获取。
- 缓存是个好习惯——每次回测都重新下载数据,浪费时间也浪费流量。我一般会把预处理好的数据存成 CSV 或 Parquet,下次直接读文件。
我的习惯: 每次拿到新数据,我都会先画个图看看。把价格序列画出来,缺失值、异常值一眼就能看出来。别偷懒,可视化是最好的数据质量检查工具。
好了,数据准备好了。下一件事,就是怎么用这些数据来构建价差、设计策略。不过那是后面章节的事了,咱们先把数据这关过了再说。