🚀 AI 一键生成 qmt 策略代码

QMT 期权实战:如何编写持有正股并自动卖出认购期权的备兑策略?

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

问题描述

QMT 怎么写备兑开仓的自动化策略?

解决方案

在 QMT 中编写备兑开仓(Covered Call)自动化策略,核心逻辑是:持有标的证券(如 ETF 或股票) + 卖出相应数量的认购期权(Call)

备兑开仓在 QMT 的交易接口中有专门的操作类型。与普通卖出开仓(保证金交易)不同,备兑开仓使用持有的现货作为担保,不需要占用现金保证金。

以下是详细的策略设计与代码实现。

1. 策略逻辑设计

  1. 初始化 (init):
    • 设置标的证券(例如 510050.SH)。
    • 设置期权账号。
    • 设定备兑参数(例如:选择下个月到期、行权价高于当前价一档的虚值合约)。
  2. 行情与持仓检查 (handlebar):
    • 获取标的证券的持仓数量。
    • 获取当前标的价格。
    • 检查是否已经持有期权空头持仓。
  3. 合约选择:
    • 根据标的价格和到期时间,筛选出合适的认购期权合约(Call)。
  4. 交易执行:
    • 如果持有标的且未持有期权:执行 备兑开仓 (opType=54)。
    • 如果期权临近到期:执行 备兑平仓 (opType=55) 或等待行权/归零。

2. 关键 API 说明

  • get_option_list: 获取期权合约列表。
  • get_instrumentdetail: 获取合约详情(行权价、到期日等)。
  • get_trade_detail_data: 获取账户持仓,判断是否有标的和期权持仓。
  • passorder: 下单函数。
    • opType=54: 备兑开仓 (Covered Open)。
    • opType=55: 备兑平仓 (Covered Close)。
    • opType=58: 证券锁定 (Lock Underlying)。部分券商要求先锁定现货才能备兑开仓,但大多数柜台支持直接发 54 指令自动锁定。

3. 策略代码实现

以下是一个完整的 Python 策略示例。该策略会检查账户中的 510050.SH 持仓,如果持有现货且没有期权持仓,则自动卖出下月到期的虚值一档认购期权。

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

def init(ContextInfo):
    # 1. 设置基本参数
    ContextInfo.account_id = '您的期权资金账号'  # 请修改为实际账号
    ContextInfo.account_type = 'STOCK_OPTION' # 账号类型:股票期权
    ContextInfo.underlying = '510050.SH'      # 标的ETF
    ContextInfo.target_month_offset = 0       # 0表示当月,1表示下月
    ContextInfo.strike_offset = 1             # 行权价偏离档位:1表示虚值一档(行权价 > 标的价)
    
    # 设置账号
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 设定定时运行,例如每天上午10:00检查一次
    # 实际盘中可以使用 handlebar 驱动,这里为了演示逻辑清晰使用定时器或在handlebar中控制频率
    print("备兑策略初始化完成")

def get_target_contract(ContextInfo):
    """
    根据规则筛选期权合约
    """
    # 1. 获取标的最新价
    last_price_data = ContextInfo.get_market_data_ex(
        ['close'], [ContextInfo.underlying], period='1d', count=1, subscribe=False
    )
    if ContextInfo.underlying not in last_price_data:
        print("无法获取标的行情")
        return None
    
    underlying_price = last_price_data[ContextInfo.underlying].iloc[-1]['close']
    print(f"标的 {ContextInfo.underlying} 当前价格: {underlying_price}")

    # 2. 确定目标到期月份 (格式 YYYYMM)
    # 这里简单处理:获取当前日期,计算目标月份
    today = datetime.date.today()
    # 简单的月份推算逻辑,实际需考虑行权日
    target_date = today + datetime.timedelta(days=30 * ContextInfo.target_month_offset) 
    target_month_str = target_date.strftime('%Y%m')
    
    # 3. 获取期权列表 (Call)
    # get_option_list(标的, 月份, 类型)
    option_list = ContextInfo.get_option_list(ContextInfo.underlying, target_month_str, "CALL")
    
    if not option_list:
        print(f"未找到 {target_month_str} 的认购期权")
        return None

    # 4. 筛选行权价
    # 获取合约详细信息,提取行权价
    contracts_info = []
    for opt_code in option_list:
        detail = ContextInfo.get_instrumentdetail(opt_code)
        if detail:
            contracts_info.append({
                'code': opt_code,
                'strike_price': detail['OptExercisePrice']
            })
    
    # 按行权价排序
    contracts_info.sort(key=lambda x: x['strike_price'])
    
    # 寻找刚好大于标的价的合约(虚值)
    target_contract = None
    
    # 找到第一个行权价 > 标的价的位置
    atm_index = -1
    for i, info in enumerate(contracts_info):
        if info['strike_price'] > underlying_price:
            atm_index = i
            break
    
    if atm_index != -1:
        # 根据偏离档位选择
        target_index = atm_index + (ContextInfo.strike_offset - 1)
        if target_index < len(contracts_info):
            target_contract = contracts_info[target_index]['code']
            print(f"选中合约: {target_contract}, 行权价: {contracts_info[target_index]['strike_price']}")
    
    return target_contract

