第三章 数据获取与清洗:用yfinance搞定股票/期货数据

做价差交易的朋友,十有八九都栽在数据上。

我见过太多人,策略回测漂亮得不行,一上实盘就崩。为什么?数据没对齐,缺失值没处理,重采样搞错了方向。说白了,数据是交易系统的地基。地基不稳,楼盖得再高也得塌。

这一章,咱们就聊聊怎么用yfinance把数据拿下来,再把它收拾得干干净净。

3.1 为什么选yfinance?

市面上能拿数据的库不少,pandas-datareader、tushare、baostock……但我个人最常用的是yfinance。原因很简单:

  • 免费——不用注册,不用API Key,拿来就用
  • 覆盖面广——美股、ETF、期货、外汇,基本都有
  • 接口简洁——几行代码就能拿到日线、分钟线

当然,它也有缺点。比如数据偶尔会断,或者某些品种的历史数据不全。嗯,这里要注意:永远不要100%信任任何数据源。我自己的习惯是,拿yfinance的数据做初步分析,再用其他源交叉验证。

核心原则:数据获取只是第一步,清洗和验证才是重头戏。

3.2 安装与基本用法

先装包。如果你还没装,打开终端敲一行:

pip install yfinance

然后导入:

import yfinance as yf
import pandas as pd
import numpy as np

拿数据很简单。比如我想拿苹果(AAPL)从2023年1月1日到2024年1月1日的日线:

aapl = yf.download('AAPL', start='2023-01-01', end='2024-01-01')
print(aapl.head())

你会得到一个DataFrame,包含Open、High、Low、Close、Volume、Adj Close六列。注意,yfinance默认返回的是调整后的价格(Adj Close),这对做回测很重要。

小技巧:如果你想要多个品种,可以传一个列表进去。比如 yf.download(['AAPL', 'MSFT', 'GOOGL'], ...),它会返回一个MultiIndex的DataFrame。

3.3 处理缺失值——别让NaN毁了你的策略

拿到的数据,十有八九有缺失。为什么?节假日、停牌、数据源抽风……原因多了去了。

我遇到过最离谱的一次,某个期货品种连续三天没有交易数据。当时我用的策略是滚动回归,结果NaN一路传播,整个协整关系都算错了。嗯,从那以后,我养成了一个习惯:拿到数据先检查缺失值

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

# 看看缺失值集中在哪些位置
missing_idx = aapl[aapl.isnull().any(axis=1)].index
print(missing_idx)

处理缺失值,常用的方法有几种:

  • 直接删除——如果缺失比例很小(比如<1%),直接dropna()就行
  • 向前填充——用上一个有效值填充,适合非交易时段的数据
  • 插值——用前后值的均值填充,适合短期缺失
  • 用其他数据源补全——如果缺失太多,建议换源

我个人最常用的是向前填充,尤其是做日线数据时:

aapl_filled = aapl.fillna(method='ffill')
注意:千万不要在回测中使用未来的数据来填充缺失值。比如用后一天的收盘价填充前一天的NaN,这叫未来函数,会严重高估策略表现。

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

数据里偶尔会出现一些离谱的值。比如某只股票突然涨了1000%,或者成交量突然变成0。这些异常值,如果不处理,会严重扭曲你的统计结果。

我记得有一次做豆粕和豆油的价差分析,发现某个时间点的价差突然扩大了10倍。查了半天,原来是数据源把价格的小数点搞错了。你想想看,这种错误要是没发现,回测结果能信吗?

常用的异常值检测方法:

  • Z-score方法——计算每个值偏离均值的标准差倍数,超过3倍就标记为异常
  • IQR方法——用四分位距来识别离群点
  • 百分比截断——直接去掉上下1%或5%的极端值

我一般用IQR方法,因为它对极端值不那么敏感:

def detect_outliers_iqr(data, column, multiplier=1.5):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - multiplier * IQR
    upper_bound = Q3 + multiplier * IQR
    return data[(data[column] < lower_bound) | (data[column] > upper_bound)]

outliers = detect_outliers_iqr(aapl, 'Close')
print(f"发现 {len(outliers)} 个异常值")

发现异常值后,怎么处理?我的建议是:先人工确认,再决定是删除还是替换。有些异常值可能是真实的市场事件(比如财报发布后的跳空),这时候就不该删。

