3、数据获取与预处理:用yfinance搞定股票数据

做协整配对,第一步就是拿数据。这一步看似简单,但坑不少。我刚开始做量化那会儿,就因为在数据预处理上偷懒,结果回测曲线漂亮得不行,实盘一跑直接傻眼——数据对齐出了问题。

说白了,数据质量决定了策略的生死。你模型再牛,喂进去的是脏数据,出来的结果就是垃圾。这一节,咱们就聊聊怎么用yfinance拿数据,怎么清洗,怎么处理缺失值,以及为什么要做重采样。

3.1 用yfinance获取股票数据

yfinance这个库,我个人觉得是散户做量化最友好的工具。免费、简单、数据覆盖广。你不需要去申请什么API Key,一行代码就能拿到历史数据。

核心函数:yf.download()

先看个最简单的例子:

import yfinance as yf

# 获取贵州茅台的历史数据
data = yf.download('600519.SS', start='2020-01-01', end='2023-12-31')
print(data.head())

这里要注意,A股代码后面要加后缀。上交所是.SS,深交所是.SZ。港股是.HK。美股就直接写代码,比如AAPL

我曾经踩过的坑:yfinance默认返回的数据是pandas DataFrame,但列名是MultiIndex。如果你同时下载多只股票,列名会变成('Adj Close', '600519.SS')这种形式。取数据的时候要小心,别直接用df['Close'],会报错。

多只股票一起下载,我习惯这样写:

import yfinance as yf

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

# 只取调整收盘价
adj_close = data['Adj Close']
print(adj_close.head())

为什么用Adj Close而不是Close?因为调整收盘价已经考虑了分红、拆股等因素。做协整分析时,价格序列需要连续、无跳变。用Adj Close能省去很多手动调整的麻烦。

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

数据拿到手,第一件事不是直接算协整,而是清洗。你想想看,如果某只股票因为停牌,连续几天价格都是同一个数,那算出来的相关性肯定是假的。

我一般会做这几步清洗:

  • 检查重复索引:确保日期没有重复
  • 检查价格异常:比如价格为0或负数,这明显是数据错误
  • 检查涨跌幅异常:单日涨跌超过50%的,大概率是数据问题

举个例子:

# 检查重复日期
if adj_close.index.duplicated().any():
    print('发现重复日期,正在处理...')
    adj_close = adj_close[~adj_close.index.duplicated(keep='first')]

# 检查价格为0或负数的行
invalid_prices = (adj_close <= 0).any(axis=1)
if invalid_prices.any():
    print(f'发现{invalid_prices.sum()}天价格异常数据')
    adj_close = adj_close[~invalid_prices]

我的小技巧:清洗完数据后,我会用df.describe()快速看一眼统计特征。如果某只股票的标准差突然变得特别大,或者最小值是0,那就要警惕了。

3.3 缺失值处理——填还是不填?

缺失值在金融数据里太常见了。停牌、节假日、数据源问题,都会导致某天没有价格。处理缺失值,没有银弹,得看情况。

我常用的几种方法:

方法 适用场景 缺点
前向填充(ffill) 停牌、非交易日 会引入未来信息?其实不会,因为用的是过去的数据
后向填充(bfill) 数据延迟发布 会引入未来信息,回测时慎用
插值法 少量缺失,且价格走势平滑 计算复杂,对突变不敏感
直接删除 缺失比例很小(<5%) 会损失数据点

我个人最常用的是前向填充。原因很简单:如果某只股票今天停牌,那它今天的价格应该和昨天一样。这是最符合市场逻辑的处理方式。

# 前向填充缺失值
adj_close_filled = adj_close.fillna(method='ffill')

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

# 检查是否还有缺失
print(f'剩余缺失值数量:{adj_close_filled.isnull().sum().sum()}')

注意:如果某只股票缺失数据超过20%,我建议直接放弃这只股票。缺失太多,填充出来的数据可信度太低,做协整分析没有意义。

3.4 重采样——统一时间频率

做协整配对,时间频率必须统一。你不能拿日线数据和周线数据去算协整,那结果肯定不对。

yfinance默认返回的是日线数据。但有时候我们需要周线或月线数据来做更长周期的分析。这时候就要用到重采样。

重采样的核心逻辑:

  • 日线转周线:取每周最后一个交易日的价格
  • 日线转月线:取每月最后一个交易日的价格
  • 日线转小时线:这个yfinance原生不支持,需要另外找数据源
# 日线转周线:取每周最后一个交易日
weekly_data = adj_close_filled.resample('W').last()

# 日线转月线:取每月最后一个交易日
monthly_data = adj_close_filled.resample('M').last()

# 日线转15分钟线(需要更细粒度的数据)
# 注意:yfinance免费版不支持日内数据
# 可以用其他数据源,比如polygon.io

这里有个细节:resample('W')默认以周日为一周的最后一天。但A股市场周五是最后一个交易日。所以我一般会这样写:

# 指定以周五为周的最后一天
weekly_data = adj_close_filled.resample('W-FRI').last()

我的经验:做协整配对,日线数据就够用了。周线和月线数据样本点太少,统计显著性不够。除非你做的是超长周期配对,比如国债期货和股指期货那种。

3.5 数据预处理完整流程

把上面这些步骤串起来,我一般会写成一个函数。这样每次跑新数据,直接调用就行。

def prepare_data(tickers, start, end, freq='D'):
    """
    完整的数据获取与预处理流程
    """
    # 1. 获取数据
    data = yf.download(tickers, start=start, end=end)
    prices = data['Adj Close']
    
    # 2. 清洗:去除重复索引和异常值
    prices = prices[~prices.index.duplicated(keep='first')]
    prices = prices[(prices > 0).all(axis=1)]
    
    # 3. 缺失值处理:前向填充
    prices = prices.fillna(method='ffill').fillna(method='bfill')
    
    # 4. 重采样
    if freq == 'W':
        prices = prices.resample('W-FRI').last()
    elif freq == 'M':
        prices = prices.resample('M').last()
    
    # 5. 删除缺失过多的股票
    missing_ratio = prices.isnull().mean()
    valid_tickers = missing_ratio[missing_ratio < 0.2].index
    prices = prices[valid_tickers]
    
    return prices

# 使用示例
prices = prepare_data(['600519.SS', '000858.SZ'], '2020-01-01', '2023-12-31')
print(prices.head())

3.6 本章知识体系

下面这张图,帮你理清数据预处理的完整流程:

数据获取与预处理流程 1. 数据获取 yfinance.download() 2. 数据清洗 去重、去异常值 3. 缺失值处理 前向/后向填充 4. 重采样 日线→周线/月线 5. 输出数据 干净、对齐的价格序列 关键点:数据对齐、频率统一、缺失值处理 最终输出:可用于协整检验的干净价格序列

数据预处理这一步,看似基础,但决定了后续所有分析的可信度。我见过太多人在这上面翻车——数据没对齐就跑去算协整,结果发现两个完全不相关的股票,协整检验居然通过了。说白了,那是数据造假,不是策略有效。

嗯,这一节的内容就到这里。数据准备好了,下一节咱们就可以开始真正的协整检验了。


专注资料整理