🚀 AI 一键生成 qmt 策略代码

如何实现 Gamma Scalping 策略来赚取波动收益?

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

问题描述

如何编写一个 Gamma Scalping 策略,实现 Delta 中性并根据价格波动进行对冲交易?

解决方案

这是一个关于 Gamma Scalping(伽马剥头皮) 策略的实现请求。

策略逻辑分析

Gamma Scalping 是一种利用期权 Gamma 特性的波动率交易策略。其核心思想是:

  1. 建立底仓:通常买入跨式组合(Long Straddle,同时买入 ATM Call 和 ATM Put),构建一个 Delta 中性但 Gamma 为正的投资组合。
  2. 动态对冲
    • 当标的价格上涨时,Call 的 Delta 增加(变得更正),Put 的 Delta 增加(变得不那么负),总 Delta 变为正数。为了保持中性,需要卖出标的资产。
    • 当标的价格下跌时,Call 的 Delta 减少,Put 的 Delta 减少(变得更负),总 Delta 变为负数。为了保持中性,需要买入标的资产。
  3. 获利来源:通过“高抛低吸”标的资产来覆盖期权的时间价值损耗(Theta),并赚取标的实际波动率大于隐含波动率时的差价。

QMT 实现关键点

  1. 期权希腊字母计算:QMT 提供了 bsm_iv (隐含波动率) 和 bsm_price (理论价),但没有直接提供实时 Delta 的接口。我们需要基于 BSM 公式手写一个 Delta 计算函数。
  2. 合约筛选:需要自动获取当前标的(如 510050.SH)的期权链,并筛选出平值(ATM)合约。
  3. 对冲阈值:为了避免频繁交易导致手续费过高,需要设置一个 Delta 偏离阈值(Threshold),只有当 Net Delta 超过该值时才进行对冲。

策略代码实现

# -*- coding: gbk -*-
import numpy as np
from scipy.stats import norm
import datetime
import math

'''
Gamma Scalping 策略
标的:510050.SH (上证50ETF)
逻辑:
1. 初始买入 ATM 跨式组合 (Long Straddle)
2. 实时计算组合 Delta
3. 当 Delta 偏离超过阈值时,交易标的 ETF 进行对冲,使其回归 Delta Neutral
'''

def init(ContextInfo):
    # --- 策略参数设置 ---
    ContextInfo.underlying = '510050.SH'  # 标的资产
    ContextInfo.account_id = '6000000000' # 请替换为您的资金账号
    ContextInfo.account_type = 'STOCK'    # 账号类型
    ContextInfo.hedge_threshold = 0.1     # Delta 对冲阈值 (绝对值)
    ContextInfo.option_qty = 10           # 期权手数
    ContextInfo.risk_free_rate = 0.03     # 无风险利率
    
    # 交易相关设置
    ContextInfo.set_account(ContextInfo.account_id)
    ContextInfo.set_universe([ContextInfo.underlying])
    
    # 全局变量
    ContextInfo.call_contract = None
    ContextInfo.put_contract = None
    ContextInfo.multiplier = 10000 # 50ETF期权合约乘数通常为10000
    
    print("Gamma Scalping 策略初始化完成")

def handlebar(ContextInfo):
    # 获取当前标的价格
    if ContextInfo.is_last_bar():
        # 获取标的最新价
        tick_data = ContextInfo.get_full_tick([ContextInfo.underlying])
        if not tick_data:
            return
        
        S = tick_data[ContextInfo.underlying]['lastPrice']
        
        # 1. 如果没有持仓,构建初始跨式组合 (Long Straddle)
        if ContextInfo.call_contract is None or ContextInfo.put_contract is None:
            open_straddle(ContextInfo, S)
            return

        # 2. 计算当前组合的 Net Delta
        net_delta = calculate_portfolio_delta(ContextInfo, S)
        
        # 3. 检查是否需要对冲
        check_and_hedge(ContextInfo, net_delta, S)

def open_straddle(ContextInfo, current_price):
    """
    构建跨式组合:寻找最近月、最平值的 Call 和 Put
    """
    # 获取期权列表 (这里简化逻辑,获取当月合约,实际需根据日期判断)
    # 获取当前日期
    current_date = datetime.datetime.now().strftime('%Y%m%d')
    
    # 获取标的对应的期权列表 (Call 和 Put)
    # 注意:get_option_list 第三个参数为空表示获取所有类型,第四个参数True表示仅获取可交易的
    options = ContextInfo.get_option_list(ContextInfo.underlying, current_date, "", True)
    
    if not options:
        print("未找到可用期权合约")
        return

    # 筛选出 Call 和 Put
    calls = [opt for opt in options if opt.endswith('C.SHO') or '购' in ContextInfo.get_instrumentdetail(opt)['InstrumentName']]
    puts = [opt for opt in options if opt.endswith('P.SHO') or '沽' in ContextInfo.get_instrumentdetail(opt)['InstrumentName']]
    
    # 寻找行权价最接近当前价格的合约 (ATM)
    atm_call = min(calls, key=lambda x: abs(ContextInfo.get_instrumentdetail(x)['OptExercisePrice'] - current_price))
    # 对应的 Put 通常行权价相同
    target_strike = ContextInfo.get_instrumentdetail(atm_call)['OptExercisePrice']
    atm_put = min(puts, key=lambda x: abs(ContextInfo.get_instrumentdetail(x)['OptExercisePrice'] - target_strike))
    
    print(f"构建跨式组合: Call={atm_call}, Put={atm_put}, 行权价={target_strike}, 标的价={current_price}")
    
    # 下单买入
    # 50: 买入开仓
    passorder(50, 1101, ContextInfo.account_id, atm_call, 5, -1, ContextInfo.option_qty, ContextInfo)
    passorder(50, 1101, ContextInfo.account_id, atm_put, 5, -1, ContextInfo.option_qty, ContextInfo)
    
    ContextInfo.call_contract = atm_call
    ContextInfo.put_contract = atm_put

