第七章:数据获取与清洗——期权链数据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 的隐含波动率有时候会突然跳变,尤其是临近到期的合约。后来我加了一道校验逻辑,才把这种异常过滤掉。

注意: Yahoo Finance 的期权数据是延迟15分钟的,不能用于实盘交易。只适合做历史回测和策略研究。

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。这种细节,文档里写得模棱两可。

我的建议: 先用 Yahoo Finance 做策略原型,等逻辑跑通了,再切换到 IBKR 做实盘。这样效率最高。

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} 天")
经验之谈: 标准化网格的密度要适中。太密了,插值误差大;太疏了,丢失细节。我一般用 9×5 的网格(9个Delta点 × 5个期限点),效果不错。

7.4 本章知识体系

下面这张图总结了本章的核心逻辑,从数据源到标准化网格的完整流程:

期权链数据处理流程 数据源 Yahoo Finance / IBKR 数据清洗 剔除异常值 · 处理缺失值 标准化网格 Delta × 期限 IV异常 剔除 价差过大 剔除 Delta越界 剔除 Delta插值 9个标准点 期限插值 5个标准点 标准化波动率曲面 9×5 网格 · 可跨品种比较 核心目标:将非标准化的期权链数据,转化为统一的 Delta-期限网格 为后续的统计套利模型提供标准化的输入数据 ⚠ 注意:不同数据源的清洗规则略有差异,IBKR数据质量更高,但清洗逻辑更复杂

说实话,数据获取和清洗这部分,看起来简单,但做起来最花时间。我刚开始做波动率曲面的时候,光清洗代码就迭代了十几个版本。每次发现新的异常模式,就加一条规则。到现在,我的清洗函数已经超过200行了。

但这一切都值得。干净的数据,是统计套利策略的基石。你想想看,如果输入的数据都是错的,再牛的模型也白搭。

本章要点:
  • Yahoo Finance 适合研究,IBKR 适合实盘
  • 数据清洗要剔除 IV 异常、价差过大、Delta 越界的合约
  • 标准化网格用 Delta 代替行权价,用天数代替到期日
  • 9×5 的网格密度是经过实践检验的合理选择

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