def handlebar(ContextInfo):
    # 限制运行频率,避免每个Tick都触发,这里仅在K线结束或特定时间运行
    if not ContextInfo.is_last_bar():
        return

    # 1. 获取账户持仓信息
    positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
    
    # 统计标的持仓和期权持仓
    underlying_vol = 0
    has_short_call = False
    short_call_code = ""
    
    for pos in positions:
        # 检查标的持仓
        if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == ContextInfo.underlying:
            underlying_vol = pos.m_nVolume
        
        # 检查是否已有期权义务仓(卖出持仓)
        # m_nDirection: 49 代表卖出(空头)
        # 也可以通过 m_strInstrumentID 判断是否是期权
        if ".SHO" in (pos.m_strInstrumentID + '.' + pos.m_strExchangeID) or ".SZO" in (pos.m_strInstrumentID + '.' + pos.m_strExchangeID):
             # 简单判断:如果有期权持仓且是空头
             if pos.m_nVolume > 0 and pos.m_nDirection == 49: 
                 has_short_call = True
                 short_call_code = pos.m_strInstrumentID + '.' + pos.m_strExchangeID

    print(f"当前标的持仓: {underlying_vol}, 是否持有期权空头: {has_short_call}")

    # 2. 策略逻辑:有标的 + 无期权 -> 开仓
    # 假设一张期权对应 10000 股标的 (ETF通常是10000)
    contract_unit = 10000 
    can_open_lots = int(underlying_vol / contract_unit)

    if can_open_lots > 0 and not has_short_call:
        print(f"满足开仓条件,可开 {can_open_lots} 张备兑")
        
        # 获取目标合约
        target_code = get_target_contract(ContextInfo)
        
        if target_code:
            # 执行备兑开仓
            # opType=54 (备兑开仓)
            # orderType=1101 (单股单账号普通下单)
            # prType=14 (对手价)
            # price=0 (对手价模式下价格无效)
            # volume=1 (张数,这里演示开1张,实际可开 can_open_lots)
            
            print(f"正在对 {target_code} 执行备兑开仓...")
            passorder(
                54,                  # opType: 备兑开仓
                1101,                # orderType
                ContextInfo.account_id,
                target_code,
                14,                  # prType: 对手价
                0,                   # price
                1,                   # volume: 开1张
                ContextInfo
            )
    elif has_short_call:
        print(f"已有期权持仓 {short_call_code},跳过开仓")
        # 这里可以添加平仓逻辑:例如临近到期,或者获利达到预期进行平仓
        # 平仓使用 passorder(55, ...) 备兑平仓
    else:
        print("标的持仓不足,无法进行备兑开仓")

4. 关键点与注意事项

  1. 备兑证券锁定 (Locking):

    • 在 QMT 的 passorder 中,opType=54 (备兑开仓) 通常会自动处理证券锁定。
    • 如果您的券商柜台不支持自动锁定,您可能需要先发送 opType=58 (证券锁定) 指令,将持有的 ETF/股票 锁定为备兑证券,然后再发送 opType=52 (卖出开仓)。
    • 推荐方式:直接使用 54,如果报错提示“备兑证券不足”,则说明需要先手动或代码调用 58 进行锁定。
  2. 合约单位:

    • ETF 期权通常一张合约对应 10,000 份 ETF。
    • 个股期权(深交所)一张合约通常对应 100 股或其他调整后的数量。
    • 代码中 contract_unit = 10000 是针对 50ETF/300ETF 的默认值,建议通过 get_instrumentdetail 获取 VolumeMultiple 属性来动态确定。
  3. 交易方向:

    • 备兑策略本质是 卖出 期权。在 get_trade_detail_data 返回的 POSITION 对象中,m_nDirection49 (ENTRUST_SELL) 表示空头持仓(义务仓)。
  4. 风险控制:

    • 备兑策略虽然风险较低(有现货保护),但如果标的大涨,会限制收益(收益封顶)。
    • 如果标的大跌,现货端会亏损,虽然期权端有权利金收入作为缓冲,但整体仍可能亏损。
  5. 实盘配置:

    • 在 QMT 界面中,确保“交易设置”里的期权账号已登录。
    • 确保 Python 库路径设置正确,且已下载必要的历史数据(如果使用了回测功能)。

Q&A

Q: 如何进行备兑平仓?
A: 将 passorder 的第一个参数 opType 改为 55

Q: 如何查询合约的行权价?
A: 使用 ContextInfo.get_instrumentdetail(code),返回的字典中 OptExercisePrice 字段即为行权价。

Q: 为什么下单失败提示“可用不足”?
A: 备兑开仓不占用资金,但占用现货。请检查您的普通账户或信用账户中是否有足额的标的证券,并且该证券是否已经划转到了期权账户的“备兑证券”科目下(如果券商不支持自动划转)。