def calculate_bsm_delta(S, K, T, r, sigma, option_type):
    """
    计算 BSM Delta
    S: 标的价格
    K: 行权价
    T: 剩余期限 (年)
    r: 无风险利率
    sigma: 隐含波动率
    option_type: 'C' or 'P'
    """
    if T <= 0 or sigma <= 0:
        return 0
        
    d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
    
    if option_type == 'C':
        return norm.cdf(d1)
    else:
        return norm.cdf(d1) - 1

def calculate_portfolio_delta(ContextInfo, S):
    """
    计算组合总 Delta (包括期权和标的持仓)
    """
    # 1. 获取期权详细信息
    call_detail = ContextInfo.get_instrumentdetail(ContextInfo.call_contract)
    put_detail = ContextInfo.get_instrumentdetail(ContextInfo.put_contract)
    
    K_call = call_detail['OptExercisePrice']
    K_put = put_detail['OptExercisePrice']
    
    # 计算剩余时间 T (年化)
    expire_date_str = str(call_detail['ExpireDate'])
    expire_date = datetime.datetime.strptime(expire_date_str, '%Y%m%d')
    now = datetime.datetime.now()
    days_to_expire = (expire_date - now).days
    T = max(days_to_expire / 365.0, 0.0001)
    
    # 2. 获取期权市场价格用于计算 IV
    call_tick = ContextInfo.get_full_tick([ContextInfo.call_contract])
    put_tick = ContextInfo.get_full_tick([ContextInfo.put_contract])
    
    if not call_tick or not put_tick:
        return 0
        
    call_price = call_tick[ContextInfo.call_contract]['lastPrice']
    put_price = put_tick[ContextInfo.put_contract]['lastPrice']
    
    # 3. 计算隐含波动率 (IV)
    # bsm_iv(optionType, objectPrices, strikePrice, optionPrice, riskFree, days, dividend)
    # 注意:QMT的bsm_iv days参数通常指天数
    iv_call = ContextInfo.bsm_iv('C', S, K_call, call_price, ContextInfo.risk_free_rate, days_to_expire, 0)
    iv_put = ContextInfo.bsm_iv('P', S, K_put, put_price, ContextInfo.risk_free_rate, days_to_expire, 0)
    
    # 4. 计算单张合约 Delta
    delta_call_unit = calculate_bsm_delta(S, K_call, T, ContextInfo.risk_free_rate, iv_call, 'C')
    delta_put_unit = calculate_bsm_delta(S, K_put, T, ContextInfo.risk_free_rate, iv_put, 'P')
    
    # 5. 获取当前持仓数量
    positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
    
    pos_call = 0
    pos_put = 0
    pos_stock = 0
    
    for pos in positions:
        if pos.m_strInstrumentID == ContextInfo.call_contract:
            pos_call = pos.m_nVolume
        elif pos.m_strInstrumentID == ContextInfo.put_contract:
            pos_put = pos.m_nVolume
        elif pos.m_strInstrumentID == ContextInfo.underlying:
            pos_stock = pos.m_nVolume # 股票/ETF持仓 Delta 为 1
    
    # 6. 计算总 Delta
    # 总 Delta = (Call Delta * Call 数量 * 乘数) + (Put Delta * Put 数量 * 乘数) + (标的 Delta * 标的数量)
    # 注意:Put Delta 为负数
    total_delta = (delta_call_unit * pos_call * ContextInfo.multiplier) + \
                  (delta_put_unit * pos_put * ContextInfo.multiplier) + \
                  (1.0 * pos_stock)
                  
    print(f"Delta监控: Call_IV={iv_call:.2f}, Put_IV={iv_put:.2f}, UnitDelta(C)={delta_call_unit:.2f}, UnitDelta(P)={delta_put_unit:.2f}, StockPos={pos_stock}, TotalDelta={total_delta:.2f}")
    
    return total_delta

