🚀 AI 一键生成 qmt 策略代码

期权卖方策略:构建 Theta 中性组合的 Python 实现

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

问题描述

如何编写赚取期权时间价值的策略?

解决方案

编写赚取期权时间价值(Theta Decay)的策略,最经典的方法是卖出宽跨式组合(Short Strangle)卖出跨式组合(Short Straddle)

这类策略的核心逻辑是:做空期权。作为期权卖方,随着时间流逝,期权的时间价值会加速衰减,从而获利。为了控制风险,通常选择虚值(OTM)期权构建宽跨式组合,即:

  1. 卖出虚值看涨期权(Short OTM Call)
  2. 卖出虚值看跌期权(Short OTM Put)

以下是一个基于 QMT 平台的 Python 策略示例。该策略会自动选择当月合约,根据标的(如沪深300ETF)价格,选择虚值一定幅度的合约进行卖出开仓,并包含止损逻辑。

策略逻辑说明

  1. 标的510300.SH(沪深300ETF)。
  2. 开仓条件:当前无持仓时,获取标的最新价,计算目标行权价(例如标的价格 $\pm$ 2%)。
  3. 合约选择:筛选当月到期的期权合约,找到行权价最接近目标的 Call 和 Put。
  4. 交易执行:卖出开仓(Short Open)这两个合约。
  5. 风控/止损:如果单腿亏损超过设定阈值(如 50%)或临近到期日,则平仓离场。

QMT 策略代码

# -*- coding: gbk -*-
import time
import datetime

def init(ContextInfo):
    # ================= 策略参数设置 =================
    ContextInfo.account_id = '您的资金账号'  # 请替换为实际账号
    ContextInfo.account_type = 'STOCK_OPTION' # 账号类型:股票期权
    ContextInfo.underlying = '510300.SH'      # 标的:沪深300ETF
    ContextInfo.otm_pct = 0.02                # 虚值幅度,例如 0.02 代表 2%
    ContextInfo.trade_vol = 1                 # 每次交易张数
    ContextInfo.stop_loss_pct = 0.5           # 单腿止损比例 (50%)
    ContextInfo.close_days_before = 2         # 到期前多少天强制平仓
    
    # 设置账号
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 缓存变量
    ContextInfo.holdings = {'CALL': None, 'PUT': None} 
    print("策略初始化完成,准备赚取时间价值...")

def handlebar(ContextInfo):
    # 获取当前K线索引
    index = ContextInfo.barpos
    # 获取当前时间
    timetag = ContextInfo.get_bar_timetag(index)
    current_date_str = timetag_to_datetime(timetag, '%Y%m%d')
    
    # 1. 获取标的最新价格
    market_data = ContextInfo.get_market_data_ex(
        ['close'], [ContextInfo.underlying], period='1d', count=1, subscribe=True
    )
    if ContextInfo.underlying not in market_data:
        return
    
    underlying_price = market_data[ContextInfo.underlying].iloc[-1]['close']
    
    # 2. 检查持仓与止损
    check_positions_and_stop_loss(ContextInfo, underlying_price, current_date_str)
    
    # 3. 如果没有持仓,且不在临近到期日,则开仓
    if ContextInfo.holdings['CALL'] is None and ContextInfo.holdings['PUT'] is None:
        # 获取当月合约到期日
        expire_date = get_current_month_expire_date(ContextInfo, current_date_str)
        if not expire_date:
            return
            
        # 检查是否临近到期(避险)
        days_to_expire = days_between(current_date_str, str(expire_date))
        if days_to_expire <= ContextInfo.close_days_before:
            print(f"临近到期日 {expire_date},暂停开新仓")
            return

        open_short_strangle(ContextInfo, underlying_price, str(expire_date))

