数据获取与清洗:从零搭建可靠的数据管道
做波动率模型,最怕什么?
不是模型不收敛,而是数据本身就有问题。我见过太多人花几个月调参,最后发现是原始数据里藏着几个异常值。嗯,今天我们就来聊聊数据获取与清洗——这个看似基础、实则决定成败的环节。
2.1 为什么数据清洗如此重要?
波动率模型对输入数据极其敏感。你想想看,一个错误的股息调整,可能导致收益率序列出现0.5%的偏差。对于日频波动率来说,这个误差足以让模型预测完全偏离。
我个人习惯把数据清洗分为三个层次:
- 完整性:有没有缺失值?停牌日怎么处理?
- 一致性:价格是否经过复权?股息和拆股是否调整?
- 准确性:有没有明显的价格错误?比如某天突然涨了100倍?
核心原则:宁可少数据,不要坏数据。波动率建模中,一个坏点可能毁掉整个协方差矩阵的估计。
2.2 使用yfinance获取数据
yfinance是我最常用的免费数据源。它直接对接雅虎财经,覆盖全球主要市场。获取数据很简单,但有几个坑要注意。
import yfinance as yf
import pandas as pd
import numpy as np
# 获取标普500指数数据
ticker = "^GSPC"
start_date = "2010-01-01"
end_date = "2023-12-31"
data = yf.download(ticker, start=start_date, end=end_date)
print(data.head())
这段代码跑完,你会得到一个包含Open、High、Low、Close、Volume、Adj Close的DataFrame。注意,Adj Close是调整后的收盘价,已经考虑了股息和拆股。
我的习惯:永远使用Adj Close计算收益率,而不是Close。因为Close没有考虑股息,会导致收益率被低估。
2.3 处理缺失值
金融数据天然有缺失。节假日、停牌、数据源故障,都会导致NaN出现。怎么处理?
我曾经在项目中遇到一个情况:某只股票因为重大资产重组停牌了3个月,复牌后直接跌停。如果简单用前值填充,模型会完全忽略这个跳空缺口。所以,处理缺失值要分情况:
# 检查缺失值
print(data.isnull().sum())
# 方法1:前向填充(适用于非交易日)
data['Adj Close'].fillna(method='ffill', inplace=True)
# 方法2:删除缺失行(适用于少量缺失)
data.dropna(inplace=True)
# 方法3:插值(适用于短期缺失)
data['Adj Close'].interpolate(method='linear', inplace=True)
| 缺失情况 | 推荐方法 | 原因 |
|---|---|---|
| 非交易日(周末、节假日) | 前向填充 | 价格不变,符合市场逻辑 |
| 停牌1-2天 | 插值 | 平滑过渡,避免跳空 |
| 长期停牌(>5天) | 删除 | 插值会引入虚假信息 |
| 数据源错误 | 删除 | 宁可缺失,不要错误 |
注意:不要对收益率序列做前向填充!收益率是百分比变化,填充会扭曲波动率估计。我见过有人这么干,结果模型预测的波动率比实际低了30%。
2.4 调整股息与拆股
股息和拆股是收益率计算中的两个大坑。先说股息:
- 除息日当天,股价会下跌(大致等于股息金额)
- 如果不调整,当天的收益率会异常为负
- Adj Close已经考虑了股息,直接用就行
再说拆股:
- 1拆2,股价减半,股数翻倍
- 如果不调整,收益率会出现-50%的假象
- yfinance的Adj Close会自动处理拆股
# 验证调整效果:计算调整前后的收益率对比
data['Return_Adj'] = data['Adj Close'].pct_change()
data['Return_Close'] = data['Close'].pct_change()
# 查看除息日附近的差异
ex_div_date = '2023-06-15' # 假设的除息日
print(data.loc[ex_div_date, ['Return_Adj', 'Return_Close']])
你会发现,在除息日,Return_Close会有一个明显的负值,而Return_Adj则平滑很多。这就是调整的价值。
2.5 计算日收益率序列
终于到了核心环节。日收益率是波动率模型的基础输入。我通常使用对数收益率,因为它具有可加性,且更符合正态分布假设。
# 计算对数收益率
data['Log_Return'] = np.log(data['Adj Close'] / data['Adj Close'].shift(1))
# 计算简单收益率
data['Simple_Return'] = data['Adj Close'].pct_change()
# 删除第一行的NaN
data.dropna(inplace=True)
# 检查收益率的基本统计
print(data[['Log_Return', 'Simple_Return']].describe())
对数收益率和简单收益率在数值上很接近(当收益率较小时),但对数收益率在数学上更优雅。我个人习惯用对数收益率做建模,用简单收益率做回测——因为简单收益率更直观,方便计算组合收益。
关键检查点:收益率序列应该围绕0波动,均值接近0,标准差反映波动率水平。如果均值明显偏离0(比如0.1%以上),说明数据可能有问题。
2.6 数据清洗的完整流程
把上面所有步骤串起来,就是一个完整的数据管道。我建议你把它封装成一个函数,方便复用。
def clean_market_data(ticker, start, end):
"""
完整的数据清洗流程
"""
# 1. 获取数据
data = yf.download(ticker, start=start, end=end)
# 2. 处理缺失值
data['Adj Close'].fillna(method='ffill', inplace=True)
data.dropna(inplace=True)
# 3. 计算收益率
data['Return'] = data['Adj Close'].pct_change()
data['Log_Return'] = np.log(data['Adj Close'] / data['Adj Close'].shift(1))
# 4. 去除异常值(3个标准差之外)
mean = data['Return'].mean()
std = data['Return'].std()
data['Return_Trimmed'] = data['Return'].clip(mean - 3*std, mean + 3*std)
# 5. 最终清理
data.dropna(inplace=True)
return data
# 使用示例
sp500_data = clean_market_data("^GSPC", "2010-01-01", "2023-12-31")
print(f"数据范围:{sp500_data.index[0]} 到 {sp500_data.index[-1]}")
print(f"有效交易日数:{len(sp500_data)}")
避坑指南:我曾经在回测中发现,某只股票的收益率序列在2015年突然出现一个-20%的异常值。查了半天,原来是数据源把一次特别股息当成了普通股息处理。所以,拿到数据后,先画个收益率时序图,肉眼扫一遍异常点。
2.7 本章知识体系
下面这张图总结了数据获取与清洗的核心流程。你可以把它当作一个检查清单,每次做波动率建模前,先过一遍。
数据清洗看起来琐碎,但它是整个波动率建模的基石。我见过太多人跳过这一步,直接跑模型,结果浪费了大量时间在调试上。记住:好的数据,胜过复杂的模型。