案例实战:构建A股50ETF期权隐含波动率曲面
好,咱们直接上手干活。这一章我打算带你把三个典型市场的波动率曲面都搭一遍——A股的50ETF、美股的SPY、还有商品期权。说白了,就是让你看看不同市场下,那套流程该怎么调。
我个人习惯是先从A股开始,毕竟数据好拿,规则也熟悉。你想想看,50ETF期权是国内流动性最好的品种之一,用它练手最合适。
数据准备:从Wind到本地DataFrame
嗯,这里要注意。50ETF期权的数据,我一般从Wind或者Tushare拉。核心字段就这几个:
- 合约代码、到期日、行权价
- 最新价、买卖价(用中间价更稳)
- 标的资产50ETF的实时价格
- 无风险利率(用SHIBOR 1M,别用国债收益率,那个期限对不上)
我曾经踩过一个坑:直接用收盘价算波动率,结果第二天开盘曲面就崩了。后来我改成用最后半小时的加权平均价,平滑很多。
import pandas as pd
import numpy as np
from scipy.optimize import brentq
# 假设df是清洗后的期权数据
def implied_volatility(price, S, K, T, r, option_type='call'):
"""用二分法反推隐含波动率"""
def bs_price(sigma):
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':
return S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
else:
return K*np.exp(-r*T)*norm.cdf(-d2) - S*norm.cdf(-d1)
try:
return brentq(lambda s: bs_price(s) - price, 0.001, 2.0)
except:
return np.nan
df['iv'] = df.apply(lambda row: implied_volatility(
row['mid_price'],
S=3.5, # 50ETF实时价格
K=row['strike'],
T=row['days_to_expiry']/365,
r=0.025
), axis=1)
曲面构建:插值与平滑
数据有了,接下来就是构建曲面。我一般用两步走:
- 按到期日分组,每个到期日单独做波动率微笑
- 在期限维度上插值,形成完整的曲面
这里有个坑:近月合约的波动率往往很陡,远月则平缓。直接做二维插值会出问题。我建议先对每个到期日做SVI拟合,再对SVI参数做期限插值。
核心逻辑:波动率曲面 = 期限结构 × 微笑形态。两者要分开处理,再合并。
from scipy.interpolate import griddata
# 构建网格:行权价比值(0.8~1.2)和剩余期限(0.1~1年)
strike_grid = np.linspace(0.8, 1.2, 50)
time_grid = np.linspace(0.1, 1.0, 20)
# 原始数据点
points = df[['moneyness', 'time_to_expiry']].values
values = df['iv'].values
# 插值
iv_surface = griddata(points, values,
(strike_grid[:, None], time_grid[None, :]),
method='cubic')
可视化:曲面长啥样
我习惯用3D曲面图来看。你注意观察:50ETF的曲面通常左边(虚值看跌)翘得比右边高,说明市场对下跌的恐惧更强烈。
小技巧:把曲面转成热力图,一眼就能看出哪个期限、哪个行权价的波动率异常。我每次做风控前必看这个图。
构建美股SPY期权隐含波动率曲面
美股市场跟A股最大的区别是什么?数据量大、流动性好、而且有完整的期限结构。SPY期权从周到期到两年期都有,曲面会更平滑。
我记得第一次做SPY曲面时,直接用A股那套代码跑,结果报错——因为SPY的期权数据里,行权价间距是1美元,而50ETF是0.05元。这个细节让我折腾了一下午。
数据源与预处理
美股数据我一般用Yahoo Finance或者IB的API。关键点:
- SPY的股息率要算进去,不然波动率会偏大
- 用期权链数据,别一个个合约拉,太慢
- 剔除交易量小于100手的合约,那些报价不靠谱
注意:美股期权有「前月」和「后月」之分,近月合约的到期日可能只有几天。我建议把剩余期限小于3天的合约去掉,不然曲面尾部会翘得离谱。
import yfinance as yf
# 获取SPY期权链
spy = yf.Ticker("SPY")
expirations = spy.options[:10] # 取最近10个到期日
all_data = []
for exp in expirations:
opt_chain = spy.option_chain(exp)
calls = opt_chain.calls
puts = opt_chain.puts
# 合并,取中间价
calls['type'] = 'call'
puts['type'] = 'put'
df_exp = pd.concat([calls, puts])
df_exp['expiration'] = pd.to_datetime(exp)
all_data.append(df_exp)
df_spy = pd.concat(all_data, ignore_index=True)
曲面构建:加入股息率校正
美股期权定价必须考虑股息。SPY的股息率大概在1.3%左右,忽略它会导致隐含波动率高估0.5~1个点。
我习惯用put-call parity来反推隐含股息率,这样更准。你想想看,如果call和put的隐含波动率不一致,那肯定是股息率设错了。
# 股息率校正:用平值call和put反推
atm_call = df_spy[(df_spy['type']=='call') & (df_spy['strike']==spy_price)]
atm_put = df_spy[(df_spy['type']=='put') & (df_spy['strike']==spy_price)]
# put-call parity: C - P = S*exp(-qT) - K*exp(-rT)
implied_q = -np.log((atm_call['lastPrice'] - atm_put['lastPrice'] +
spy_price*np.exp(-r*T)) / spy_price) / T
曲面特征:美股更「平」
SPY的波动率曲面跟50ETF有个明显区别:微笑更对称。左边和右边的高度差不多,不像A股那样左边明显高。这是因为美股市场机构投资者多,对冲需求更均衡。
我做过一个统计:SPY的波动率偏斜(skew)大概在-2到-3之间,而50ETF的skew经常到-5以下。说白了,A股市场对下跌的恐惧更强烈。
构建商品期权曲面
商品期权跟股票期权完全是两码事。我最早做铜期权曲面时,被它的期限结构搞懵了——近月波动率比远月还低,跟股票完全反着来。
为什么会这样?因为商品有存储成本、有季节性、有交割机制。这些因素都会影响波动率曲面。
商品期权的特殊处理
做商品期权曲面,你得额外处理几个东西:
- 期货价格:商品期权标的通常是期货,不是现货
- 便利收益:持有实物商品带来的收益,会影响定价
- 季节性:农产品有收割季,能源有取暖季
关键区别:商品期权的波动率曲面不是「微笑」,而是「偏斜+期限结构」的混合体。近月合约的波动率往往由供需决定,远月则由宏观因素决定。
案例:原油期权曲面
我拿原油期权举个例子。原油的波动率曲面有个特点:深度虚值看涨期权的波动率特别高,因为市场总是担心地缘政治导致油价暴涨。
# 原油期货数据示例
# 假设我们拿到了CL期货的期权链
oil_data = {
'strike': [65, 70, 75, 80, 85, 90],
'call_iv': [0.28, 0.25, 0.23, 0.24, 0.27, 0.32],
'put_iv': [0.35, 0.30, 0.26, 0.24, 0.23, 0.22],
'time_to_expiry': 0.25 # 3个月
}
# 注意看:call的IV在两端都高,但put的IV在低行权价更高
# 这就是典型的「波动率偏斜」+「尾部风险溢价」
曲面构建流程总结
不管哪个市场,流程其实就这几步:
- 数据清洗:去异常值、去流动性差的合约
- 计算隐含波动率:用BS模型反推,注意股息/便利收益
- 微笑拟合:每个到期日单独拟合SVI或多项式
- 期限插值:对SVI参数做期限维度的插值
- 曲面输出:生成网格数据,用于定价或风控
嗯,这里要注意。第三步和第四步的顺序不能乱。我见过有人直接对所有数据做二维插值,结果曲面出现负波动率——那肯定是方法错了。
避坑指南:我曾经在商品期权上直接用股票期权的SVI参数初值,结果拟合不收敛。后来我改成先用ATM波动率做初值,再迭代优化,问题就解决了。
好了,三个市场的曲面构建案例就讲到这里。你回去可以拿真实数据跑一遍,看看不同市场的曲面长什么样。有什么问题,咱们下节课再聊。