def open_short_strangle(ContextInfo, current_price, expire_date):
    """
    构建卖出宽跨式组合
    """
    # 计算目标行权价
    target_call_strike = current_price * (1 + ContextInfo.otm_pct)
    target_put_strike = current_price * (1 - ContextInfo.otm_pct)
    
    # 获取期权列表
    # get_option_list(标的, 到期日, 类型)
    # 注意:get_option_list 的到期日参数如果是YYYYMMDD格式,isavailable=True返回可交易的
    call_list = ContextInfo.get_option_list(ContextInfo.underlying, expire_date, "CALL", True)
    put_list = ContextInfo.get_option_list(ContextInfo.underlying, expire_date, "PUT", True)
    
    # 筛选最接近目标行权价的合约
    target_call = find_closest_contract(ContextInfo, call_list, target_call_strike)
    target_put = find_closest_contract(ContextInfo, put_list, target_put_strike)
    
    if target_call and target_put:
        print(f"标的价格: {current_price:.3f}")
        print(f"目标Call行权价: {target_call_strike:.3f}, 选中合约: {target_call}")
        print(f"目标Put行权价: {target_put_strike:.3f}, 选中合约: {target_put}")
        
        # 下单:卖出开仓 (opType=52)
        # 卖出 Call
        passorder(52, 1101, ContextInfo.account_id, target_call, 5, -1, ContextInfo.trade_vol, ContextInfo)
        ContextInfo.holdings['CALL'] = target_call
        
        # 卖出 Put
        passorder(52, 1101, ContextInfo.account_id, target_put, 5, -1, ContextInfo.trade_vol, ContextInfo)
        ContextInfo.holdings['PUT'] = target_put
        
        print("卖出宽跨式组合指令已发送")

def check_positions_and_stop_loss(ContextInfo, underlying_price, current_date_str):
    """
    检查持仓,执行止损或到期平仓
    """
    # 获取账户持仓信息
    positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
    
    # 更新本地持仓状态(防止重启后丢失)
    has_call = False
    has_put = False
    
    for pos in positions:
        instrument = pos.m_strInstrumentID
        
        # 检查是否需要到期平仓
        detail = ContextInfo.get_instrumentdetail(instrument)
        expire_date = detail.get('ExpireDate')
        if expire_date:
            days = days_between(current_date_str, str(expire_date))
            if days <= ContextInfo.close_days_before:
                print(f"合约 {instrument} 临近到期(剩{days}天),强制平仓")
                close_position(ContextInfo, instrument, pos.m_nVolume)
                continue

        # 检查止损 (简单逻辑:如果期权价格上涨过多,代表亏损)
        # 注意:卖方是价格上涨亏损。这里简化使用标的价格位移判断,实际应获取期权持仓盈亏
        # 获取期权最新价
        opt_data = ContextInfo.get_market_data_ex(['close'], [instrument], period='1d', count=1)
        if instrument in opt_data:
            current_opt_price = opt_data[instrument].iloc[-1]['close']
            avg_cost = pos.m_dOpenPrice
            
            # 卖方亏损计算:(现价 - 成本) / 成本 > 阈值
            if avg_cost > 0:
                loss_pct = (current_opt_price - avg_cost) / avg_cost
                if loss_pct > ContextInfo.stop_loss_pct:
                    print(f"触发止损: {instrument}, 成本:{avg_cost}, 现价:{current_opt_price}, 亏损率:{loss_pct:.2%}")
                    close_position(ContextInfo, instrument, pos.m_nVolume)
                    continue

        # 更新状态
        if instrument == ContextInfo.holdings['CALL']: has_call = True
        if instrument == ContextInfo.holdings['PUT']: has_put = True

    # 如果持仓被平掉,更新内存状态
    if not has_call: ContextInfo.holdings['CALL'] = None
    if not has_put: ContextInfo.holdings['PUT'] = None

def close_position(ContextInfo, code, volume):
    """
    买入平仓
    """
    # opType=53: 买入平仓
    passorder(53, 1101, ContextInfo.account_id, code, 5, -1, volume, ContextInfo)
    print(f"平仓指令已发送: {code}, 数量: {volume}")

