3、数据获取与清洗:使用yfinance获取历史数据,处理缺失值,调整复权价格

做量化回测,第一步就是搞数据。没有干净的数据,后面所有模型都是空中楼阁。我个人习惯用 yfinance 这个库,免费、方便,纳斯达克的数据基本都能拉下来。但注意,免费的东西有坑,我踩过不少,今天一并告诉你。

3.1 安装与基础用法

先装库,一行命令搞定:

pip install yfinance

然后拉取苹果公司的历史数据。代码很简单:

import yfinance as yf

# 下载 AAPL 从 2020-01-01 到 2023-12-31
data = yf.download('AAPL', start='2020-01-01', end='2023-12-31')
print(data.head())

你会看到返回的 DataFrame 包含 OpenHighLowCloseVolumeAdj Close 六列。嗯,这里要注意,Adj Close 是复权收盘价,后面会细说。

小技巧: 如果你要同时拉多只股票,可以传一个列表进去。比如 yf.download(['AAPL', 'MSFT', 'GOOGL'], ...)。返回的 DataFrame 会带 MultiIndex,处理起来稍微麻烦点,但习惯就好。

3.2 处理缺失值——别让空数据毁了你的策略

现实世界的数据很少是完美的。节假日、停牌、数据源故障,都会导致缺失值。我在项目中遇到过,某次回测跑出来收益惊人,结果发现是因为某天数据缺失,程序自动用前值填充,导致买入信号误判。

常见的处理方式有三种:

方法 说明 适用场景
向前填充 用上一个非空值填充 非交易日的缺失(如周末)
向后填充 用下一个非空值填充 数据开头缺失
直接删除 丢掉含 NaN 的行 缺失比例很小(<5%)

我个人习惯先检查缺失比例:

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

# 如果缺失很少,直接删除
data_clean = data.dropna()

# 如果缺失较多,用向前填充
data_filled = data.fillna(method='ffill')
避坑指南: 我曾经用 ffill 填充了连续三天的缺失数据,结果那三天恰好是公司发布重大利空消息后的停牌期。复牌后股价暴跌,但我的策略因为用了前值,完全没检测到风险。所以,填充前一定要搞清楚缺失的原因。

3.3 复权价格——为什么不能用原始收盘价?

你想想看,一只股票今天收盘 100 元,明天分红 10 元,后天开盘价直接变成 90 元。如果你用原始收盘价做回测,K 线图上会出现一个巨大的跳空缺口,技术指标全乱套。这就是为什么需要复权。

复权分两种:

  • 前复权: 调整历史价格,让价格走势连续。当前价格不变,历史价格按比例缩小。这是最常用的方式。
  • 后复权: 调整当前价格,让历史价格不变。当前价格会变得很大,适合看长期收益。

yfinance 默认返回的 Adj Close 就是前复权价格。但注意,它只调整了收盘价,OpenHighLow 还是原始数据。如果你要用这些列做技术分析,需要手动复权。

手动复权的逻辑很简单:

# 计算复权因子
adj_factor = data['Adj Close'] / data['Close']

# 对 OHLC 进行复权
data['Open_adj'] = data['Open'] * adj_factor
data['High_adj'] = data['High'] * adj_factor
data['Low_adj'] = data['Low'] * adj_factor
data['Close_adj'] = data['Adj Close']  # 直接用 yfinance 提供的
核心要点: 回测时一定要用复权价格。否则分红、送股、拆股都会让你的策略信号失真。我见过有人用原始价格跑回测,结果策略在分红日频繁开仓,手续费都亏光了。

3.4 数据清洗的完整流程

把上面几步串起来,就是一个完整的数据清洗 pipeline。我一般这样写:

def clean_stock_data(ticker, start, end):
    # 1. 下载数据
    raw = yf.download(ticker, start=start, end=end)
    
    # 2. 处理缺失值
    if raw.isnull().sum().sum() > 0:
        # 检查缺失比例
        missing_pct = raw.isnull().sum() / len(raw)
        if missing_pct.max() < 0.05:
            raw = raw.dropna()
        else:
            raw = raw.fillna(method='ffill').fillna(method='bfill')
    
    # 3. 复权处理
    adj_factor = raw['Adj Close'] / raw['Close']
    raw['Open'] = raw['Open'] * adj_factor
    raw['High'] = raw['High'] * adj_factor
    raw['Low'] = raw['Low'] * adj_factor
    raw['Close'] = raw['Adj Close']
    
    # 4. 只保留需要的列
    clean = raw[['Open', 'High', 'Low', 'Close', 'Volume']]
    
    return clean

# 使用示例
aapl_clean = clean_stock_data('AAPL', '2020-01-01', '2023-12-31')
print(aapl_clean.head())

这个函数我用了好几年,基本没出过问题。但要注意,如果股票有多次拆股或分红,yfinanceAdj Close 可能不够精确。对于高频交易或期权定价,建议用专业数据源。

3.5 数据获取与清洗的核心流程

下面这张图,是我自己总结的数据处理流程。你看一眼,基本就明白整个链条了。

数据获取与清洗核心流程 步骤1:数据获取 yfinance.download() 步骤2:缺失值处理 ffill / bfill / dropna 步骤3:复权处理 Adj Close 复权 缺失比例 < 5%? 是 → dropna / 否 → ffill 输出:清洗后的OHLC数据 可用于回测、技术分析 注意:复权因子 = Adj Close / Close,需对OHLC四列分别调整

说白了,数据清洗就是三步走:拉数据、补空缺、调复权。每一步都有坑,但只要你按这个流程走,至少能保证数据质量在及格线以上。我刚开始做量化时,经常在数据上翻车,后来养成了每次回测前先跑一遍清洗函数的习惯,再也没出过低级错误。

个人经验: 建议把清洗后的数据存成 CSV 或 Parquet 格式,下次回测直接读取,省得每次重新下载。我一般按 ticker_clean.parquet 命名,读取速度比 CSV 快 10 倍。

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