ID: 406010
1. 策略思路
1.1. 理论基础
隐含波动率可以理解为市场对标的资产未来波动率的预期
相同到期时间和行权价格的认沽和认购期权的隐含波动率应该相等(对相同标的资产的预期相同)
当认沽和认购期权之间的隐含波动率不同时,就存在套利空间
隐含波动率存在套利空间时,可以通过做多隐含波动率低的期权,做空隐含波动率高的期权实现套利
反向操作认沽和认购期权得到的组合,其收益等同于一个forward,可以通过基础资产进行对冲,减少基础资产价格波动带来的风险
1.2. 实现思路
1. 筛选出所有剩余有效期为30天的期权(持有时间30天)
计算期权剩余有效期
根据剩余有效期对期权数据进行筛选
2. 计算合约的隐含波动率
定义函数计算欧式期权的价值
定义函数计算期权的Vega值
使用数值方法计算的隐含波动率
3. 根据隐含波动率筛选可以交易的期权对(隐含波动率之差大的期权对)
将相同到期日的认沽和认购期权配对
计算期权对的隐含波动率之差
4. 计算筛选结果的盈亏情况
计算期权和标的资产(50ETF)的期初和期末价格
计算期权和标的资产组合的收益率
#!/usr/bin/env python
# coding: utf-8
# # 隐含波动率套利策略
from math import log, e, sqrt, pi
from scipy import stats
import tushare as ts
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
sns.set_style('darkgrid')
mpl.rcParams['figure.figsize'] = (12,6) # 设置matplotlie全局参数
# In[2]:
pip install tushare
# In[4]:
import tushare as ts
pro = ts.pro_api('#settoken')
# In[5]:
df = pro.opt_basic(exchange='DCE', fields='ts_code,name,exercise_type,list_date,delist_date')
df.tail()
# In[19]:
# 读入期权交易数据
filepath=r"C:\Users\pc\Documents\WeChat Files\wxid_ehvkz0rvd75q12\FileStorage\File\2020-11\option1.csv"
data = pd.read_csv(filepath)
data
# ### 2.1. 根据期权调整标记筛选数据
# In[20]:
def get_modify_mark(ticker):
""" 定义函数,从ticker字符串中获取期权合约调整标记 """
return ticker[-6]
ticker = '510050C1503M02200'
get_modify_mark(ticker)
# In[6]:
# # 将get_modify_mark()函数映射到ticker列上,获取所有数据的合约调整标记
modifiy_mark = data.ticker.map(get_modify_mark)
modifiy_mark.head()
# In[7]:
# 筛选并保留原始期权数据中调整标记为M的期权数据,即不考虑基础资产分红等情况
data = data[modifiy_mark == 'M'].copy()
# ### 2.2. 获取期权类型和行权价
# In[8]:
def get_opt_type(ticker: str):
""" 定义函数,从ticker字符串中获取期权类型 """
return ticker[6].lower()
ticker = '510050C1503M02200'
get_opt_type(ticker)
# In[9]:
def get_strick(ticker: str):
""" 定义函数,从ticker字符串中提取出期权的行权价格 """
return int(ticker[-5:]) / 1000
ticker = '510050C1503M02200'
get_strick(ticker)
# In[10]:
# 从合约代码中获取合约行权价格
data['strikePrice'] = data.ticker.map(get_strick)
# 从合约代码中获取合约类型
data['optType'] = data.ticker.map(get_opt_type)
data
# ### 2.3. 计算期权剩余有效期
# In[11]:
# 将字符串日期转换为日期时间类型
data['tradeDate'] = pd.to_datetime(data.tradeDate)
# In[12]:
# 根据交易日期计算合约到期日期(仅对已经到期的合约有效,未到期合约只能获得数据中最后一个交易日的日期
last_trade_date = data.groupby('ticker').tradeDate.last()
last_trade_date
# In[13]:
# 将合约到期日期填入 data 数据表中
data['lastTradeDate'] = data.ticker.map(last_trade_date)
data
# In[14]:
# 计算合约剩余时间
data['daysLeft'] = (data.lastTradeDate - data.tradeDate).map(lambda x: x.days)
data
# ### 2.4. 计算期权匹配字符串
# In[56]:
def get_match_mark(ticker: str):
return ticker[-10:]
# In[57]:
# 根据合约代码计算match_mark,match_mark的作用是筛选出相同到期时间的认沽和认购期权
data['match_mark'] = data.ticker.map(get_match_mark)
data.head()
# In[58]:
etf =ts.pro_bar('510050', start_date='20140101')
etf
# In[59]:
pro.opt_daily('510050C1503M02200',trade_date='2018-01-01')
# In[60]:
df = pro.opt_daily(ts_code= '10002495.SH')
df
# In[61]:
ts.pro_bar(ts_code='000001.SZ')
# ### 2.5. 获取50ETF行情数据
# In[62]:
# 获取50etf价格数据
etf =ts.pro_bar('510050', '2014-01-01')
# 将日期列转换为日期时间格式,并设为索引,为数据拼接做准备
etf['date'] = pd.to_datetime(etf.date)
etf.set_index('date', inplace=True)
# 计算50etf日收益率
etf['d_return'] = etf.close.pct_change()
# 计算50etf的历史波动率
etf['volatility'] = etf.d_return.rolling(120).std() * sqrt(252)
etf
# ## 3. 策略实现
# ### 3.1. 计算隐含波动率
# In[63]:
# 筛选特定到期时间的合约
days_left_selected = data[data.daysLeft == 30].copy()
days_left_selected
# In[64]:
# 将50etf数据填入到筛选后的数据表
days_left_selected['etfClose'] = days_left_selected.tradeDate.map(etf.close)
days_left_selected['volatility'] = days_left_selected.tradeDate.map(etf.volatility)
days_left_selected
# In[65]:
def get_option_price(S, X, r, q, sigma, T, opt_type='call'):
"""
根据BSM模型计算期权价格
param S: 标的资产当前价格
param X: 期权行权价格
param r: 无风险收益率
param q: 基础资产分红收益率
param sigma:标的资产年化波动率
param T: 以年计算的期权到期时间
param opt_type: 'call'或'put'
return 返回期权价格
"""
d1 = (log(S/X) + (r + sigma**2/2) * T) / (sigma * sqrt(T))
d2 = d1 - sigma * sqrt(T)
N1, N2 = stats.norm.cdf([d1, d2])
call = S * N1 - X * e ** (-r * T) * N2 # 计算认购期权价格
if opt_type.lower()[0] == 'c':
return call
else:
put = call + X * e ** (-r * T) - S # 利用期权平价模型计算put价格
return put
# In[66]:
def vega(S, X, r, q, sigma, T):
"""
定义函数计算期权的Vega
param S: 标的资产当前价格
param X: 期权行权价格
param r: 复合无风险收益率
param q: 基础资产分红收益率
param sigma:标的资产年化波动率
param T: 以年计算的期权到期时间
return:返回期权的 Vega 值
"""
d1 = (log(e**(-q*T)*S/X) + (r + sigma**2/2) * T) / (sigma * sqrt(T))
# N_dash = e ** (-d1 ** 2 / 2)/sqrt(2 * pi)
N_dash = stats.norm.pdf(d1)
return S * N_dash * sqrt(T)
# In[67]:
S = 2.676
X = 2.2
r = 0.0246
q = 0
sigma = 0.4
# T = 30/365
T = 0.08219
vega(S, X, r, q, sigma, T)
# In[68]:
# 牛顿法计算隐含波动率
def get_implied_vol(S, X, r, q, T, V_mkt, opt_type='call'):
"""
根据期权数据计算隐含波动率
param S: 标的资产当前价格
param X: 期权行权价格
param r: 复合无风险收益率
param q: 基础资产分红收益率
param T: 以年计算的期权到期时间
param V_mkt: 期权的市场价格
param opt_type:期权类型,put或call
return:返回隐含波动率
"""
sigma = 0.5
error = 10**(-6)
for i in range(100):
V = get_option_price(S, X, r, q, sigma, T, opt_type) # BSM计算的期权价格
if abs(V - V_mkt) < error: # 精度达到要求
return sigma
Vega = vega(S, X, r, q, sigma, T)
if Vega < 0.0001: # Vega太小的情况下会导致计算不收敛,因此在Vega过小时直接返回一个很小的隐含波动率
return np.nan
sigma = sigma - (V - V_mkt)/ Vega # 迭代计算sigma,g(x)=(V-V_mkt), g'(x)=vega
return sigma
# In[77]:
get_implied_vol(S, X, r, q, T, 1, opt_type='call')
# In[117]:
def get_iv_wrapper(option_record):
""" 定义函数,根据期权数据信息计算对应的隐含波动率 """
S = option_record.etfClose
X = option_record.strikePrice
r = 0.0246
q = 0
T = option_record.daysLeft / 365
V_mkt = option_record.closePrice
opt_type = option_record.optType
return get_implied_vol(S, X, r, q, T, V_mkt, opt_type)
# In[118]:
record = days_left_selected.iloc[6]
record
# In[119]:
# 测试get_iv_wrapper()函数
get_iv_wrapper(record)
# In[120]:
# 计算合约对应的隐含波动率
days_left_selected['impliedVol'] = days_left_selected.apply(get_iv_wrapper, axis=1)
days_left_selected
# ### 3.2. 根据隐含波动率筛选可以交易的期权对
# In[122]:
# 仅保留隐含波动率不为空值的期权合约做下一步处理
iv_selcted = days_left_selected[days_left_selected.impliedVol.notna()].copy()
iv_selcted
# In[146]:
# 将认沽和认购合约分开,为计算差值做准备
call = iv_selcted[iv_selcted.optType == 'c'].set_index(['match_mark', 'tradeDate'])
put = iv_selcted[iv_selcted.optType == 'p'].set_index(['match_mark', 'tradeDate'])
call
# In[148]:
len(put)
# In[124]:
# 计算认沽和认购合约的隐含波动率差值
iv_diff = call.impliedVol - put.impliedVol
iv_diff
# In[125]:
iv_diff.name = 'iv_diff'
# In[126]:
# 观察认沽和认购期权隐含波动率差值的分布情况
iv_diff.hist(bins=50, alpha=0.5)
plt.show()
# In[127]:
# 筛选出隐含波动率只差大于0.1的期权对
selected = pd.DataFrame(iv_diff[iv_diff < -0.2])
selected
# ### 3.3. 根据筛选结果计算每次交易的盈亏
# #### 3.3.1. 处理单一期权组合
# In[128]:
# 获取期权对代码和起始交易时间
match_mark_sample = selected.index[1][0]
start_date_sample = selected.index[1][1]
start_date_sample
# In[149]:
match_mark_sample
# In[129]:
# 获取期权到期时间
data_selected = data[(data.match_mark == match_mark_sample) & (data.tradeDate >= start_date_sample)]
end_date_sample = data_selected.tradeDate.sort_values().iloc[-1]
end_date_sample
# In[130]:
columns = ['start_date', 'end_date']
# In[131]:
# 获得起始和终止时间的认购期权的收盘价
call = data_selected[(data_selected.optType == 'c')]
call_close = call.closePrice.iloc[[0, -1]]
call_close
# In[132]:
# 获得起始和终止时间的认沽期权的收盘价
put = data_selected[(data_selected.optType == 'p')]
put_close = put.closePrice.iloc[[0, -1]]
put_close
# In[133]:
# 获取起始和终止时间50etf的收盘价
etf_close = etf.close.loc[[start_date_sample, end_date_sample]]
etf_close
# In[134]:
# 单笔交易的价格变动和盈亏情况
portfolio = pd.DataFrame([call_close.values, put_close.values, etf_close.values], index=['call', 'put', 'etf'], columns=['start', 'end'])
portfolio['factor'] = [1, -1, -1]
portfolio['profit'] = (portfolio.end - portfolio.start) * portfolio.factor
portfolio
# In[135]:
# 计算交易盈亏
profit = portfolio.profit.sum()
profit
# In[150]:
# 计算初始资金
initial_cost = portfolio.at['call', 'start'] + portfolio.at['etf', 'start'] * 0.5 - portfolio.at['put', 'start'] # 按照50%的做空保证金计算初始占用资金的情况
initial_cost
# In[151]:
# 计算收益率
rate_of_return = profit / initial_cost
rate_of_return
# #### 3.3.2. 计算所有筛选出的期权对的交易结果
# In[138]:
# 定义函数,获得一个期权组合对应的起止价格和50_etf的价格
def get_close_data(iv_diff_series, data_df, etf_df):
match_mark = iv_diff_series.name[0]
start_date = iv_diff_series.name[1]
data_selected = data_df[(data_df.match_mark == match_mark) & (data_df.tradeDate >= start_date)]
end_date = data_selected.tradeDate.sort_values().iloc[-1]
call = data_selected[(data_selected.optType == 'c')]
call_close = call.closePrice.iloc[[0, -1]]
put = data_selected[(data_selected.optType == 'p')]
put_close = put.closePrice.iloc[[0, -1]]
etf_close = etf.close.loc[[start_date, end_date]]
result = pd.concat([call_close, put_close, etf_close], ignore_index=True)
result.name =iv_diff_series.name
return result
# In[139]:
# 对所有期权组合应用get_close_data()函数,获得相关的价格数据
# selected = pd.DataFrame(selected)
cost_profit = selected.apply(get_close_data, args=[data, etf], axis=1)
cost_profit.columns = ['call_start', 'call_end', 'put_start', 'put_end', 'etf_start', 'etf_end']
data_concat = pd.concat([selected, cost_profit], axis=1)
data_concat
# In[140]:
# 计算收益
data_concat['call_pl'] = data_concat.call_end - data_concat.call_start
data_concat['put_pl'] = data_concat.put_start - data_concat.put_end
data_concat['etf_pl'] = data_concat.etf_start - data_concat.etf_end
data_concat['total_pl'] = data_concat.call_pl + data_concat.put_pl + data_concat.etf_pl
data_concat['initial_cost'] = data_concat.call_start - data_concat.put_start + data_concat.etf_start * 0.5 # 按照50%融券保证金比例计算
data_concat['rate_of_return'] = data_concat.total_pl / data_concat.initial_cost
data_concat
# In[144]:
data_concat.rate_of_return.hist(bins=100, alpha=0.5)
plt.show()
# In[145]:
data_concat[data_concat.rate_of_return == data_concat.rate_of_return.min()]
# ### 3.4. 策略细节说明:
# * 选择剩余到期时间为一个月的合约主要考虑到期权合约存在时间价值,剩余时间较长的合约,会存在比较大的时间价值的减值
# * 只选择认购期权波动率低而认沽期权波动率高的机会进行操作,因为这种情况下的对冲操作是做空基础资产,占用资金相对较少,可以实现更高的资金利用率
#
# ## 4. 扩展方向:
# * 不同到期时间对套利结果的影响
# * 不同行权价格对套利结果的影响(平价 vs. 深度价内或价外)
# * 不进行delta对冲的情况下组合整体的收益情况
# In[6]:
from IPython.core.interactiveshell import InteractiveShell
# In[7]:
InteractiveShell.ast_node_interactivity = "all"
# In[ ]: