🚀 AI 一键生成 joinquant 策略代码

如何在 JoinQuant 中实现期权的牛市价差(Bull Call Spread)组合策略?

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

问题描述

请帮我编写一个期权牛市价差策略

解决方案

这是一个基于聚宽(JoinQuant)平台的**期权牛市价差(Bull Call Spread)**策略。

策略原理

牛市价差策略是指在看涨标的资产,但预期涨幅有限或希望降低权利金成本时使用的策略。

  • 操作:买入一份较低行权价($K_L$)的认购期权(Call),同时卖出一份较高行权价($K_H$)的认购期权(Call)。两份期权属于同一标的、同一到期日。
  • 收益:标的大涨时获利,最大收益锁定为 $(K_H - K_L) - \text{净权利金支出}$。
  • 风险:标的大跌时亏损,最大亏损锁定为初始支付的净权利金。

策略代码

以下代码以 50ETF (510050.XSHG) 为标的,构建牛市价差组合。

# -*- coding: utf-8 -*-
import jqdata
import math
import datetime
import re

def initialize(context):
    """
    初始化函数
    """
    # 设定基准
    set_benchmark('510050.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')
    
    # 全局变量设置
    g.underlying = '510050.XSHG' # 标的资产:50ETF
    g.trade_day_num = 0          # 记录交易天数
    
    # 策略参数
    g.spread_width = 1           # 价差宽度(档位),例如买入平值,卖出虚值1档
    g.close_days_before_expire = 2 # 距离到期日几天前平仓
    
    # 每天开盘时运行
    run_daily(trade_logic, '09:30')

def trade_logic(context):
    """
    每日交易主逻辑
    """
    # 1. 检查持仓,如果临近到期,则平仓
    check_and_close_positions(context)
    
    # 2. 如果没有持仓,则开仓
    if len(context.portfolio.positions) == 0:
        open_bull_spread(context)

def check_and_close_positions(context):
    """
    检查持仓并平仓
    """
    if len(context.portfolio.positions) == 0:
        return

    # 获取当前持仓的一个合约(假设都是同一月份到期)
    holding_code = list(context.portfolio.positions.keys())[0]
    info = get_security_info(holding_code)
    
    # 计算距离到期日的天数
    days_to_expire = (info.end_date - context.current_dt.date()).days
    
    # 如果距离到期日小于设定阈值,或者已经过了最后交易日,全平
    if days_to_expire <= g.close_days_before_expire:
        log.info("合约临近到期(剩余%d天),平仓所有头寸" % days_to_expire)
        for code in context.portfolio.positions.keys():
            order_target(code, 0)

def open_bull_spread(context):
    """
    开仓牛市价差组合
    """
    # 1. 获取标的当前价格
    underlying_price = get_current_data()[g.underlying].last_price
    
    # 2. 获取所有期权合约
    # 注意:这里获取的是当天的所有期权,需要筛选
    options = get_all_securities(types=['option'], date=context.current_dt.date())
    if options.empty:
        return
    
    # 3. 筛选出50ETF的认购期权(Call)
    # 命名规则通常包含标的名称,且认购包含"购"
    call_options = []
    for code, row in options.iterrows():
        # 简单筛选:代码对应50ETF,且是认购期权,且未退市
        # 注意:实际生产中应通过 underlying_symbol 属性筛选,这里简化通过名称判断
        if '50ETF' in row['display_name'] and '购' in row['display_name']:
            # 排除已经到期或即将到期的(比如当天就是到期日)
            if row['end_date'] > context.current_dt.date() + datetime.timedelta(days=g.close_days_before_expire + 5):
                call_options.append(code)
    
    if not call_options:
        log.info("未找到合适的认购期权合约")
        return

    # 4. 按到期日分组,选择最近的一个主力月份(通常是当月或次月)
    # 获取合约详细信息以得到到期日
    option_infos = [get_security_info(code) for code in call_options]
    
    # 找到最近的到期日
    sorted_dates = sorted(list(set([x.end_date for x in option_infos])))
    target_date = sorted_dates[0]
    
    # 筛选出该到期日的所有合约
    target_month_options = [x for x in option_infos if x.end_date == target_date]
    
    # 5. 解析行权价并排序
    # 聚宽API没有直接给出strike_price字段,通常需要从display_name解析
    # 格式如 "50ETF购8月3000",最后的数字即为行权价
    options_with_strike = []
    for info in target_month_options:
        strike = parse_strike_price(info.display_name)
        if strike:
            options_with_strike.append({'code': info.code, 'strike': strike, 'name': info.display_name})
    
    # 按行权价从小到大排序
    options_with_strike.sort(key=lambda x: x['strike'])
    
    if not options_with_strike:
        return

    # 6. 寻找平值合约(ATM)作为买入腿
    # 找到行权价最接近标的当前价格的合约
    atm_option = min(options_with_strike, key=lambda x: abs(x['strike'] - underlying_price))
    atm_index = options_with_strike.index(atm_option)
    
    # 确定买入合约(低行权价)
    buy_leg = atm_option
    
    # 确定卖出合约(高行权价)
    # 卖出虚值合约(行权价 > 标的价格),这里简单取 ATM 向上 g.spread_width 档
    sell_index = atm_index + g.spread_width
    
    if sell_index >= len(options_with_strike):
        log.info("无法找到足够高行权价的卖出合约")
        return
        
    sell_leg = options_with_strike[sell_index]
    
    # 再次确认是牛市价差:买低行权价,卖高行权价
    if buy_leg['strike'] >= sell_leg['strike']:
        log.info("行权价选择错误,未开仓")
        return

    log.info("标的价格: %.3f" % underlying_price)
    log.info("构建牛市价差: 买入 %s (行权价%.3f), 卖出 %s (行权价%.3f)" % 
             (buy_leg['name'], buy_leg['strike'], sell_leg['name'], sell_leg['strike']))
    
    # 7. 下单
    # 买入认购
    order(buy_leg['code'], 1)
    # 卖出认购
    order(sell_leg['code'], -1)

