🚀 AI 一键生成 qmt 策略代码

基于 get_option_iv 的波动率回归交易策略实现

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

问题描述

如何获取期权隐含波动率并编写波动率套利策略?

解决方案

获取期权隐含波动率及波动率套利策略

在 QMT 平台中,获取期权隐含波动率(Implied Volatility, IV)主要有两种方式:直接获取系统计算好的实时 IV,或者利用 BSM 模型根据市场价格自行反推。

波动率套利策略通常涉及 Delta 中性策略(如跨式组合 Straddle 或 宽跨式组合 Strangle)。

以下将详细介绍如何获取数据,并提供一个完整的 做多波动率(Long Straddle) 策略示例。该策略在隐含波动率处于历史低位时买入跨式组合(同时买入平值认购和认沽),在波动率回归高位或获利时平仓。

1. 获取隐含波动率的方法

方法一:直接获取实时 IV (推荐)

使用 ContextInfo.get_option_iv(optioncode) 接口。这是最简单的方法,直接返回交易所或柜台计算的实时数据。

iv = ContextInfo.get_option_iv('10003280.SHO')
print(f"实时隐含波动率: {iv}")

方法二:使用 BSM 模型反推

使用 ContextInfo.bsm_iv(...) 接口。这适用于回测或需要自定义无风险利率、剩余天数等参数的场景。

# 参数:期权类型, 标的价格, 行权价, 期权现价, 无风险利率, 剩余天数, 分红率
iv = ContextInfo.bsm_iv('C', 3.51, 3.5, 0.0725, 0.03, 15, 0)

2. 波动率套利策略实现 (做多波动率)

策略逻辑:

  1. 标的:50ETF (510050.SH) 及其对应的期权。
  2. 合约选择:选择当月到期的 平值 (ATM) 认购 (Call) 和 认沽 (Put) 期权。
  3. 入场条件:当平值期权的平均隐含波动率低于设定阈值(例如 15%),且当前无持仓时,构建买入跨式组合(Buy Call + Buy Put)。
  4. 出场条件:当平均隐含波动率高于设定阈值(例如 25%),或期权临近到期(剩余天数 < 3天)时,平掉所有仓位。
  5. 风控:简单的全平仓逻辑。

完整策略代码

# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import datetime
import time

def init(ContextInfo):
    # 1. 设置账户信息 (请替换为您的真实资金账号)
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
    ContextInfo.account_type = 'STOCK_OPTION' # 账号类型:股票期权
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 2. 策略参数设置
    ContextInfo.underlying = '510050.SH'  # 标的:50ETF
    ContextInfo.low_iv_threshold = 0.15   # 入场阈值:IV < 15%
    ContextInfo.high_iv_threshold = 0.25  # 出场阈值:IV > 25%
    ContextInfo.days_to_expire_limit = 3  # 临近到期天数限制
    ContextInfo.trade_unit = 1            # 每次交易张数
    
    # 3. 全局变量
    ContextInfo.holding_call = None       # 当前持有的Call代码
    ContextInfo.holding_put = None        # 当前持有的Put代码
    
    print("波动率套利策略初始化完成")

def handlebar(ContextInfo):
    # 仅在最后一根K线(实时行情)或回测的每一根K线运行
    # 如果是实时盘中,建议加上 ContextInfo.is_last_bar() 判断
    
    # 获取当前时间
    current_time = timetag_to_datetime(ContextInfo.get_bar_timetag(ContextInfo.barpos), '%Y%m%d')
    
    # 1. 获取标的资产(50ETF)的最新价格
    underlying_data = ContextInfo.get_market_data_ex(
        ['close'], [ContextInfo.underlying], period='1d', count=1, subscribe=True
    )
    if ContextInfo.underlying not in underlying_data:
        return
    
    underlying_price = underlying_data[ContextInfo.underlying].iloc[-1]['close']
    
    # 2. 获取当月期权合约列表
    # 获取当前年月,格式 YYYYMM
    current_month = current_time[0:6] 
    
    # 获取认购和认沽合约列表
    # 注意:get_option_list 第四个参数 True 表示只获取当前可交易的
    calls = ContextInfo.get_option_list(ContextInfo.underlying, current_month, "CALL", True)
    puts = ContextInfo.get_option_list(ContextInfo.underlying, current_month, "PUT", True)
    
    if not calls or not puts:
        print("未获取到期权合约列表")
        return

    # 3. 筛选平值合约 (ATM)
    # 逻辑:寻找行权价与标的价格差值最小的合约
    atm_call = get_atm_contract(ContextInfo, calls, underlying_price)
    atm_put = get_atm_contract(ContextInfo, puts, underlying_price)
    
    if not atm_call or not atm_put:
        return

    # 4. 获取隐含波动率 (IV)
    iv_call = ContextInfo.get_option_iv(atm_call)
    iv_put = ContextInfo.get_option_iv(atm_put)
    
    # 异常值处理
    if iv_call <= 0 or iv_put <= 0:
        # 如果实时IV获取失败,可以使用bsm_iv自行计算,这里简单跳过
        return
        
    avg_iv = (iv_call + iv_put) / 2
    
    # 5. 检查到期日
    # 获取合约详细信息以计算剩余天数
    detail = ContextInfo.get_instrumentdetail(atm_call)
    expire_date_str = str(detail['ExpireDate']) # 格式 YYYYMMDD
    days_left = days_between(current_time, expire_date_str)
    
    print(f"日期:{current_time}, 标的价格:{underlying_price:.3f}, ATM Call:{atm_call}, ATM Put:{atm_put}, 平均IV:{avg_iv:.4f}, 剩余天数:{days_left}")

    # 6. 交易逻辑
    
    # 情况A:临近到期,强制平仓
    if days_left <= ContextInfo.days_to_expire_limit:
        if ContextInfo.holding_call or ContextInfo.holding_put:
            print(f"临近到期({days_left}天),强制平仓")
            close_positions(ContextInfo)
        return

    # 情况B:当前无持仓,且 IV 低于阈值 -> 开仓 (做多波动率)
    if ContextInfo.holding_call is None and ContextInfo.holding_put is None:
        if avg_iv < ContextInfo.low_iv_threshold:
            print(f"IV ({avg_iv:.4f}) 低于阈值,开仓买入跨式组合")
            
            # 买入认购
            passorder(50, 1101, ContextInfo.account_id, atm_call, 5, -1, ContextInfo.trade_unit, ContextInfo)
            # 买入认沽
            passorder(50, 1101, ContextInfo.account_id, atm_put, 5, -1, ContextInfo.trade_unit, ContextInfo)
            
            # 更新持仓记录
            ContextInfo.holding_call = atm_call
            ContextInfo.holding_put = atm_put

    # 情况C:当前有持仓,且 IV 高于阈值 -> 平仓 (止盈)
    elif ContextInfo.holding_call is not None and ContextInfo.holding_put is not None:
        if avg_iv > ContextInfo.high_iv_threshold:
            print(f"IV ({avg_iv:.4f}) 高于阈值,平仓获利")
            close_positions(ContextInfo)
            
        # 注意:实际交易中,如果标的价格大幅波动导致持仓不再是平值,
        # 可能需要进行 Delta 对冲或移仓(Rolling),本示例简化为持有直到IV回归或到期。