def find_closest_contract(ContextInfo, contract_list, target_strike):
    """
    在合约列表中寻找行权价最接近目标的合约
    """
    closest_contract = None
    min_diff = float('inf')
    
    for code in contract_list:
        # 获取合约详细信息
        detail = ContextInfo.get_option_detail_data(code)
        strike_price = detail.get('OptExercisePrice')
        
        if strike_price:
            diff = abs(strike_price - target_strike)
            if diff < min_diff:
                min_diff = diff
                closest_contract = code
                
    return closest_contract

def get_current_month_expire_date(ContextInfo, current_date_str):
    """
    获取当月或次月期权的到期日
    """
    # 这里简化处理:获取标的对应的所有期权合约,解析出最近的一个到期日
    # 实际应用中可以使用 get_option_list 传入 YYYYMM 格式获取
    
    # 获取当前年月
    year = int(current_date_str[0:4])
    month = int(current_date_str[4:6])
    
    # 尝试获取当月合约
    month_str = f"{year}{month:02d}"
    # 随便取一个类型获取列表,主要是为了拿到日期
    # 注意:QMT API 中 get_option_list 第二个参数如果是月份,返回该月到期的合约
    # 但我们需要具体的到期日YYYYMMDD
    
    # 这种方式比较间接,更直接的是遍历几个合约看ExpireDate
    # 为了演示方便,我们取标的的一个合约列表,找出大于当前日期的最小到期日
    
    # 获取当前可交易的所有Call
    all_calls = ContextInfo.get_option_list(ContextInfo.underlying, "", "CALL", True)
    
    valid_dates = set()
    current_date_int = int(current_date_str)
    
    for code in all_calls[:50]: # 抽样检查,避免遍历太多
        detail = ContextInfo.get_instrumentdetail(code)
        edate = detail.get('ExpireDate')
        if edate and edate > current_date_int:
            valid_dates.add(edate)
            
    if not valid_dates:
        return None
        
    # 返回最近的到期日
    return min(valid_dates)

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

代码关键点解析

  1. passorder 交易类型

    • 期权交易与股票不同,必须指定具体的开平仓类型。
    • 52:卖出开仓(Short Open),这是赚取时间价值的关键动作。
    • 53:买入平仓(Buy Close),用于止损或获利了结。
    • 1101:代表单股/单张、限价/市价委托(配合价格类型使用)。这里使用了 5 (最新价) 和 -1 (价格无效,配合最新价使用)。
  2. 合约选择 (get_option_list & get_option_detail_data)

    • get_option_list 用于获取某标的下的期权合约列表。
    • get_option_detail_data (或 get_instrumentdetail) 用于获取合约的具体属性,最重要的是 行权价 (OptExercisePrice)到期日 (ExpireDate)
    • 策略通过计算 current_price * (1 ± otm_pct) 来定位虚值合约,这样可以获得权利金的同时保留一定的安全边际。
  3. 风险控制

    • 临近到期平仓:期权在到期日附近 Gamma 风险巨大(标的小幅波动会导致 Delta 剧烈变化),且面临行权指派风险。策略设置了 close_days_before,在到期前 2 天强制平仓。
    • 价格止损:作为卖方,收益有限(权利金),风险无限。代码中简单计算了期权价格涨幅,如果涨幅超过 50%(意味着亏损),则强制平仓。
  4. 数据获取

    • 使用 get_market_data_ex 获取标的 ETF 的价格,这是判断 ATM(平值)位置的基础。

风险提示

  • 保证金风险:卖出期权需要缴纳保证金,如果标的向不利方向大幅波动,可能面临追加保证金或强平风险。
  • 黑天鹅风险:宽跨式策略在市场横盘时表现最好,但如果市场出现单边暴涨或暴跌,卖方会面临巨大亏损。实盘中建议配合 Delta 对冲 或购买更深虚值的期权进行保护(即构建铁以此鹰式组合 Iron Condor)。

如何运行

  1. 在 QMT 策略编辑器中新建一个 Python 策略。
  2. 将上述代码复制进去。
  3. 修改 init 中的 account_id 为您的真实期权账号。
  4. 确保已下载 510300.SH 及相关期权的历史数据(如果是回测)或确保行情连接正常(如果是实盘)。
  5. 点击运行。