def parse_strike_price(display_name):
    """
    从期权名称中解析行权价
    例如: "50ETF购8月3000" -> 3.000
    """
    try:
        # 提取名称末尾的数字
        match = re.search(r'(\d+)$', display_name)
        if match:
            strike_str = match.group(1)
            # 50ETF期权通常行权价是放大了1000倍显示的,比如3000代表3.0
            # 但具体倍数可能随交易所规则变化,这里假设是标准格式
            # 也可以通过 get_price 获取价格反推,或者假设名称最后就是行权价
            # 聚宽数据中,50ETF期权名称通常是 "50ETF购X月2.100" 或者 "3000" 形式
            # 如果包含小数点
            if '.' in display_name:
                 match_float = re.search(r'(\d+\.\d+)$', display_name)
                 if match_float:
                     return float(match_float.group(1))
            
            # 如果是整数,通常需要除以1000 (针对50ETF期权旧命名规则)
            # 现在的命名规则很多直接带有小数点,或者直接是行权价
            # 为了稳健,我们这里做一个简单的处理:
            # 如果数字大于100,除以1000;否则直接用
            val = float(strike_str)
            if val > 100: 
                return val / 1000.0
            return val
    except:
        pass
    return None

策略要点说明

  1. 合约筛选 (get_all_securities)

    • 我们使用 types=['option'] 获取所有期权。
    • 通过名称过滤出 50ETF (Call)。
    • 通过 end_date 过滤掉即将到期的合约,避免刚开仓就面临交割。
  2. 行权价解析 (parse_strike_price)

    • 聚宽的基础数据 API 返回的对象中,行权价信息不直接暴露在 SecurityInfo 的属性中,通常包含在 display_name 里。
    • 代码中使用正则表达式提取名称末尾的数字作为行权价。针对 50ETF 期权,如果解析出的数字很大(如 3000),通常代表 3.000 元,代码中做了除以 1000 的处理。
  3. 构建组合

    • 买入腿 ($K_L$):选择平值期权(ATM),即行权价最接近当前标的价格的合约。
    • 卖出腿 ($K_H$):选择虚值期权(OTM),即行权价高于 ATM 的合约(代码中通过 g.spread_width 控制档位)。
  4. 风控与平仓

    • check_and_close_positions 函数中,每日检查距离到期日的天数。
    • 为了避免进入行权交割流程(这通常需要大量现金或现券),策略设定在到期前 2 天强制平仓。

Q&A

Q: 为什么使用 order(code, -1) 来卖出期权?
A: 在聚宽中,amount 为负数代表卖出。对于期权,如果当前持仓为 0,卖出即为卖空(Short Open),即成为义务方,收取权利金。

Q: 如何调整价差的宽度?
A: 修改 initialize 函数中的 g.spread_width。设置为 1 表示卖出比买入高 1 个档位的行权价;设置为 2 则价差更宽,最大潜在收益更高,但胜率可能降低。

Q: 为什么需要解析行权价?
A: 聚宽的标准 get_security_info 返回的对象主要包含上市退市时间等基础信息,不直接包含 Greeks 或行权价字段。虽然可以通过扩展数据获取,但解析名称是最通用且不依赖额外数据权限的方法。