问题描述
解决方案
这是一个非常经典的期权波动率交易策略问题。要实现这个策略,我们需要解决两个核心问题:如何计算隐含波动率(Implied Volatility, IV) 以及 如何构建基于IV分位数的均值回归策略。
以下是详细的理论解释和完整的JoinQuant策略代码。
第一部分:如何计算期权的隐含波动率
隐含波动率是将期权的市场价格代入期权定价模型(通常是Black-Scholes-Merton模型),反推出来的波动率参数。由于BSM公式中波动率 $\sigma$ 无法直接通过解析解求出,通常使用数值方法(如二分法或牛顿-拉夫逊法)来逼近求解。
计算步骤:
- 获取参数:标的价格($S$)、执行价格($K$)、无风险利率($r$)、剩余到期时间($T$)、期权市场价格($C$ 或 $P$)。
- 建立方程:$f(\sigma) = BSM(\sigma) - MarketPrice = 0$。
- 求解:寻找使方程成立的 $\sigma$。
第二部分:策略逻辑 (IV均值回归)
策略思想:隐含波动率具有均值回归的特性。
- IV过高时:市场恐慌或过度定价,预期波动率会下降。此时做空波动率(卖出跨式组合 Short Straddle),赚取权利金和波动率下降的收益。
- IV过低时:市场过于平静,预期波动率会上升。此时做多波动率(买入跨式组合 Long Straddle),通过标的大幅波动获利。
具体规则:
- 选取50ETF作为标的。
- 选取当月平值(ATM)期权合约。
- 计算过去N天(如20天)的IV历史数据,计算当前的IV分位数(Percentile)。
- 开仓:
- 若 IV分位数 > 80%:卖出平值Call + 卖出平值Put(Short Straddle)。
- 若 IV分位数 < 20%:买入平值Call + 买入平值Put(Long Straddle)。
- 平仓:当IV回归到中位数(40%-60%)或合约临近到期时平仓。
第三部分:JoinQuant 策略代码实现
以下代码包含了一个完整的隐含波动率计算器和交易策略。
# -*- coding: utf-8 -*-
from jqdata import *
import math
import numpy as np
import pandas as pd
from scipy.stats import norm
from scipy.optimize import brentq
import datetime
def initialize(context):
# 设定基准
set_benchmark('510050.XSHG')
# 设定滑点
set_slippage(FixedSlippage(0.002))
# 设定手续费 (期权手续费较高,需根据实际情况设定)
set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=5, close_commission=5, min_commission=5), type='option')
# 策略参数
g.underlying = '510050.XSHG' # 标的:50ETF
g.window = 20 # 观察IV的历史窗口
g.high_threshold = 80 # IV高分位阈值
g.low_threshold = 20 # IV低分位阈值
g.risk_free_rate = 0.03 # 无风险利率假设 3%
# 存储历史IV
g.iv_history = []
# 每天运行一次
run_daily(trade, '14:50')
# ================= 工具函数:BSM模型与IV计算 =================
def bsm_price(S, K, T, r, sigma, option_type):
"""计算BSM模型理论价格"""
if T <= 0 or sigma <= 0:
return 0
d1 = (math.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * math.sqrt(T))
d2 = d1 - sigma * math.sqrt(T)
if option_type == 'call':
price = S * norm.cdf(d1) - K * math.exp(-r * T) * norm.cdf(d2)
else:
price = K * math.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
return price
def implied_volatility(price, S, K, T, r, option_type):
"""
反推隐含波动率
使用 Brent 方法求解 f(sigma) = bsm_price(sigma) - market_price = 0
"""
if price <= 0:
return 0
# 定义目标函数
def objective_function(sigma):
return bsm_price(S, K, T, r, sigma, option_type) - price
try:
# 在 0.001 到 5.0 (500%) 之间寻找解
iv = brentq(objective_function, 0.001, 5.0)
return iv
except:
return 0
# ================= 策略逻辑 =================
def get_atm_options(context, underlying):
"""获取当月平值期权合约 (Call 和 Put)"""
# 获取所有期权
all_options = get_all_securities(types=['option'], date=context.current_dt).index.tolist()
# 筛选出标的为50ETF的期权
options = [op for op in all_options if get_security_info(op).display_name.startswith('50ETF')]
if not options:
return None, None, None
# 获取标的价格
s_price = get_current_data()[underlying].last_price
# 获取期权详细信息
valid_options = []
for op in options:
info = get_security_info(op)
# 筛选未到期且剩余时间大于5天的合约(避免临期风险)
days_to_maturity = (info.end_date - context.current_dt.date()).days
if 5 < days_to_maturity < 40: # 选取近月合约
# 解析行权价 (JoinQuant期权代码通常不直接包含K,需从info获取或解析)
# 这里简化处理,假设我们能获取到行权价,实际需调用 get_security_info
# 注意:实际回测中可能需要通过 name 解析或额外数据源获取 K
# 这里的逻辑是简化的,实际需根据API返回的 strike_price 字段
# 假设 info 对象没有 strike_price,我们用 naming convention 或额外数据
# 聚宽 get_security_info 返回对象通常不含 strike_price,需要用 get_option_contract_info (如果存在)
# 或者解析 display_name 如 "50ETF购9月3000"
# 为了代码可运行,我们尝试解析 display_name 中的行权价
# 示例名: "50ETF购10月2.800"
try:
name = info.display_name
strike_str = name.split('月')[-1]
strike = float(strike_str)
op_type = 'call' if '购' in name else 'put'
valid_options.append({
'code': op,
'strike': strike,
'end_date': info.end_date,
'type': op_type,
'T': days_to_maturity / 365.0
})
except:
continue
if not valid_options:
return None, None, None
# 找到行权价最接近标的价格的合约(平值)
valid_options.sort(key=lambda x: abs(x['strike'] - s_price))
best_strike = valid_options[0]['strike']
# 筛选出该行权价的 Call 和 Put
atm_call = None
atm_put = None
for op in valid_options:
if abs(op['strike'] - best_strike) < 0.001:
if op['type'] == 'call':
atm_call = op
elif op['type'] == 'put':
atm_put = op
return atm_call, atm_put, s_price
def trade(context):
# 1. 获取平值期权
atm_call_info, atm_put_info, s_price = get_atm_options(context, g.underlying)
if not atm_call_info or not atm_put_info:
return
# 2. 获取期权当前价格
current_data = get_current_data()
call_price = current_data[atm_call_info['code']].last_price
put_price = current_data[atm_put_info['code']].last_price
# 3. 计算隐含波动率 (取Call和Put的平均值)
iv_call = implied_volatility(call_price, s_price, atm_call_info['strike'], atm_call_info['T'], g.risk_free_rate, 'call')
iv_put = implied_volatility(put_price, s_price, atm_put_info['strike'], atm_put_info['T'], g.risk_free_rate, 'put')
current_iv = (iv_call + iv_put) / 2.0
if current_iv == 0:
return
# 4. 更新IV历史数据
g.iv_history.append(current_iv)
if len(g.iv_history) > g.window:
g.iv_history.pop(0)
# 数据不足时不交易
if len(g.iv_history) < g.window:
return
# 5. 计算分位数
iv_array = np.array(g.iv_history)
percentile = sum(iv_array < current_iv) / len(iv_array) * 100
log.info(f"当前IV: {current_iv:.4f}, IV分位数: {percentile:.2f}%")
# 6. 交易信号判断
positions = context.portfolio.positions
# === 信号:IV过高,做空波动率 (卖出跨式) ===
if percentile > g.high_threshold:
# 如果当前持有买入仓位,先平仓
for security in list(positions.keys()):
if positions[security].total_amount > 0: # 多头持仓
order_target(security, 0)
# 开空仓 (注意:需确保账户有卖空权限,回测中通常支持)
# 卖出 Call 和 Put
if positions[atm_call_info['code']].total_amount == 0:
order(atm_call_info['code'], -10) # 卖出10张
if positions[atm_put_info['code']].total_amount == 0:
order(atm_put_info['code'], -10) # 卖出10张
log.info("IV高位,卖出跨式组合 (Short Straddle)")
# === 信号:IV过低,做多波动率 (买入跨式) ===
elif percentile < g.low_threshold:
# 如果当前持有卖出仓位,先平仓
for security in list(positions.keys()):
if positions[security].total_amount < 0: # 空头持仓
order_target(security, 0)
# 开多仓
if positions[atm_call_info['code']].total_amount == 0:
order(atm_call_info['code'], 10)
if positions[atm_put_info['code']].total_amount == 0:
order(atm_put_info['code'], 10)
log.info("IV低位,买入跨式组合 (Long Straddle)")
# === 均值回归平仓 (可选) ===
elif 40 < percentile < 60:
# IV 回归中位数附近,平掉所有仓位止盈/止损
for security in list(positions.keys()):
order_target(security, 0)
log.info("IV回归中值,平仓观望")
代码关键点解析
-
IV计算核心 (
implied_volatility):- 使用了
scipy.optimize.brentq(布伦特法)来求解方程。这是比二分法更快、比牛顿法更稳定的求根算法。 - 目标是找到一个
sigma,使得BSM公式计算出的理论价格等于当前市场价格。
- 使用了
-
合约筛选 (
get_atm_options):- 策略自动寻找**平值(ATM)**合约,即行权价最接近当前标的价格的合约。
- 筛选了剩余到期时间在5到40天之间的近月合约,因为近月合约流动性最好,且对波动率变化最敏感(Vega较大),但避开了临近到期日(<5天)的剧烈Gamma风险。
-
交易逻辑:
- Short Straddle (卖出跨式):当IV处于历史高位(>80分位),预期IV会下降或标的横盘。卖出Call和Put,赚取权利金。
- Long Straddle (买入跨式):当IV处于历史低位(<20分位),预期会有大行情或IV反弹。买入Call和Put。
注意事项
- 数据权限:期权数据在JoinQuant通常需要申请权限或购买数据包。
- 行权价解析:代码中通过解析
display_name来获取行权价(如"50ETF购10月2.800"),这在回测中通常有效,但在实盘或数据结构变更时可能需要调整。 - 风险提示:
- 卖出跨式(Short Straddle)风险无限:如果标的价格发生剧烈单边波动,卖方亏损可能无限大。实盘中通常会买入更虚值的期权构建**铁以此蝶式(Iron Butterfly)**来锁定最大亏损。
- 保证金:卖出期权需要缴纳保证金,回测中需确保资金充足。
Q&A
Q: 为什么使用平值期权(ATM)来计算IV?
A: 平值期权的流动性通常最好,且其Vega值(价格对波动率的敏感度)最大,最能代表市场的平均波动率水平。
Q: 代码中的 brentq 求解失败怎么办?
A: 代码中使用了 try...except 块。如果期权价格深度实值或虚值,或者市场流动性缺失导致价格异常,IV可能无法计算,此时函数返回0,策略会跳过该次交易。
Q: 如何控制卖方风险?
A: 可以在代码中加入止损逻辑。例如,当组合亏损达到权利金的某一比例(如50%)时强制平仓。或者将策略改为卖出宽跨式(Strangle)或铁鹰式(Iron Condor)以限制风险。