def get_atm_contract(ContextInfo, contract_list, underlying_price):
    """
    获取平值合约
    """
    min_diff = 99999.0
    target_contract = None
    
    for code in contract_list:
        detail = ContextInfo.get_instrumentdetail(code)
        strike_price = detail['OptExercisePrice']
        diff = abs(strike_price - underlying_price)
        
        if diff < min_diff:
            min_diff = diff
            target_contract = code
            
    return target_contract

def days_between(d1, d2):
    """
    计算两个日期字符串(YYYYMMDD)之间的天数差
    """
    try:
        date1 = datetime.datetime.strptime(d1, "%Y%m%d")
        date2 = datetime.datetime.strptime(d2, "%Y%m%d")
        return (date2 - date1).days
    except:
        return 0

def close_positions(ContextInfo):
    """
    平掉所有记录的持仓
    """
    if ContextInfo.holding_call:
        # 卖出平仓 Call
        passorder(51, 1101, ContextInfo.account_id, ContextInfo.holding_call, 5, -1, ContextInfo.trade_unit, ContextInfo)
        ContextInfo.holding_call = None
        
    if ContextInfo.holding_put:
        # 卖出平仓 Put
        passorder(51, 1101, ContextInfo.account_id, ContextInfo.holding_put, 5, -1, ContextInfo.trade_unit, ContextInfo)
        ContextInfo.holding_put = None

代码关键点解析

  1. 获取期权链 (get_option_list)

    • 我们使用 ContextInfo.get_option_list 配合当前月份,动态获取当月的所有认购和认沽合约。
    • 参数 True 确保只获取当前可交易的合约(过滤掉未上市或已退市的)。
  2. 计算平值合约 (get_atm_contract)

    • 通过遍历合约列表,利用 get_instrumentdetail 获取行权价 (OptExercisePrice)。
    • 计算行权价与标的现价的差值绝对值,最小者即为平值合约 (ATM)。
  3. 获取隐含波动率 (get_option_iv)

    • 直接调用 QMT 内置接口。这是做波动率策略的核心数据源。
    • 策略中取 Call 和 Put 的 IV 平均值作为市场情绪的参考。
  4. 交易指令 (passorder)

    • opType=50: 买入开仓。
    • opType=51: 卖出平仓。
    • prType=5: 最新价下单(回测常用,实盘建议用对手价 14 或限价)。
  5. 到期日管理

    • 期权策略必须严格管理时间价值衰减(Theta)。代码中设置了 days_to_expire_limit,在临近到期前强制平仓,避免进入交割月或承受过大的 Gamma 风险。

注意事项

  • 数据权限:获取期权数据(特别是 IV 和 Greeks)通常需要开通 Level-2 或特定的期权数据权限,请确认您的 QMT 账号具备此权限。
  • 实盘调整
    • 实盘中下单价格类型建议修改为对手价或挂单价,避免滑点。
    • 实盘中需要通过 get_trade_detail_data 实时查询真实持仓,而不是仅依赖 ContextInfo.holding_call 变量,以防止程序重启后状态丢失。
  • Delta 对冲:上述策略是纯粹的 IV 投机。更专业的波动率套利需要在持有期间不断调整标的或期权仓位,使组合的 Delta 保持为 0(Delta Neutral),从而剥离方向性风险,纯粹赚取 Vega(波动率)收益。

Q&A

Q1: 为什么 get_option_iv 返回的值有时候是 0 或负数?
A1: 这通常发生在期权流动性极差、深度实值/虚值,或者刚刚开盘数据尚未稳定时。在策略中必须对 IV 进行有效性过滤(如 if iv > 0.01),避免基于错误数据下单。

Q2: 如何实现 Delta 中性对冲(Delta Neutral)?
A2: 您可以使用 ContextInfo.get_option_detail_data(code) 获取期权的 Greeks(如 Delta),或者使用 BSM 公式自行计算。当组合的 Delta 偏离 0 超过一定阈值时,通过买卖标的资产(如 50ETF)来进行对冲。

Q3: 回测时如何设置期权的手续费?
A3: 在 init 函数中使用 ContextInfo.set_commission(...)。期权通常按张收费(例如每张 2 元),不同于股票的按金额比例收费。设置时需注意 commissionType 参数的选择。