第十一章:数据获取与清洗:如何从交易所/数据商获取期权链数据,数据清洗与预处理

做Gamma交易,最怕什么?

不是模型算错了,而是你拿到的数据本身就是错的。

我见过太多交易员,花大把时间优化策略,结果回头一看——原始数据里藏着几十个异常点。嗯,那感觉就像你精心搭建的沙堡,地基却是烂的。

这一章,我们就来聊聊期权链数据的获取与清洗。说白了,就是怎么把交易所或数据商给的“毛坯房”数据,装修成能直接用的“精装房”。

11.1 数据源的选择:交易所 vs 数据商

我个人习惯把数据源分成两类:官方渠道和第三方渠道。

数据源类型 代表 优点 缺点
交易所直连 CBOE、上交所、大商所 数据最原始、无延迟、无二次加工 接口复杂、费用高、需要专线
专业数据商 Bloomberg、Reuters、Wind 接口友好、附带清洗、历史数据全 贵、有二次加工可能引入偏差
开源/免费源 Yahoo Finance、Quandl 免费、快速上手 数据质量参差不齐、延迟高
我的建议: 实盘交易用交易所直连或专业数据商。回测研究可以用免费源,但一定要做严格清洗。

你想想看,如果拿免费数据做回测,发现年化收益30%,兴冲冲上实盘——结果数据源一换,收益直接腰斩。这种坑,我踩过不止一次。

11.2 期权链数据的核心字段

不管从哪个源拿数据,期权链的核心字段都差不多。我列一个标准结构:

# 标准期权链数据字段
字段列表:
- trade_date: 交易日期
- symbol: 标的代码
- option_code: 期权合约代码
- strike: 行权价
- expiration: 到期日
- call_put: 看涨/看跌标识
- open: 开盘价
- high: 最高价
- low: 最低价
- close: 收盘价
- volume: 成交量
- open_interest: 持仓量
- implied_vol: 隐含波动率
- delta, gamma, theta, vega: 希腊字母
- underlying_price: 标的价格
- days_to_expiry: 剩余天数

重点注意: 隐含波动率这个字段,不同数据商的计算方法可能不同。有的用BS模型,有的用二叉树。我建议自己算一遍,或者至少确认数据商的计算方式。

11.3 数据获取实战:从交易所API拉数据

以国内中金所为例,我写一个简单的数据获取流程。注意,这只是演示逻辑,实际接口需要申请权限。

import requests
import pandas as pd
from datetime import datetime

def fetch_option_chain(symbol, date):
    """
    从交易所获取期权链数据
    实际接口地址和参数需要替换
    """
    url = f"https://api.exchange.com/option/chain"
    params = {
        "symbol": symbol,
        "date": date,
        "type": "all"
    }
    
    # 这里需要处理认证
    headers = {
        "Authorization": "Bearer YOUR_API_KEY"
    }
    
    response = requests.get(url, params=params, headers=headers)
    
    if response.status_code == 200:
        data = response.json()
        df = pd.DataFrame(data['data'])
        return df
    else:
        print(f"请求失败: {response.status_code}")
        return None

# 使用示例
df_raw = fetch_option_chain("IO", "2024-01-15")
print(f"获取到 {len(df_raw)} 条期权数据")
我曾经踩过的坑: 有一次我连续三天拉数据,发现某只期权的持仓量突然暴增10倍。我以为是市场异动,兴奋得不行。结果一查,是交易所数据接口升级,把持仓量的单位从“手”改成了“张”。嗯,从那以后,我每次拉数据都会先检查字段的单位和定义。

11.4 数据清洗:从脏数据到干净数据

数据清洗,说白了就是“去伪存真”。我总结了一套标准流程:

11.4.1 缺失值处理

期权数据里,缺失值很常见。比如某天某个深度虚值期权没有成交,价格就是空的。

# 缺失值处理策略
def clean_missing_values(df):
    # 1. 删除全部为空的列
    df = df.dropna(axis=1, how='all')
    
    # 2. 对于价格字段,用前向填充
    price_cols = ['open', 'high', 'low', 'close']
    df[price_cols] = df[price_cols].fillna(method='ffill')
    
    # 3. 对于成交量,空值填0
    df['volume'] = df['volume'].fillna(0)
    
    # 4. 对于隐含波动率,用同一行权价附近的均值填充
    df['implied_vol'] = df.groupby('strike')['implied_vol'].transform(
        lambda x: x.fillna(x.mean())
    )
    
    return df

11.4.2 异常值检测

我个人习惯用“3-sigma”法则加“业务逻辑”双重过滤。

def detect_outliers(df):
    # 业务逻辑过滤
    # 1. 价格不能为负
    df = df[df['close'] >= 0]
    
    # 2. 隐含波动率不能为负,且不能超过500%
    df = df[(df['implied_vol'] >= 0) & (df['implied_vol'] <= 5.0)]
    
    # 3. 成交量不能为负
    df = df[df['volume'] >= 0]
    
    # 统计方法过滤
    # 对隐含波动率做3-sigma过滤
    mean_vol = df['implied_vol'].mean()
    std_vol = df['implied_vol'].std()
    df = df[(df['implied_vol'] >= mean_vol - 3*std_vol) & 
            (df['implied_vol'] <= mean_vol + 3*std_vol)]
    
    return df