3.5 数据对齐——价差交易的核心

做价差交易,最怕什么?两个品种的数据没对齐。

比如你想做沪深300和中证500的配对交易。沪深300有数据,中证500也有数据,但它们的交易日不完全一样。有些日子沪深300有交易,中证500没有。如果你直接拿这两个序列做价差,结果就是一堆NaN。

数据对齐,说白了就是让两个序列在时间轴上保持一致。yfinance返回的数据默认是按日期索引的,所以对齐起来很方便:

# 拿两个品种的数据
sp500 = yf.download('^GSPC', start='2023-01-01', end='2024-01-01')
nasdaq = yf.download('^IXIC', start='2023-01-01', end='2024-01-01')

# 对齐:只保留两个序列都有的日期
aligned = pd.concat([sp500['Close'], nasdaq['Close']], axis=1, keys=['SP500', 'NASDAQ'])
aligned = aligned.dropna()  # 删除任一缺失的行
print(aligned.head())

这里有个细节要注意:对齐时用inner join还是outer join? 我个人习惯用inner join,只保留两个品种都有数据的日期。这样虽然会损失一些样本,但能保证价差计算的准确性。

实战经验:做期货价差时,不同合约的到期日不同,数据对齐会更复杂。我通常的做法是:先拿到主力连续合约,再对齐。如果某个合约换月了,要特别小心,别把新旧合约的价格混在一起。

3.6 重采样——从日线到周线、月线

有时候我们需要把高频数据降采样到低频。比如你拿的是日线数据,但策略是周频调仓,那就需要把日线重采样成周线。

重采样的核心是:确定聚合规则。开盘价用第一个值,收盘价用最后一个值,最高价用最大值,最低价用最小值,成交量用总和。

# 把日线重采样成周线
weekly = aapl.resample('W').agg({
    'Open': 'first',
    'High': 'max',
    'Low': 'min',
    'Close': 'last',
    'Volume': 'sum'
})
print(weekly.head())

常用的重采样频率:

  • 'W'——周线,默认以周日为结束
  • 'M'——月线,以月末为结束
  • 'Q'——季线
  • 'Y'——年线
避坑指南:重采样时,注意时间标签的边界。比如'W'默认是周日结束,但A股市场周五就收盘了。我建议用'W-FRI'来指定周五为周结束日。

3.7 本章知识体系

下面这张图,帮你理清数据获取与清洗的完整流程:

数据获取与清洗流程 1. 数据获取 yfinance.download() 2. 缺失值处理 ffill / dropna / 插值 3. 异常值检测 Z-score / IQR / 截断 4. 数据对齐 inner join / outer join 5. 重采样 日线→周线/月线 6. 输出清洗后数据 保存为CSV / 直接使用 关键要点: • 数据获取:选择可靠的数据源,多源交叉验证 • 缺失值:先检查比例,再选择处理方法 • 异常值:人工确认后再处理,不要盲目删除 • 数据对齐:价差交易必须使用inner join • 重采样:注意时间边界和聚合规则

3.8 完整示例:从获取到清洗的一站式代码

最后,给你一个完整的示例。把上面讲的内容串起来:

import yfinance as yf
import pandas as pd
import numpy as np

# 1. 获取数据
tickers = ['AAPL', 'MSFT']
data = yf.download(tickers, start='2023-01-01', end='2024-01-01')['Close']

# 2. 检查缺失值
print("缺失值统计:")
print(data.isnull().sum())

# 3. 处理缺失值(向前填充)
data_filled = data.fillna(method='ffill')

# 4. 检测异常值(IQR方法)
def remove_outliers(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return series.clip(lower, upper)

data_clean = data_filled.apply(remove_outliers)

# 5. 数据对齐(已经对齐了,但再确认一下)
data_aligned = data_clean.dropna()

# 6. 重采样到周线
weekly = data_aligned.resample('W-FRI').last()

print("清洗完成!")
print(weekly.head())

这段代码,你直接复制就能用。把tickers换成你自己的品种,把日期范围改一改,就能跑起来。

最后提醒一句:数据清洗没有一劳永逸的方案。不同的策略、不同的品种,对数据的要求都不一样。我的建议是:每次拿到新数据,都先做一遍完整的清洗流程。别偷懒,偷懒的代价是策略失效。

公众号:蓝海资料掘金营,微信deep3321