4. 波动率曲面数据获取:从交易所/数据商获取期权链数据(Python示例)

做波动率曲面,第一步不是建模,而是拿数据。

我见过太多人一上来就搞什么SVI、SSVI拟合,结果数据源都没搞清楚。数据不对,后面全是白费功夫。说白了,期权链数据是波动率曲面的「原材料」,原材料有问题,再好的厨子也做不出好菜。

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

先聊聊数据从哪来。我个人习惯把数据源分成两类:

  • 交易所直连:比如上交所、深交所、中金所。数据最原始,延迟最低,但接入成本高,适合机构。
  • 数据商接口:比如万得、聚宽、Tushare、AKShare。封装好了,拿来就能用,适合个人和中小团队。

你想想看,如果你只是做研究,没必要去搞交易所直连。数据商接口足够用了。我在项目中遇到过用AKShare抓取50ETF期权数据,速度还行,就是偶尔会断流,需要加个重试机制。

小提示: 国内期权数据,50ETF和300ETF期权流动性最好。商品期权像豆粕、白糖,数据量小,但做波动率曲面也够用。

4.2 期权链数据结构长什么样?

拿到数据后,先别急着算。看一眼数据结构,心里有个底。

一个标准的期权链数据,通常包含这些字段:

字段名 说明 示例
code 期权合约代码 10003714.SH
trade_date 交易日期 2024-01-15
expire_date 到期日 2024-03-27
strike 行权价 2.800
call_put 看涨/看跌 C/P
close 收盘价 0.0456
implied_vol 隐含波动率 0.1823
volume 成交量 12500
open_interest 持仓量 38000

嗯,这里要注意:隐含波动率字段,有些数据商不直接提供。如果拿不到,你就得自己用BS公式反推。我后面会讲怎么算。

4.3 用Python抓取期权链数据

直接上代码。我用的是AKShare,免费、开源,国内数据覆盖得不错。

先装库:

pip install akshare pandas numpy

然后写个函数,抓取50ETF期权链:

import akshare as ak
import pandas as pd
from datetime import datetime, timedelta

def fetch_option_chain(symbol="50ETF", date="20240115"):
    """
    获取期权链数据
    symbol: 标的代码,如50ETF、300ETF
    date: 交易日期,格式YYYYMMDD
    """
    # 获取所有期权合约
    option_df = ak.option_50etf_hist(symbol=symbol, trade_date=date)
    
    if option_df.empty:
        print(f"警告:{date} 无数据,可能非交易日")
        return None
    
    # 只保留需要的字段
    cols = ['合约代码', '交易日期', '到期日', '行权价', 
            '看涨看跌', '收盘价', '隐含波动率', '成交量', '持仓量']
    
    # 重命名列,方便后续处理
    option_df = option_df[cols]
    option_df.columns = ['code', 'trade_date', 'expire_date', 'strike',
                         'call_put', 'close', 'implied_vol', 'volume', 'open_interest']
    
    # 转换日期格式
    option_df['trade_date'] = pd.to_datetime(option_df['trade_date'])
    option_df['expire_date'] = pd.to_datetime(option_df['expire_date'])
    
    # 计算剩余期限(年)
    option_df['ttm'] = (option_df['expire_date'] - option_df['trade_date']).dt.days / 365.0
    
    return option_df

# 使用示例
df = fetch_option_chain("50ETF", "20240115")
print(df.head())
print(f"共获取 {len(df)} 条期权合约数据")

跑完之后,你会看到类似这样的输出:

       code trade_date expire_date  strike call_put   close  implied_vol  volume  open_interest       ttm
0  10003714  2024-01-15  2024-03-27   2.800        C  0.0456       0.1823   12500         38000  0.197260
1  10003715  2024-01-15  2024-03-27   2.850        C  0.0321       0.1756    8900         25000  0.197260
2  10003716  2024-01-15  2024-03-27   2.900        C  0.0210       0.1689    5600         18000  0.197260
...
注意: 我曾经遇到过一个问题——AKShare的接口偶尔会返回空数据,尤其是节假日前后。建议加个重试机制,比如最多重试3次,每次间隔2秒。

4.4 数据清洗:别让脏数据毁了你的曲面

数据拿到手,别急着用。先洗一洗。

我一般做这几步:

  • 去空值:隐含波动率或收盘价为空的,直接删掉。
  • 去异常值:隐含波动率超过0.8或者小于0.05的,基本是数据错误。
  • 去深度虚值:行权价离现价太远的合约,流动性差,隐含波动率失真。
  • 去零成交量:没有成交的合约,价格没有参考意义。

代码实现:

