4. 波动率曲面数据获取:从交易所/数据商获取期权链数据(Python示例)
做波动率曲面,第一步不是建模,而是拿数据。
我见过太多人一上来就搞什么SVI、SSVI拟合,结果数据源都没搞清楚。数据不对,后面全是白费功夫。说白了,期权链数据是波动率曲面的「原材料」,原材料有问题,再好的厨子也做不出好菜。
4.1 数据源的选择:交易所 vs 数据商
先聊聊数据从哪来。我个人习惯把数据源分成两类:
- 交易所直连:比如上交所、深交所、中金所。数据最原始,延迟最低,但接入成本高,适合机构。
- 数据商接口:比如万得、聚宽、Tushare、AKShare。封装好了,拿来就能用,适合个人和中小团队。
你想想看,如果你只是做研究,没必要去搞交易所直连。数据商接口足够用了。我在项目中遇到过用AKShare抓取50ETF期权数据,速度还行,就是偶尔会断流,需要加个重试机制。
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
...
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 本章知识体系
下面这张图,帮你理清本章的核心逻辑:
这张图展示了从数据源到最终曲面原始数据的完整流程。每一步都有坑,每一步都需要细心处理。数据质量决定了曲面的可信度,别在这个环节偷懒。