第七章:数据获取与清洗——期权链数据API与标准化网格构建
做期权波动率曲面统计套利,第一步就是搞定数据。我见过太多人把精力全放在模型上,结果数据源一塌糊涂,最后回测漂亮、实盘翻车。说白了,数据质量决定了你的策略天花板。
这一章,咱们就聊聊怎么拿到干净的期权链数据,怎么清洗它,再构建出标准化的Delta/期限网格。嗯,这里每一步都有坑,我踩过不少,今天一并告诉你。
7.1 期权链数据API:从哪里拿数据?
我个人习惯把数据源分成两类:免费的和收费的。免费的有Yahoo Finance,收费的代表是盈透证券(IBKR)。你想想看,做套利策略对数据精度要求极高,免费数据有时候会坑你。
7.1.1 Yahoo Finance API
Yahoo Finance 胜在方便,适合快速验证想法。但要注意,它的期权数据有延迟,而且某些深度虚值合约可能缺失。
import yfinance as yf
import pandas as pd
# 获取某只股票的期权链
ticker = "SPY"
stock = yf.Ticker(ticker)
# 获取所有到期日
expirations = stock.options
print(f"到期日列表: {expirations[:5]}...")
# 获取最近到期日的期权链
expiry = expirations[0]
opt_chain = stock.option_chain(expiry)
# 看涨和看跌数据
calls = opt_chain.calls
puts = opt_chain.puts
print(calls[['strike', 'lastPrice', 'impliedVolatility', 'delta']].head())
我在项目中遇到过一个问题:Yahoo Finance 的隐含波动率有时候会突然跳变,尤其是临近到期的合约。后来我加了一道校验逻辑,才把这种异常过滤掉。
7.1.2 IBKR API(盈透证券)
如果你要做实盘,IBKR 是绕不开的选择。它的数据实时、准确,而且支持全球多个市场。不过,IBKR 的 API 学习曲线有点陡。
from ib_insync import *
# 连接到IBKR TWS或IB Gateway
ib = IB()
ib.connect('127.0.0.1', 7497, clientId=1)
# 创建合约对象
contract = Stock('SPY', 'SMART', 'USD')
# 获取期权链
chains = ib.reqSecDefOptParams(contract.symbol, '', contract.secType, contract.conId)
# 找到SPY的期权链参数
for chain in chains:
if chain.tradingClass == 'SPY':
expirations = chain.expirations
strikes = chain.strikes
break
print(f"到期日数量: {len(expirations)}")
print(f"行权价数量: {len(strikes)}")
说实话,IBKR 的 API 文档写得不够友好。我刚开始用的时候,光是连接就折腾了半天。后来发现是端口号没配对——TWS 用 7497,IB Gateway 用 4001。这种细节,文档里写得模棱两可。
7.2 数据清洗:剔除异常值、处理缺失值
拿到原始数据后,千万别直接扔进模型。期权数据里什么妖魔鬼怪都有。我曾经遇到过某只股票的看涨期权隐含波动率标成 999%,一看就是数据错误。
7.2.1 剔除异常值
常见的异常值包括:
- 隐含波动率异常:超过 500% 或小于 0.1% 的,基本是数据错误
- 买卖价差过大:价差超过中间价的 20%,说明流动性不足
- Delta 越界:Delta 应该在 0 到 1 之间(看涨)或 -1 到 0 之间(看跌)
- 行权价重复:同一到期日出现两个相同行权价,取成交量大的那个
def clean_option_data(df):
"""清洗期权链数据"""
# 剔除隐含波动率异常
df = df[(df['impliedVolatility'] > 0.01) & (df['impliedVolatility'] < 5.0)]
# 剔除Delta越界
df = df[(df['delta'] >= -1.0) & (df['delta'] <= 1.0)]
# 剔除价差过大的合约
df['spread_pct'] = (df['ask'] - df['bid']) / ((df['ask'] + df['bid']) / 2)
df = df[df['spread_pct'] < 0.2]
# 剔除成交量或持仓量为0的合约(流动性太差)
df = df[(df['volume'] > 0) | (df['openInterest'] > 0)]
return df
# 应用清洗
clean_calls = clean_option_data(calls)
clean_puts = clean_option_data(puts)
print(f"清洗前看涨合约数: {len(calls)}")
print(f"清洗后看涨合约数: {len(clean_calls)}")
你可能会问:为什么要把持仓量为0的也剔除?嗯,我吃过这个亏。有些合约虽然显示有报价,但根本没有成交量,你挂单进去根本成交不了。这种数据放进模型,只会污染你的曲面。
7.2.2 处理缺失值
期权数据缺失是常态。比如某个行权价没有报价,或者某个到期日的数据不全。处理方法取决于你的策略需求:
| 缺失情况 | 处理方法 | 适用场景 |
|---|---|---|
| 单个行权价缺失 | 线性插值 | 波动率曲面平滑 |
| 整个到期日缺失 | 时间序列插值或剔除 | 期限结构分析 |
| 深度虚值合约缺失 | 用模型外推(如SVI) | 尾部风险定价 |
| 临近到期合约缺失 | 直接剔除 | 避免到期效应干扰 |
# 线性插值示例:填充缺失的隐含波动率
from scipy.interpolate import interp1d
# 假设我们有某个到期日的行权价和IV数据
strikes = [100, 105, 110, 115, 120]
ivs = [0.25, 0.23, None, 0.21, 0.22] # 110行权价缺失
# 剔除缺失值进行插值
valid_strikes = [s for s, iv in zip(strikes, ivs) if iv is not None]
valid_ivs = [iv for iv in ivs if iv is not None]
f = interp1d(valid_strikes, valid_ivs, kind='linear', fill_value='extrapolate')
iv_110 = f(110)
print(f"插值得到的110行权价IV: {iv_110:.4f}")
7.3 构建标准化的Delta/期限网格
拿到清洗后的数据,下一步就是构建标准网格。为什么要标准化?因为不同股票的期权行权价和到期日都不一样,没法直接比较。标准化后,我们才能做跨品种的统计套利。
7.3.1 Delta网格
我个人习惯用 Delta 代替行权价作为横轴。Delta 的好处是它天然反映了期权的 moneyness,而且在不同股票之间具有可比性。
常用的 Delta 网格点:
- 看涨期权:0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90
- 看跌期权:-0.90, -0.80, -0.70, -0.60, -0.50, -0.40, -0.30, -0.20, -0.10
def build_delta_grid(df, target_deltas):
"""将期权数据映射到标准Delta网格"""
from scipy.interpolate import griddata
# 提取有效数据点
points = df[['delta', 'days_to_expiry']].values
values = df['impliedVolatility'].values
# 创建网格
grid_delta, grid_tenor = np.meshgrid(target_deltas, target_tenors)
# 插值
grid_iv = griddata(points, values, (grid_delta, grid_tenor), method='cubic')
return grid_delta, grid_tenor, grid_iv
# 定义标准Delta网格
target_deltas = [0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90]
target_tenors = [30, 60, 90, 180, 365] # 到期天数
grid_delta, grid_tenor, grid_iv = build_delta_grid(clean_calls, target_deltas)
7.3.2 期限网格
期限网格通常用到期天数或年化期限表示。我常用的期限网格点:
| 到期天数 | 年化期限 | 说明 |
|---|---|---|
| 7 | 0.019 | 周度合约 |
| 30 | 0.082 | 月度合约 |
| 60 | 0.164 | 双月合约 |
| 90 | 0.247 | 季度合约 |
| 180 | 0.493 | 半年合约 |
| 365 | 1.000 | 年度合约 |
你可能会问:为什么不用自然到期日?因为不同月份的天数不一样,用天数标准化后,不同合约之间才有可比性。说白了,30天就是30天,不管你是1月还是2月。
7.3.3 完整的标准化流程
把上面所有步骤串起来,就是一套完整的数据处理流水线:
def process_option_chain(ticker, target_deltas, target_tenors):
"""完整的期权链数据处理流程"""
# 1. 获取数据
stock = yf.Ticker(ticker)
expirations = stock.options[:5] # 取最近5个到期日
all_data = []
for expiry in expirations:
chain = stock.option_chain(expiry)
calls = chain.calls
# 2. 清洗
calls = clean_option_data(calls)
# 3. 计算到期天数
expiry_date = pd.to_datetime(expiry)
calls['days_to_expiry'] = (expiry_date - pd.Timestamp.now()).days
all_data.append(calls)
# 合并所有到期日数据
full_data = pd.concat(all_data)
# 4. 构建标准网格
grid_delta, grid_tenor, grid_iv = build_delta_grid(full_data, target_deltas, target_tenors)
return grid_delta, grid_tenor, grid_iv
# 执行
grid_delta, grid_tenor, grid_iv = process_option_chain('SPY', target_deltas, target_tenors)
print(f"网格形状: {grid_iv.shape}")
print(f"Delta范围: {grid_delta[0,0]:.2f} ~ {grid_delta[0,-1]:.2f}")
print(f"期限范围: {grid_tenor[0,0]:.0f} ~ {grid_tenor[-1,0]:.0f} 天")
7.4 本章知识体系
下面这张图总结了本章的核心逻辑,从数据源到标准化网格的完整流程:
说实话,数据获取和清洗这部分,看起来简单,但做起来最花时间。我刚开始做波动率曲面的时候,光清洗代码就迭代了十几个版本。每次发现新的异常模式,就加一条规则。到现在,我的清洗函数已经超过200行了。
但这一切都值得。干净的数据,是统计套利策略的基石。你想想看,如果输入的数据都是错的,再牛的模型也白搭。
- Yahoo Finance 适合研究,IBKR 适合实盘
- 数据清洗要剔除 IV 异常、价差过大、Delta 越界的合约
- 标准化网格用 Delta 代替行权价,用天数代替到期日
- 9×5 的网格密度是经过实践检验的合理选择
公众号:蓝海资料掘金营,微信deep3321