def clean_option_data(df, spot_price=None):
    """
    清洗期权链数据
    spot_price: 标的资产现价,用于过滤深度虚值
    """
    # 去空值
    df = df.dropna(subset=['implied_vol', 'close'])
    
    # 去异常隐含波动率
    df = df[(df['implied_vol'] > 0.05) & (df['implied_vol'] < 0.8)]
    
    # 去零成交量
    df = df[df['volume'] > 0]
    
    # 如果提供了标的价格,过滤深度虚值(行权价偏离超过30%)
    if spot_price is not None:
        df = df[(df['strike'] > spot_price * 0.7) & 
                (df['strike'] < spot_price * 1.3)]
    
    # 按到期日和行权价排序
    df = df.sort_values(['expire_date', 'strike'])
    
    return df.reset_index(drop=True)

# 假设50ETF现价为2.750
spot = 2.750
df_clean = clean_option_data(df, spot_price=spot)
print(f"清洗后剩余 {len(df_clean)} 条数据")

清洗完之后,数据量通常会减少20%-30%。别心疼,留下来的才是能用的。

4.5 多期限数据合并:构建曲面基础

波动率曲面需要多个到期日的数据。单看一个到期日,那叫波动率微笑,不是曲面。

我习惯的做法是:循环抓取多个交易日的数据,然后按到期日分组。

def build_surface_data(symbol="50ETF", start_date="20240101", end_date="20240131"):
    """
    构建多期限波动率曲面数据
    """
    date_list = pd.date_range(start=start_date, end=end_date, freq='B')  # 仅交易日
    all_data = []
    
    for date in date_list:
        date_str = date.strftime("%Y%m%d")
        df = fetch_option_chain(symbol, date_str)
        if df is not None and not df.empty:
            df_clean = clean_option_data(df)
            if not df_clean.empty:
                all_data.append(df_clean)
    
    if not all_data:
        print("未获取到任何有效数据")
        return None
    
    result = pd.concat(all_data, ignore_index=True)
    print(f"共获取 {len(result)} 条数据,覆盖 {len(result['expire_date'].unique())} 个到期日")
    
    return result

# 构建曲面数据
surface_data = build_surface_data("50ETF", "20240101", "20240131")

输出示例:

共获取 12500 条数据,覆盖 4 个到期日

4个到期日,每个到期日有几十个行权价,这就是曲面的原始素材了。

4.6 数据可视化:看一眼数据长什么样

数据拿到手,画个图看看分布。我习惯用散点图展示隐含波动率在行权价和期限上的分布。

import matplotlib.pyplot as plt

def plot_vol_surface_scatter(df):
    """
    绘制隐含波动率散点图
    """
    fig = plt.figure(figsize=(12, 8))
    ax = fig.add_subplot(111, projection='3d')
    
    # 按到期日分组,不同颜色
    for i, (expire, group) in enumerate(df.groupby('expire_date')):
        xs = group['strike'].values
        ys = group['ttm'].values
        zs = group['implied_vol'].values
        ax.scatter(xs, ys, zs, label=str(expire.date()), alpha=0.7)
    
    ax.set_xlabel('行权价')
    ax.set_ylabel('剩余期限(年)')
    ax.set_zlabel('隐含波动率')
    ax.set_title('50ETF期权隐含波动率分布')
    ax.legend()
    
    plt.show()

# 调用
plot_vol_surface_scatter(df_clean)

看到图之后,你就能直观感受到:波动率曲面不是平的,它有自己的形状。有的地方高,有的地方低,这就是我们后面要建模的原因。

4.7 避坑指南:我踩过的几个坑

做数据获取这么多年,有些坑我替你们踩过了:

  • 坑一:日期格式。交易所的日期格式五花八门,有的用YYYYMMDD,有的用YYYY-MM-DD。统一用datetime处理,别手写字符串解析。
  • 坑二:合约代码规则。不同交易所的合约代码规则不一样。上交所是数字代码,中金所是字母+数字。别搞混了。
  • 坑三:除权除息。标的分红或送股时,期权合约会调整。数据商一般会处理,但如果你自己算隐含波动率,记得用调整后的行权价。
  • 坑四:数据延迟。免费接口的数据通常有1-2天的延迟。做回测没问题,做实盘交易不行。
核心要点: 数据获取是波动率曲面构建的基石。选对数据源、做好清洗、理解数据结构,后面建模才能事半功倍。别图快,数据质量比数量重要得多。

4.8 本章知识体系

下面这张图,帮你理清本章的核心逻辑:

波动率曲面数据获取流程 数据源选择 交易所直连(机构) 数据商接口(个人/团队) 数据获取 Python接口调用 多期限数据合并 数据清洗 去空值 去异常值 去深度虚值 输出:清洗后的期权链数据 数据可视化 波动率曲面原始数据 数据获取 → 清洗 → 可视化 → 曲面构建

这张图展示了从数据源到最终曲面原始数据的完整流程。每一步都有坑,每一步都需要细心处理。数据质量决定了曲面的可信度,别在这个环节偷懒。

专注资料整理