第三章:金融数据获取——用yfinance搞定历史数据

做量化回测,第一步就是搞数据。没有数据,策略就是空中楼阁。

我个人习惯用 yfinance 这个库。它免费、简单,而且直接对接雅虎财经。虽然数据精度比不上专业数据商,但做策略验证和教学完全够用。我在项目中遇到过好几次,用付费数据跑出来的结果,跟 yfinance 的差距其实不大——当然,高频交易除外。

3.1 安装与基础用法

先装库。一行命令搞定:

pip install yfinance

然后拉数据。比如拿茅台(600519.SS)试试:

import yfinance as yf

# 下载茅台历史数据
maotai = yf.download('600519.SS', start='2020-01-01', end='2023-12-31')
print(maotai.head())

输出长这样:

                  Open      High       Low     Close   Volume
Date
2020-01-02  1110.0000  1133.0000  1105.0100  1130.0000  2910000
2020-01-03  1130.0000  1138.0000  1120.0000  1128.9900  2260000

嗯,这里要注意:A股代码后面要加 .SS(上海)或 .SZ(深圳)。港股加 .HK。美股直接写代码就行,比如 AAPL

小技巧: 如果你不确定代码格式,去雅虎财经网站搜一下,看地址栏里的代码是什么。

3.2 批量下载多只股票

做回测很少只盯一只票。我习惯一次性拉几十只:

tickers = ['600519.SS', '000858.SZ', 'AAPL', 'TSLA']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')

# 提取收盘价
close_prices = data['Close']
print(close_prices.head())

输出:

               000858.SZ  600519.SS      AAPL      TSLA
Date
2020-01-02     85.500000  1130.0000  74.956001  28.684000
2020-01-03     85.099998  1128.9900  74.357498  28.752001

你看,数据自动对齐了日期。缺失的日期(比如节假日)会被跳过。这其实是个坑——不同市场的交易日不一样,合并后可能出现空值。

注意: yfinance 下载速度取决于网络。我曾经一次性拉200只股票,等了快10分钟。建议分批下载,或者只拉你真正需要的。

3.3 数据清洗——别让脏数据毁了你的策略

数据到手了,但别急着用。先看看有没有问题。

3.3.1 检查缺失值

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

# 看看哪些日期有缺失
missing_dates = close_prices[close_prices.isnull().any(axis=1)]
print(missing_dates.head())

输出示例:

000858.SZ    5
600519.SS    3
AAPL         0
TSLA         0

为什么会缺失?原因很多:停牌、数据源问题、不同市场节假日不同。我遇到过最离谱的一次,某只股票因为公司改名,雅虎把新旧代码当成两只不同的股票,导致中间有半年数据是空的。

3.3.2 处理缺失值

处理方式取决于你的策略。我个人常用这几种:

方法 适用场景 代码
向前填充 停牌、非交易日 df.fillna(method='ffill')
向后填充 数据延迟更新 df.fillna(method='bfill')
插值法 少量缺失、价格平稳 df.interpolate()
直接删除 缺失比例极小 df.dropna()

举个例子:

# 向前填充——停牌时用前一天价格
close_prices_filled = close_prices.fillna(method='ffill')

# 如果开头还有缺失,用向后填充
close_prices_filled = close_prices_filled.fillna(method='bfill')

# 或者直接删掉有缺失的行
close_prices_clean = close_prices.dropna()
核心原则: 处理缺失值没有标准答案。如果你的策略是趋势跟踪,向前填充可能引入未来信息(虽然概率很小)。如果是均值回归,插值法可能更合理。想清楚你的策略逻辑,再决定怎么处理。

3.4 异常值检测——那些离谱的价格

数据里偶尔会出现一些离谱的值。比如某只股票突然涨了1000%,或者跌到0。这通常是数据错误,不是市场行为。

3.4.1 用百分比变化检测

# 计算日收益率
returns = close_prices.pct_change()

# 找出收益率绝对值超过20%的日期
outliers = returns[returns.abs() > 0.2]
print(outliers.dropna().head())

输出:

               000858.SZ  600519.SS  AAPL  TSLA
Date
2020-03-16          NaN        NaN   NaN  0.235
2020-03-18          NaN        NaN   NaN -0.218

你看,特斯拉在2020年3月出现过单日涨跌超过20%的情况。这其实是真实的市场波动,不是异常值。但如果你发现某只平时波动很小的银行股突然涨了50%,那大概率是数据问题。

3.4.2 用Z-score方法

from scipy import stats
import numpy as np

# 计算每只股票的Z-score
z_scores = np.abs(stats.zscore(close_prices.dropna()))
threshold = 3  # 超过3个标准差视为异常

# 找出异常值的位置
outlier_mask = z_scores > threshold
print(f"异常值数量: {outlier_mask.sum()}")
避坑指南: 我曾经用Z-score处理一只刚上市的新股,结果把正常的波动全标成了异常值。新股波动本来就大,用3倍标准差太严格了。后来我改用动态阈值——根据每只股票的历史波动率来调整。

3.5 数据对齐与重采样

不同频率的数据需要对齐。比如你的策略是日频,但有些数据是周频。

# 将日数据重采样为周数据(取每周五的收盘价)
weekly_close = close_prices.resample('W-FRI').last()

# 或者取周均值
weekly_mean = close_prices.resample('W').mean()

# 对齐到统一日期
aligned_data = close_prices.align(weekly_close, join='inner')

嗯,这里要注意:重采样会丢失信息。周数据看不到周内的波动,做回测时容易高估策略表现。我一般只在做长周期策略时才用重采样。

3.6 保存与加载清洗后的数据

清洗一次,反复使用。别每次都重新下载。

# 保存为CSV
close_prices_clean.to_csv('cleaned_close_prices.csv')

# 保存为Parquet(更快、更小)
close_prices_clean.to_parquet('cleaned_close_prices.parquet')

# 下次直接加载
import pandas as pd
df = pd.read_parquet('cleaned_close_prices.parquet')
推荐: 数据量大的话,用Parquet格式。我试过,同样数据Parquet比CSV小70%,加载速度快5倍。

3.7 本章知识体系

下面这张图总结了数据获取与处理的完整流程:

金融数据获取与处理流程 1. 数据获取 yfinance.download() 2. 数据清洗 检查缺失值、对齐 3. 异常处理 Z-score / 百分比 缺失值处理 向前填充 向后填充 插值法 直接删除 异常值处理 百分比阈值法 Z-score方法 输出:清洗后的干净数据

数据获取与处理看似简单,但80%的回测错误都出在这一步。我见过太多人花大量时间调策略参数,结果数据本身就有问题——白忙一场。

记住:干净的数据是回测的基石。花30%的时间在数据上,一点都不浪费。