数据获取与预处理:让数据说话的第一步

做量化回测,最怕什么?

我个人觉得,最怕的就是数据本身有问题。你策略再牛,模型再花哨,数据是脏的,结果就是垃圾进垃圾出。今天咱们就来聊聊,怎么用 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())

处理缺失值,我一般分三步走:

  1. 先看缺失比例——如果某只股票缺失超过 20%,我建议直接放弃,别硬补。
  2. 再看缺失模式——是随机缺失,还是集中在某个时间段?如果是节假日导致的,那很正常。
  3. 最后选填充方法——常用的有向前填充、插值、或者直接删除。

我个人最常用的是向前填充(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 本章知识体系总览

下面这张图,帮你把整个数据获取与预处理的流程串起来。我画这张图的时候,特意把「检查缺失比例」这个步骤放大了——因为这是最容易忽略、也最容易出问题的地方。

数据获取与预处理流程 1. 数据获取 yfinance.download() 2. 缺失值处理 ffill() / bfill() 3. 时间戳对齐 dropna() 检查缺失比例 > 20% 则移除该资产 ✅ 通过 → 继续对齐 ❌ 不通过 → 移除资产 最终输出:干净的对齐数据 可用于价差计算和策略回测

2.7 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 别信默认参数——yfinance 的默认参数不一定适合你的场景。比如 auto_adjust 默认是 False,我建议你手动改成 True
  • 注意时区——yfinance 返回的时间戳是 UTC 的,如果你做的是 A 股策略,记得转成北京时间。我吃过这个亏,回测里好好的,实盘一跑发现时间对不上。
  • 数据量别贪多——我见过有人一口气下载 20 年的分钟级数据,结果电脑直接卡死。先想清楚你需要什么频率、什么时间范围,按需获取。
  • 缓存是个好习惯——每次回测都重新下载数据,浪费时间也浪费流量。我一般会把预处理好的数据存成 CSV 或 Parquet,下次直接读文件。

我的习惯: 每次拿到新数据,我都会先画个图看看。把价格序列画出来,缺失值、异常值一眼就能看出来。别偷懒,可视化是最好的数据质量检查工具。

好了,数据准备好了。下一件事,就是怎么用这些数据来构建价差、设计策略。不过那是后面章节的事了,咱们先把数据这关过了再说。


蓝海数据掘金营,专注资料整理