第一章:数据获取与清洗——用yfinance搞定期权数据
做波动率期限结构分析,第一步就是搞数据。这活儿看着简单,其实坑不少。我刚开始做期权量化那会儿,光在数据清洗上就栽过好几个跟头。今天咱们就聊聊,怎么用Python把期权数据从yfinance里拽出来,再收拾得干干净净。
1.1 为什么选yfinance?
市面上能拿期权数据的渠道不少,但yfinance有个好处——免费、简单、覆盖面广。虽然它数据精度比不上Bloomberg或者Wind,但做策略研究和回测完全够用。我个人习惯先用yfinance快速验证想法,等策略成熟了再换专业数据源。
说白了,yfinance就像一把瑞士军刀。功能不算最强,但胜在顺手。你想想看,几行代码就能拿到标普500期权的完整链数据,这性价比没谁了。
1.2 安装与导入
先装包,这个简单:
pip install yfinance pandas numpy
导入的时候我建议这样写:
import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
嗯,这里要注意一点——yfinance版本更新挺勤快的。我去年有个项目,就因为版本不一致,跑出来的数据格式全变了。建议你固定版本号:
pip install yfinance==0.2.28
1.3 获取期权链数据
核心逻辑其实就三步:选标的、拿期权链、提取具体合约。咱们以SPY为例:
# 获取SPY期权链
spy = yf.Ticker("SPY")
# 查看所有到期日
expirations = spy.options
print(f"可用到期日: {expirations[:5]}") # 只看前5个
# 获取最近到期日的期权链
opt_chain = spy.option_chain(expirations[0])
# 看涨和看跌数据
calls = opt_chain.calls
puts = opt_chain.puts
这里有个坑——option_chain()返回的数据里,strike列是行权价,lastPrice是最新成交价,impliedVolatility就是我们要的隐含波动率。我曾经遇到过数据里impliedVolatility全是NaN的情况,后来发现是当天没有交易,换了个交易日就好了。
1.4 数据清洗——别让脏数据毁了你的模型
拿到原始数据后,清洗是重头戏。我总结了一套标准流程:
- 去空值:删除隐含波动率或成交量为空的记录
- 去异常值:隐含波动率超过200%或低于5%的,基本是数据错误
- 去深度实值/虚值:Delta绝对值小于0.05或大于0.95的合约,流动性太差
- 去临近到期:到期日小于3天的合约,价格行为太诡异
代码实现长这样:
def clean_option_data(df, min_dte=3, max_iv=2.0, min_iv=0.05):
"""
清洗期权数据
df: 原始DataFrame
min_dte: 最小到期天数
"""
# 计算到期天数
df['dte'] = (df['expiration'] - datetime.now()).dt.days
# 清洗逻辑
clean_df = df[
(df['impliedVolatility'].notna()) & # 去空值
(df['volume'] > 0) & # 去零成交量
(df['impliedVolatility'] < max_iv) & # 去异常高IV
(df['impliedVolatility'] > min_iv) & # 去异常低IV
(df['dte'] >= min_dte) # 去临近到期
]
return clean_df
避坑指南:我曾经在清洗时忘了处理除权除息日的数据。那天SPY分红,期权价格剧烈波动,隐含波动率直接飙到300%+。如果不剔除这些异常点,整个期限结构曲线都会变形。
1.5 构建标准化数据集
清洗完数据后,我们需要把它整理成统一格式。我个人习惯用这种结构:
| 字段名 | 类型 | 说明 |
|---|---|---|
| ticker | str | 标的代码 |
| date | datetime | 数据日期 |
| expiration | datetime | 到期日 |
| strike | float | 行权价 |
| option_type | str | call/put |
| implied_vol | float | 隐含波动率 |
| delta | float | Delta值 |
| volume | int | 成交量 |
| open_interest | int | 未平仓量 |
| dte | int | 到期天数 |
标准化函数:
def standardize_option_data(raw_df, ticker, data_date):
"""
将原始数据转为标准化格式
"""
std_df = pd.DataFrame()
std_df['ticker'] = ticker
std_df['date'] = data_date
std_df['expiration'] = raw_df['expiration']
std_df['strike'] = raw_df['strike']
std_df['option_type'] = raw_df['option_type']
std_df['implied_vol'] = raw_df['impliedVolatility']
std_df['delta'] = raw_df['delta']
std_df['volume'] = raw_df['volume']
std_df['open_interest'] = raw_df['openInterest']
std_df['dte'] = (raw_df['expiration'] - data_date).dt.days
return std_df
1.6 完整流程——从零到标准化数据集
把上面所有步骤串起来,就是一个完整的pipeline:
def get_clean_option_data(ticker, target_date=None):
"""
一站式获取并清洗期权数据
"""
if target_date is None:
target_date = datetime.now()
# 获取数据
stock = yf.Ticker(ticker)
expirations = stock.options
all_data = []
for exp in expirations[:5]: # 取最近5个到期日
chain = stock.option_chain(exp)
# 合并call和put
calls = chain.calls.copy()
puts = chain.puts.copy()
calls['option_type'] = 'call'
puts['option_type'] = 'put'
combined = pd.concat([calls, puts])
combined['expiration'] = pd.to_datetime(exp)
all_data.append(combined)
# 合并所有到期日
full_df = pd.concat(all_data, ignore_index=True)
# 清洗
clean_df = clean_option_data(full_df)
# 标准化
std_df = standardize_option_data(clean_df, ticker, target_date)
return std_df
# 使用示例
spy_data = get_clean_option_data("SPY")
print(f"获取到 {len(spy_data)} 条有效期权数据")
小技巧:yfinance获取期权数据时,建议加上time.sleep(1)避免请求过快被封。我一般会写个循环,每次请求间隔1-2秒。
1.7 数据质量检查
数据清洗完,别急着用。先做个快速检查:
# 检查各到期日的合约数量
print(spy_data.groupby('dte').size())
# 检查隐含波动率的分布
print(spy_data['implied_vol'].describe())
# 检查是否有重复数据
print(f"重复数据量: {spy_data.duplicated().sum()}")
如果发现某个到期日的合约数量特别少,或者隐含波动率分布明显异常,那就要回头看看是不是清洗条件设得太严了。我一般会先宽松清洗,再逐步收紧,找到那个平衡点。
1.8 本章知识体系
下面这张图,把整个数据获取与清洗的流程串起来了:
这张图把整个流程分成了六个步骤。从数据获取开始,到最终输出标准化数据集。中间的数据清洗和标准化是最关键的两步,也是我花时间最多的地方。
核心要点:数据清洗不是越严格越好,也不是越宽松越好。关键是要理解你的策略需要什么样的数据。做波动率期限结构分析,我们最关心的是隐含波动率的准确性和一致性。清洗标准要围绕这个目标来设定。
好了,数据获取和清洗这块就聊到这儿。下一章咱们会深入探讨如何利用这些标准化数据,构建真正的波动率期限结构曲线。
公众号:蓝海资料掘金营,微信deep3321