问题描述
解决方案
这是一个关于利用期权波动率偏斜(Volatility Skew)构建套利策略的详细指南。
策略概述:波动率偏斜套利
波动率偏斜是指同一标的资产、同一到期日但行权价不同的期权,其隐含波动率(IV)不一致的现象。在股票和指数期权市场中,通常表现为“偏斜”(Skew)或“微笑”(Smirk)形态,即虚值认沽期权(OTM Put)的隐含波动率往往高于平值(ATM)或虚值认购期权(OTM Call)。
策略逻辑(垂直价差策略):
本策略旨在利用过度陡峭的波动率偏斜获利。
- 观察: 监控虚值认沽期权(OTM Put)与平值认沽期权(ATM Put)之间的隐含波动率差值(IV Spread)。
- 信号: 当 IV Spread 超过设定阈值(例如 5%),认为市场对下行风险过度定价(OTM Put 太贵)。
- 执行:
- 卖出 高 IV 的虚值认沽期权(Short OTM Put)。
- 买入 较低 IV 的平值认沽期权(Long ATM Put)作为保护。
- 这构成了一个牛市看跌价差(Bull Put Spread),即贷方价差(Credit Spread)。
- 获利: 随着时间流逝或波动率偏斜回归正常,价差缩小或期权归零,策略获利。
QMT 策略代码实现
以下代码展示了如何在 QMT 中计算隐含波动率并执行基于偏斜的套利交易。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import time
from datetime import datetime
def init(ContextInfo):
# 1. 策略参数设置
ContextInfo.underlying = '510050.SH' # 标的:上证50ETF
ContextInfo.account_id = 'YOUR_ACCOUNT_ID' # 请替换为您的资金账号
ContextInfo.account_type = 'STOCK_OPTION' # 账号类型:期权
ContextInfo.skew_threshold = 0.05 # 偏斜阈值:5% (OTM IV - ATM IV)
ContextInfo.trade_qty = 1 # 交易手数
ContextInfo.holding = False # 持仓状态标记
# 2. 设置账号
ContextInfo.set_account(ContextInfo.account_id)
# 3. 费率与无风险利率设置
ContextInfo.risk_free_rate = 0.03 # 无风险利率 3%
ContextInfo.dividend_rate = 0.0 # 股息率
print("波动率偏斜策略初始化完成")
def handlebar(ContextInfo):
# 获取当前K线位置
index = ContextInfo.barpos
realtime = ContextInfo.get_bar_timetag(index)
current_date_str = timetag_to_datetime(realtime, '%Y%m%d')
# 1. 获取标的最新价格
underlying_data = ContextInfo.get_market_data_ex(
['close'], [ContextInfo.underlying], period='1d', count=1, subscribe=True
)
if ContextInfo.underlying not in underlying_data:
return
S = underlying_data[ContextInfo.underlying].iloc[-1]['close']
# 2. 获取期权链
# 获取当月或次月合约(这里简化为获取当前交易日可交易的期权)
# 注意:实际生产中应筛选特定到期月份
option_list = ContextInfo.get_option_list(ContextInfo.underlying, current_date_str, "PUT", True)
if not option_list:
print("未获取到期权合约列表")
return
# 3. 筛选 ATM 和 OTM 合约
# 获取合约详细信息以得到行权价和到期日
options_meta = []
for opt_code in option_list:
detail = ContextInfo.get_option_detail_data(opt_code)
if detail:
options_meta.append({
'code': opt_code,
'strike': detail['OptExercisePrice'],
'expire': detail['ExpireDate']
})
if not options_meta:
return
# 过滤出最近到期月的合约(简化逻辑:取列表中第一个到期日)
target_expire = min([x['expire'] for x in options_meta])
target_options = [x for x in options_meta if x['expire'] == target_expire]
# 计算剩余天数 (Days to Maturity)
expire_date = datetime.strptime(str(target_expire), '%Y%m%d')
curr_date = datetime.strptime(current_date_str, '%Y%m%d')
days_to_maturity = (expire_date - curr_date).days
if days_to_maturity <= 0:
return # 到期日不交易
days_year = days_to_maturity / 365.0
# 寻找 ATM (平值) 和 OTM (虚值)
# ATM: 行权价最接近标的价格
# OTM Put: 行权价 < 标的价格 (这里取比ATM低两档的合约作为示例)
sorted_options = sorted(target_options, key=lambda x: x['strike'])
# 找到 ATM 索引
atm_index = -1
min_diff = float('inf')
for i, opt in enumerate(sorted_options):
diff = abs(opt['strike'] - S)
if diff < min_diff:
min_diff = diff
atm_index = i
if atm_index == -1 or atm_index < 2:
return # 找不到合适的合约
atm_opt = sorted_options[atm_index]
otm_opt = sorted_options[atm_index - 2] # 选取行权价更低的虚值期权
# 4. 获取期权市场价格
opt_codes = [atm_opt['code'], otm_opt['code']]
opt_market_data = ContextInfo.get_market_data_ex(
['close'], opt_codes, period='1d', count=1, subscribe=True
)
if atm_opt['code'] not in opt_market_data or otm_opt['code'] not in opt_market_data:
return
price_atm = opt_market_data[atm_opt['code']].iloc[-1]['close']
price_otm = opt_market_data[otm_opt['code']].iloc[-1]['close']
# 5. 计算隐含波动率 (IV)
# bsm_iv(optionType, objectPrices, strikePrice, optionPrice, riskFree, days, dividend)
# 注意:days 参数在 bsm_iv 中通常指年化时间或具体天数,QMT API文档中days为剩余天数
iv_atm = ContextInfo.bsm_iv(
'P', S, atm_opt['strike'], price_atm,
ContextInfo.risk_free_rate, days_year * 365, ContextInfo.dividend_rate
)
iv_otm = ContextInfo.bsm_iv(
'P', S, otm_opt['strike'], price_otm,
ContextInfo.risk_free_rate, days_year * 365, ContextInfo.dividend_rate
)
# 过滤无效IV
if np.isnan(iv_atm) or np.isnan(iv_otm) or iv_atm <= 0.001:
return
skew = iv_otm - iv_atm
print(f"标的: {S:.3f}, ATM({atm_opt['strike']}) IV: {iv_atm:.2%}, OTM({otm_opt['strike']}) IV: {iv_otm:.2%}, Skew: {skew:.2%}")
# 6. 交易逻辑
# 如果未持仓且偏斜过大 (OTM IV 远高于 ATM IV),卖出 OTM,买入 ATM
if not ContextInfo.holding and skew > ContextInfo.skew_threshold:
print(f"触发偏斜套利信号: Skew {skew:.2%} > {ContextInfo.skew_threshold:.2%}")
# 卖出 OTM Put (Short High IV)
sell_open(otm_opt['code'], ContextInfo.trade_qty, 'LATEST', 0, ContextInfo, ContextInfo.account_id)
# 买入 ATM Put (Long Low IV) - 保护腿
buy_open(atm_opt['code'], ContextInfo.trade_qty, 'LATEST', 0, ContextInfo, ContextInfo.account_id)
ContextInfo.holding = True
ContextInfo.holding_codes = [otm_opt['code'], atm_opt['code']]
# 7. 平仓逻辑 (简化:当偏斜回归或到期前平仓)
# 这里演示:如果 Skew 回归到 1% 以内,平仓获利
elif ContextInfo.holding:
# 重新计算当前持仓合约的 Skew (逻辑同上,略)
# 简单起见,这里假设如果 Skew < 0.01 则平仓
if skew < 0.01:
print(f"偏斜回归,平仓: Skew {skew:.2%}")
# 平 OTM 空头 (买入平仓)
buy_close_tdayfirst(ContextInfo.holding_codes[0], ContextInfo.trade_qty, 'LATEST', 0, ContextInfo, ContextInfo.account_id)
# 平 ATM 多头 (卖出平仓)
sell_close_tdayfirst(ContextInfo.holding_codes[1], ContextInfo.trade_qty, 'LATEST', 0, ContextInfo, ContextInfo.account_id)
ContextInfo.holding = False
代码关键点解析
-
数据获取 (
get_market_data_ex&get_option_list):- 首先获取标的资产(如 510050.SH)的价格。
- 使用
get_option_list获取当前可交易的认沽(PUT)期权列表。 - 使用
get_option_detail_data获取行权价和到期日,以便筛选出 ATM 和 OTM 合约。
-
隐含波动率计算 (
bsm_iv):- 这是策略的核心。QMT 内置了
ContextInfo.bsm_iv函数,基于 Black-Scholes 模型反推波动率。 - 参数注意:需要传入标的价格、行权价、期权现价、无风险利率、剩余天数等。
- Skew 计算:
Skew = IV_OTM - IV_ATM。
- 这是策略的核心。QMT 内置了
-
合约选择逻辑:
- ATM (平值):行权价最接近标的现价的合约。
- OTM (虚值):对于认沽期权,行权价低于标的现价。代码中简单选取了比 ATM 低两档(index - 2)的合约。
-
交易执行:
- 开仓:当
Skew > 阈值时,卖出高 IV 的 OTM Put (sell_open),买入低 IV 的 ATM Put (buy_open)。 - 平仓:当
Skew回归正常(例如小于 1%)时,反向操作平仓。
- 开仓:当
注意事项
- 数据频率:上述代码在
handlebar中运行,如果是日线周期,每天计算一次。对于波动率套利,通常建议使用分钟线(如15m或1h)以捕捉盘中机会。 - 保证金风险:卖出期权(Short Put)涉及保证金交易。虽然买入了 ATM Put 构成了价差组合(Spread),限制了最大亏损,但实盘中需确保账户有足够的保证金。
- 流动性风险:深度虚值期权的流动性可能较差,买卖价差(Bid-Ask Spread)较大,可能会吞噬套利利润。建议在下单时使用限价单(
FIX)而非市价单。 - 模型假设:BSM 模型假设波动率是常数,而我们正是利用波动率不为常数(偏斜)来获利,计算出的 IV 仅作为相对参考指标。
Q&A
Q: 为什么通常使用认沽期权(Put)来做波动率偏斜套利?
A: 在股票市场中,投资者通常更担心市场崩盘而非暴涨,因此对虚值认沽期权(OTM Put)有强烈的避险需求,导致其价格被推高,隐含波动率显著高于平值期权,形成了典型的“波动率微笑”左侧高翘的形态。
Q: ContextInfo.bsm_iv 计算失败返回什么?
A: 如果输入参数不合理(例如期权价格低于内在价值,或者计算不收敛),该函数可能返回 nan (Not a Number)。代码中必须包含 np.isnan() 的检查逻辑,否则会导致程序报错或产生错误信号。
Q: 如何优化合约的选择?
A: 代码中简单使用了“低两档”的合约。更严谨的做法是根据 Delta 值来选择,例如选择 Delta = -0.5 的作为 ATM,Delta = -0.2 的作为 OTM,这样能更标准化地衡量偏斜程度。