def check_and_hedge(ContextInfo, net_delta, current_price):
    """
    检查并执行对冲
    """
    threshold = ContextInfo.hedge_threshold * ContextInfo.option_qty * ContextInfo.multiplier
    
    # 如果 Net Delta 绝对值超过阈值 (例如 0.1 * 10 * 10000 = 10000 Delta,即相当于10000股ETF)
    # 这里阈值设置需要根据实际资金量和风险偏好调整,示例中 threshold 设为 0.1 股是不合理的,
    # 通常阈值设为几百或几千 Delta (对应几百或几千股标的)
    
    # 修正阈值逻辑:假设我们容忍 0.2 的 Delta 偏离 (相对于1手期权)
    # 实际容忍 Delta = 0.2 * 持仓手数 * 10000
    real_threshold = 0.2 * ContextInfo.option_qty * ContextInfo.multiplier
    
    print(f"当前 Net Delta: {net_delta:.2f}, 对冲触发阈值: +/-{real_threshold:.2f}")

    if abs(net_delta) > real_threshold:
        # 计算需要交易的标的数量
        # 如果 Delta 为正 (Too Long),需要卖出标的 -> 数量为负
        # 如果 Delta 为负 (Too Short),需要买入标的 -> 数量为正
        hedge_qty = -1 * net_delta
        
        # 调整为 100 的整数倍 (A股/ETF 手数限制)
        hedge_qty = int(hedge_qty / 100) * 100
        
        if hedge_qty == 0:
            return

        print(f"执行对冲交易: 目标数量 {hedge_qty} 股")
        
        if hedge_qty > 0:
            # 买入标的 (23: 买入)
            passorder(23, 1101, ContextInfo.account_id, ContextInfo.underlying, 5, -1, abs(hedge_qty), ContextInfo)
        else:
            # 卖出标的 (24: 卖出)
            # 注意:如果是ETF,且没有持仓,可能需要融券卖出(28),这里假设是持有现货进行对冲或已有底仓
            # 简单的 Gamma Scalping 通常在 Delta 变负时买入,变正时卖出刚才买入的
            passorder(24, 1101, ContextInfo.account_id, ContextInfo.underlying, 5, -1, abs(hedge_qty), ContextInfo)

代码核心功能解析

  1. init 初始化

    • 设置了标的为 510050.SH
    • 定义了 hedge_threshold(对冲阈值),这是策略的关键参数,决定了对冲的频率。阈值越小,对冲越频繁,Gamma 捕获越精确,但手续费越高。
  2. open_straddle 建仓

    • 使用 get_option_list 获取期权链。
    • 通过比较 OptExercisePrice 和当前标的价格,自动筛选出最接近平值(ATM)的 Call 和 Put。
    • 使用 passorder 发送买入开仓指令。
  3. calculate_bsm_delta 希腊字母计算

    • 这是 QMT API 中缺失的部分。我们引入 scipy.stats.norm 来计算标准正态分布的累积分布函数,实现了 Black-Scholes 模型中的 Delta 公式。
    • $Delta_{Call} = N(d_1)$
    • $Delta_{Put} = N(d_1) - 1$
  4. calculate_portfolio_delta 组合 Delta 计算

    • 关键步骤:利用 ContextInfo.bsm_iv 反推实时的隐含波动率。因为 Delta 对波动率敏感,使用实时 IV 比使用历史波动率更准确。
    • 计算公式:$Total Delta = \sum (Option Delta \times Position \times Multiplier) + (Stock Position \times 1)$。
    • 注意期权乘数(通常是 10000),这意味着 1 手期权的 Delta 变动相当于 10000 股标的。
  5. check_and_hedge 对冲逻辑

    • Delta > 0:代表组合偏多头(通常是因为标的大涨,Call Delta 逼近 1,Put Delta 逼近 0)。此时需要卖出标的来减少 Delta。
    • Delta < 0:代表组合偏空头(通常是因为标的大跌,Put Delta 逼近 -1)。此时需要买入标的来增加 Delta。
    • 这种“跌买涨卖”的行为正是 Gamma Scalping 获利的本质。

常见问题与注意事项 (Q&A)

Q: 为什么代码中要计算 IV 而不是直接用固定波动率?
A: Delta 的值会随着波动率的变化而变化。使用 ContextInfo.bsm_iv 利用当前期权市场价格反推 IV,能得到更符合当前市场预期的 Delta 值,使对冲更精准。

Q: 如何处理卖出对冲时的券源问题?
A: 在 A 股市场,如果你没有标的底仓,当 Delta 变为正数需要卖出标的时,你必须进行融券卖出(OpType 28)。上述代码使用的是普通卖出(OpType 24),这假设你已经持有标的或者是在平掉之前买入的对冲仓位。如果是纯粹的 Gamma Scalping,建议使用信用账户(Margin Account)。

Q: 什么时候平仓?
A: 该策略代码主要展示了动态对冲逻辑。实际应用中,你需要在 handlebar 中加入止盈止损逻辑,或者在期权临近到期日(Gamma 风险剧增)时平仓。

Q: 为什么需要 scipy 库?
A: QMT 的 Python 环境自带了 numpyscipy。计算正态分布 CDF ($N(x)$) 是计算 BSM Delta 的必要步骤。

Q: 阈值 real_threshold 应该设为多少?
A: 这是一个权衡。如果设得太低(如 0),你会每一笔 Tick 都在交易,手续费会吞噬所有利润。如果设得太高,你的 Delta 敞口会很大,变成方向性投机。通常建议设置为期权总 Delta 的 10%-20% 左右的偏离度。