🚀 AI 一键生成 joinquant 策略代码

如何在 JoinQuant 中实现期权的 Delta 中性对冲策略?

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

问题描述

如何编写一个期权的Delta中性对冲策略?

解决方案

编写一个期权Delta中性(Delta Neutral)对冲策略,通常包含以下几个核心步骤:

  1. 策略逻辑:构建一个期权组合(例如卖出跨式组合 Short Straddle),同时持有标的资产(如50ETF),使得整体组合的Delta值为0。
  2. 期权选择:通常选择平值(ATM)或近月合约,流动性较好。
  3. Greeks计算:使用BSM(Black-Scholes-Merton)模型计算期权的Delta值。
  4. 动态对冲:随着标的价格变动和时间流逝,Delta会发生变化,需要定期(如每日收盘前)调整标的资产的持仓量,使总Delta回归为0。

以下是一个完整的JoinQuant策略代码示例。该策略以 50ETF (510050.XSHG) 为标的,构建 卖出跨式组合(Short Straddle) 并通过买卖50ETF现货进行 Delta中性对冲

策略代码

# -*- coding: utf-8 -*-
from jqdata import *
import math
from scipy.stats import norm
import datetime

def initialize(context):
    # 设定标的:50ETF
    g.underlying = '510050.XSHG'
    # 设定基准
    set_benchmark(g.underlying)
    # 开启动态复权
    set_option('use_real_price', True)
    # 过滤报错日志
    log.set_level('order', 'error')
    
    # 策略参数
    g.risk_free_rate = 0.03  # 无风险利率
    g.contract_multiplier = 10000  # 50ETF期权合约乘数通常为10000
    
    # 交易设置
    set_order_cost(OrderCost(close_tax=0, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # 每天收盘前进行对冲检查
    run_daily(delta_hedging, '14:50')

def delta_hedging(context):
    """
    核心逻辑:
    1. 如果没有持仓,构建卖出跨式组合(卖出平值Call + 卖出平值Put)。
    2. 计算当前期权持仓的总Delta。
    3. 计算使总Delta为0所需的标的(50ETF)数量。
    4. 调整标的持仓。
    """
    # 获取标的当前价格
    underlying_price = get_current_data()[g.underlying].last_price
    
    # 1. 仓位管理:如果当前没有期权持仓,则开仓
    if len(context.portfolio.short_positions) == 0:
        open_straddle(context, underlying_price)
    
    # 2. 检查期权到期:如果临近到期(例如小于3天),平仓并等待换月(简化处理,这里先平仓)
    check_expiry(context)

    # 3. 计算期权组合的总Delta
    total_option_delta = 0
    
    # 遍历所有空头持仓(我们是卖方)
    for security, position in context.portfolio.short_positions.items():
        # 跳过标的本身,只计算期权
        if security == g.underlying:
            continue
            
        # 获取期权信息
        info = get_security_info(security)
        if not info:
            continue
            
        # 计算剩余期限 T (年化)
        today = context.current_dt.date()
        maturity = info.end_date
        days_to_maturity = (maturity - today).days
        T = max(days_to_maturity / 365.0, 0.0001)
        
        # 获取行权价 K
        # 注意:聚宽API获取行权价可能需要解析名字或查询额外数据,这里简化假设可以通过API获取
        # 实际中建议使用 get_option_contracts 获取详细信息,这里为了通用性使用简易解析
        # 假设 symbol 格式能关联到行权价,或者我们重新获取一次合约信息
        # 在此示例中,我们重新计算Greeks需要行权价,我们假设开仓时记录了,或者通过名称解析
        # 为保证代码可运行,这里使用近似波动率计算Delta
        
        # 获取行权价 (需要根据实际合约代码逻辑获取,这里简化处理,假设我们能获取到)
        # 实际实盘中建议维护一个g.options_map来存储合约详情
        strike_price = get_strike_price(security) 
        
        # 计算历史波动率作为隐含波动率的替代 (简化处理)
        sigma = get_historical_volatility(g.underlying, 20)
        
        # 计算单份合约Delta
        opt_type = 'call' if '购' in info.display_name else 'put'
        d = calculate_bsm_delta(underlying_price, strike_price, T, g.risk_free_rate, sigma, opt_type)
        
        # 卖方Delta符号相反
        # 总Delta = 单份Delta * 合约乘数 * 持仓数量 * (-1 因为是卖方)
        pos_delta = d * g.contract_multiplier * position.total_amount * (-1)
        total_option_delta += pos_delta

    # 4. 计算对冲所需的标的头寸
    # 目标:Total_Delta = Option_Delta + Stock_Delta = 0
    # Stock_Delta = 1 * Stock_Shares
    # 所以:Target_Stock_Shares = -Option_Delta
    
    target_stock_amount = -total_option_delta
    
    # 取整到100股
    target_stock_amount = int(target_stock_amount / 100) * 100
    
    # 5. 执行对冲交易
    current_stock_amount = context.portfolio.positions[g.underlying].total_amount
    
    # 设置阈值,避免微小变动频繁交易(例如变动超过500股才调整)
    if abs(target_stock_amount - current_stock_amount) >= 500:
        log.info(f"执行对冲: 期权Delta: {total_option_delta:.2f}, 当前标的持仓: {current_stock_amount}, 目标标的持仓: {target_stock_amount}")
        order_target(g.underlying, target_stock_amount)

def open_straddle(context, current_price):
    """构建卖出跨式组合"""
    # 获取当月或次月合约
    # 这里简化逻辑:获取所有期权,筛选出50ETF期权,找到行权价最接近当前价格的合约
    
    # 获取所有期权列表 (注意:回测速度可能受影响,实盘需优化筛选逻辑)
    # 实际使用建议使用 get_option_contracts
    # 这里为了演示,我们模拟获取平值合约的过程
    
    target_date = context.current_dt.date()
    # 获取标的50ETF的期权合约
    # 注意:jqdata的get_all_securities包含options
    # 但更推荐使用 query 结合 opt.OPT_CONTRACT_INFO 表,这里使用简易逻辑
    
    # 假设我们选取下个月到期的合约以保证流动性和时间价值
    # 实际代码中需要根据日期筛选合约代码
    # 这里仅做逻辑演示,具体获取合约代码需根据JQData API文档的 get_option_contracts
    
    # 模拟:找到平值合约代码 (需用户根据实际数据环境完善)
    # 示例代码无法直接遍历所有期权,因此我们打印日志提示
    log.info("寻找平值期权合约进行开仓...")
    
    # --- 真实环境获取合约逻辑 ---
    # 1. 获取主力合约月份
    # 2. 获取该月份所有行权价
    # 3. 找到 abs(strike - current_price) 最小的 call 和 put
    # 4. order(call, -10); order(put, -10)
    # -------------------------
    
    # 由于无法在纯文本中动态获取准确的期权代码,此处留空或需替换为具体逻辑
    # 建议使用 jqdata.get_option_contracts(underlying, date)
    pass 

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

def get_historical_volatility(security, days):
    """计算历史波动率"""
    prices = attribute_history(security, days, '1d', ['close'])
    # 计算对数收益率
    log_returns = np.log(prices['close'] / prices['close'].shift(1))
    # 计算标准差并年化
    volatility = log_returns.std() * math.sqrt(252)
    return volatility

def get_strike_price(option_code):
    """
    解析期权行权价
    注意:这依赖于期权信息中的行权价字段,或者从名称中解析
    """
    # 尝试从API获取
    try:
        # 聚宽 get_security_info 可能不直接包含 strike_price,需查表
        # 这里为了演示,假设我们可以通过某种方式获取
        # 实际建议:使用 query(opt.OPT_CONTRACT_INFO).filter(...)
        return 3.0 # 这是一个占位符,实际需查询数据库
    except:
        return 3.0

def check_expiry(context):
    """检查到期日,临近到期平仓"""
    for security in list(context.portfolio.short_positions.keys()):
        if security == g.underlying:
            continue
        info = get_security_info(security)
        days = (info.end_date - context.current_dt.date()).days
        if days <= 2:
            log.info(f"合约 {security} 即将到期,平仓")
            order_target(security, 0)

策略实现的关键点解析

  1. Delta 计算 (BSM模型)

    • 代码中 calculate_bsm_delta 函数实现了Black-Scholes公式。
    • Delta 衡量期权价格对标的价格变动的敏感度。
    • Call Delta $\in (0, 1)$,Put Delta $\in (-1, 0)$。
    • 注意:代码中使用历史波动率(Historical Volatility)代替隐含波动率(Implied Volatility)。在更精细的策略中,你应该反推市场价格的隐含波动率来计算Delta,或者直接使用聚宽提供的Greeks数据(如果购买了相应数据权限)。
  2. 对冲逻辑

    • 组合Delta = $\sum (Option_Delta \times Multiplier \times Position) + (Stock_Delta \times Stock_Shares)$。
    • 因为标的资产(50ETF)的Delta恒为1,所以 $Stock_Delta = 1$。
    • 为了使组合Delta为0,我们需要持有的标的数量 $Stock_Shares = - \sum (Option_Delta \times Multiplier \times Position)$。
    • 如果计算结果为正,说明我们需要买入ETF;如果为负,说明需要卖出ETF(或融券卖出,本策略假设只做多ETF对冲,如果需要完全对冲可能需要融券权限)。
  3. 合约选择 (Open Straddle)

    • 代码中 open_straddle 部分留作了逻辑框架。在实际编写时,你需要使用 get_option_contracts 或查询 opt.OPT_CONTRACT_INFO 表来筛选出:
      • 标的为510050。
      • 到期日适中(如次月)。
      • 行权价最接近当前ETF价格(平值 ATM)。
  4. 交易成本与滑点

    • Delta中性策略属于高频调整策略,对交易成本非常敏感。代码中设置了 min_commission 和阈值(如变动超过500股才交易),以防止过度交易磨损本金。

如何完善此策略

要在聚宽回测环境中成功运行,你需要补充获取期权合约的具体代码。以下是一个获取平值合约的辅助函数示例(需配合 jqdata 库):

def get_atm_options(underlying, date):
    # 查询期权合约信息
    q = query(opt.OPT_CONTRACT_INFO).filter(
        opt.OPT_CONTRACT_INFO.underlying_symbol == underlying,
        opt.OPT_CONTRACT_INFO.maturity_date > date + datetime.timedelta(days=20) # 选次月
    ).order_by(opt.OPT_CONTRACT_INFO.maturity_date.asc())
    
    df = opt.run_query(q)
    if df.empty:
        return None, None
        
    # 找到最近的到期日
    target_maturity = df.iloc[0]['maturity_date']
    df_current = df[df['maturity_date'] == target_maturity]
    
    # 获取标的价格
    current_price = get_price(underlying, end_date=date, count=1, fields=['close'])['close'][0]
    
    # 寻找行权价最接近的
    df_current['diff'] = abs(df_current['exercise_price'] - current_price)
    df_current = df_current.sort_values('diff')
    
    # 分别找Call和Put
    atm_call = df_current[df_current['contract_type'] == 'CO'].iloc[0]['code']
    atm_put = df_current[df_current['contract_type'] == 'PO'].iloc[0]['code']
    
    return atm_call, atm_put

常见问题与Q&A

Q: 为什么需要Delta中性对冲?
A: 卖出跨式组合(Short Straddle)主要赚取时间价值(Theta)和波动率下降(Vega)的收益。但是,如果标的价格大幅向一个方向移动,卖方会面临巨大亏损。Delta对冲通过持有标的资产,抵消了标的价格小幅波动带来的方向性风险,让策略更纯粹地赚取Theta收益。

Q: 多久对冲一次比较好?
A: 这是一个权衡问题。实时对冲(每分钟)能让Delta保持极低,但交易成本极高。每日收盘对冲成本低,但日内风险敞口大。通常建议设定一个Delta阈值(例如Delta偏离超过0.2时)或固定时间(如每日收盘)进行对冲。

Q: 策略最大的风险是什么?
A: Gamma风险。当标的价格发生剧烈跳空(如黑天鹅事件)时,Delta会瞬间发生巨大变化,之前的对冲仓位会失效,且来不及调整,导致巨额亏损。此外,如果隐含波动率大幅上升(Vega风险),卖方也会亏损。