🚀 AI 一键生成 joinquant 策略代码

如何在 JoinQuant 平台计算期权的隐含波动率并进行交易?

joinquant | 发布时间: 2025/12/18
以下内容由 EasyQuant 生成。

问题描述

如何计算期权的隐含波动率,并编写一个在隐含波动率高时卖出、低时买入的策略?

解决方案

这是一个非常经典的期权波动率交易策略问题。要实现这个策略,我们需要解决两个核心问题:如何计算隐含波动率(Implied Volatility, IV) 以及 如何构建基于IV分位数的均值回归策略

以下是详细的理论解释和完整的JoinQuant策略代码。

第一部分:如何计算期权的隐含波动率

隐含波动率是将期权的市场价格代入期权定价模型(通常是Black-Scholes-Merton模型),反推出来的波动率参数。由于BSM公式中波动率 $\sigma$ 无法直接通过解析解求出,通常使用数值方法(如二分法或牛顿-拉夫逊法)来逼近求解。

计算步骤:

  1. 获取参数:标的价格($S$)、执行价格($K$)、无风险利率($r$)、剩余到期时间($T$)、期权市场价格($C$ 或 $P$)。
  2. 建立方程:$f(\sigma) = BSM(\sigma) - MarketPrice = 0$。
  3. 求解:寻找使方程成立的 $\sigma$。

第二部分:策略逻辑 (IV均值回归)

策略思想:隐含波动率具有均值回归的特性。

  • IV过高时:市场恐慌或过度定价,预期波动率会下降。此时做空波动率(卖出跨式组合 Short Straddle),赚取权利金和波动率下降的收益。
  • IV过低时:市场过于平静,预期波动率会上升。此时做多波动率(买入跨式组合 Long Straddle),通过标的大幅波动获利。

具体规则

  1. 选取50ETF作为标的。
  2. 选取当月平值(ATM)期权合约。
  3. 计算过去N天(如20天)的IV历史数据,计算当前的IV分位数(Percentile)。
  4. 开仓
    • 若 IV分位数 > 80%:卖出平值Call + 卖出平值Put(Short Straddle)。
    • 若 IV分位数 < 20%:买入平值Call + 买入平值Put(Long Straddle)。
  5. 平仓:当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回归中值,平仓观望")

代码关键点解析

  1. IV计算核心 (implied_volatility)

    • 使用了 scipy.optimize.brentq(布伦特法)来求解方程。这是比二分法更快、比牛顿法更稳定的求根算法。
    • 目标是找到一个 sigma,使得BSM公式计算出的理论价格等于当前市场价格。
  2. 合约筛选 (get_atm_options)

    • 策略自动寻找**平值(ATM)**合约,即行权价最接近当前标的价格的合约。
    • 筛选了剩余到期时间在5到40天之间的近月合约,因为近月合约流动性最好,且对波动率变化最敏感(Vega较大),但避开了临近到期日(<5天)的剧烈Gamma风险。
  3. 交易逻辑

    • Short Straddle (卖出跨式):当IV处于历史高位(>80分位),预期IV会下降或标的横盘。卖出Call和Put,赚取权利金。
    • Long Straddle (买入跨式):当IV处于历史低位(<20分位),预期会有大行情或IV反弹。买入Call和Put。

注意事项

  1. 数据权限:期权数据在JoinQuant通常需要申请权限或购买数据包。
  2. 行权价解析:代码中通过解析 display_name 来获取行权价(如"50ETF购10月2.800"),这在回测中通常有效,但在实盘或数据结构变更时可能需要调整。
  3. 风险提示
    • 卖出跨式(Short Straddle)风险无限:如果标的价格发生剧烈单边波动,卖方亏损可能无限大。实盘中通常会买入更虚值的期权构建**铁以此蝶式(Iron Butterfly)**来锁定最大亏损。
    • 保证金:卖出期权需要缴纳保证金,回测中需确保资金充足。

Q&A

Q: 为什么使用平值期权(ATM)来计算IV?
A: 平值期权的流动性通常最好,且其Vega值(价格对波动率的敏感度)最大,最能代表市场的平均波动率水平。

Q: 代码中的 brentq 求解失败怎么办?
A: 代码中使用了 try...except 块。如果期权价格深度实值或虚值,或者市场流动性缺失导致价格异常,IV可能无法计算,此时函数返回0,策略会跳过该次交易。

Q: 如何控制卖方风险?
A: 可以在代码中加入止损逻辑。例如,当组合亏损达到权利金的某一比例(如50%)时强制平仓。或者将策略改为卖出宽跨式(Strangle)或铁鹰式(Iron Condor)以限制风险。