一个小技巧: 对于期权数据,我建议先做业务逻辑过滤,再做统计过滤。因为有些异常值在统计上正常,但业务上明显不合理。比如某只深度实值期权的隐含波动率是0.01%,统计上可能没超3-sigma,但业务上这明显是数据错误。

11.4.3 数据对齐与插值

做Gamma交易,我们需要完整的波动率曲面。但实际数据往往有缺失,比如某些行权价没有报价。

def interpolate_vol_surface(df):
    """
    对波动率曲面进行插值
    使用三次样条插值
    """
    from scipy.interpolate import griddata
    
    # 提取已知点
    points = df[['strike', 'days_to_expiry']].values
    values = df['implied_vol'].values
    
    # 生成完整的网格
    strikes = np.linspace(df['strike'].min(), df['strike'].max(), 50)
    expiries = np.linspace(df['days_to_expiry'].min(), df['days_to_expiry'].max(), 20)
    grid_x, grid_y = np.meshgrid(strikes, expiries)
    
    # 插值
    grid_z = griddata(points, values, (grid_x, grid_y), method='cubic')
    
    return grid_x, grid_y, grid_z

11.5 数据预处理:为Gamma交易做准备

数据清洗干净后,还需要做预处理,才能用于Gamma交易策略。

11.5.1 计算希腊字母

如果数据商没有提供希腊字母,或者你想用自己的模型算,可以这样:

from scipy.stats import norm
import numpy as np

def calculate_greeks(S, K, T, r, sigma, option_type):
    """
    计算期权希腊字母
    S: 标的价格
    K: 行权价
    T: 剩余时间(年)
    r: 无风险利率
    sigma: 隐含波动率
    option_type: 'call' 或 'put'
    """
    d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)
    
    if option_type == 'call':
        delta = norm.cdf(d1)
        gamma = norm.pdf(d1) / (S * sigma * np.sqrt(T))
    else:
        delta = -norm.cdf(-d1)
        gamma = norm.pdf(d1) / (S * sigma * np.sqrt(T))
    
    return delta, gamma

11.5.2 构建波动率曲面

这是Gamma交易的核心输入。我习惯把数据整理成矩阵形式:

def build_vol_surface(df):
    """
    构建波动率曲面矩阵
    行:行权价
    列:到期时间
    值:隐含波动率
    """
    # 透视表
    vol_surface = df.pivot_table(
        values='implied_vol',
        index='strike',
        columns='days_to_expiry',
        aggfunc='mean'
    )
    
    # 排序
    vol_surface = vol_surface.sort_index()
    vol_surface = vol_surface[sorted(vol_surface.columns)]
    
    return vol_surface

核心要点: 波动率曲面的质量,直接决定了Gamma交易的效果。我建议每次构建完曲面后,都做一次可视化检查。如果曲面出现不合理的“尖刺”或“凹陷”,说明数据清洗还有问题。

11.6 数据质量检查清单

最后,我分享一个自己用了多年的检查清单:

  1. 完整性检查: 每个交易日的数据是否完整?有没有跳空?
  2. 一致性检查: 同一标的的看涨和看跌期权,平价附近的隐含波动率是否接近?
  3. 合理性检查: 深度实值期权的隐含波动率是否异常低?深度虚值的是否异常高?
  4. 时间序列检查: 同一合约的隐含波动率是否出现不合理的跳跃?
  5. 套利检查: 是否存在明显的套利机会?如果有,可能是数据错误。
我曾经犯过的错: 有一次我忽略了“除权除息”对期权数据的影响。某只股票分红后,所有期权的行权价和合约规模都调整了,但我没更新数据。结果波动率曲面出现了一个巨大的“坑”,差点导致策略误判。从那以后,我每次遇到分红、拆股等事件,都会重新拉一遍数据。

数据获取与清洗,听起来很枯燥,但它是所有量化交易的基石。你想想看,如果连数据都是错的,再牛的模型又有什么用?

我个人习惯把数据清洗的代码封装成一个独立的模块,每次跑策略前先跑一遍清洗流程。虽然麻烦,但能省掉后面90%的debug时间。

期权链数据获取与清洗流程 数据获取 交易所直连 数据商API 开源数据源 数据清洗 缺失值处理 异常值检测 数据对齐插值 数据预处理 计算希腊字母 构建波动率曲面 数据质量检查 干净可用的期权链数据 数据质量 = 策略生命线 清洗不彻底,模型再强也是空中楼阁

数据清洗这件事,做一次不难,难的是每次都做、每次都做对。我建议你把清洗流程写成自动化脚本,每天开盘前自动跑一遍。这样,你就能把精力放在真正重要的事情上——交易决策。

记住一句话:垃圾进,垃圾出。数据质量,就是你的策略生命线。

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