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 包含 Open、High、Low、Close、Volume、Adj 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 就是前复权价格。但注意,它只调整了收盘价,Open、High、Low 还是原始数据。如果你要用这些列做技术分析,需要手动复权。
手动复权的逻辑很简单:
# 计算复权因子
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())
这个函数我用了好几年,基本没出过问题。但要注意,如果股票有多次拆股或分红,yfinance 的 Adj Close 可能不够精确。对于高频交易或期权定价,建议用专业数据源。
3.5 数据获取与清洗的核心流程
下面这张图,是我自己总结的数据处理流程。你看一眼,基本就明白整个链条了。
说白了,数据清洗就是三步走:拉数据、补空缺、调复权。每一步都有坑,但只要你按这个流程走,至少能保证数据质量在及格线以上。我刚开始做量化时,经常在数据上翻车,后来养成了每次回测前先跑一遍清洗函数的习惯,再也没出过低级错误。
ticker_clean.parquet 命名,读取速度比 CSV 快 10 倍。