🚀 AI 一键生成 qmt 策略代码

如何构建一个日历价差策略来赚取时间价值的衰减?

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

问题描述

如何用代码实现一个日历价差组合:卖出一个下个月到期的平值看涨期权,同时买入一个下下个月到期的平值看涨期权?

解决方案

这是一个关于量化交易策略实现的问题。

策略逻辑分析

日历价差(Calendar Spread) 是一种利用不同到期月份期权的时间价值衰减差异来获利的策略。
具体到你的需求:卖出近月平值看涨期权 + 买入远月平值看涨期权

  • 标的:假设为 50ETF(510050.SH)或 300ETF(510300.SH)。
  • 腿1(空头):卖出开仓(Sell Open),下个月到期(近月),行权价最接近当前标的价格(平值),类型为看涨(Call)。
  • 腿2(多头):买入开仓(Buy Open),下下个月到期(远月),行权价最接近当前标的价格(平值),类型为看涨(Call)。
  • 核心难点:如何在代码中自动识别“下个月”和“下下个月”的到期日,并筛选出平值合约。

QMT 代码实现

以下是完整的 Python 策略代码。代码包含了合约筛选逻辑、平值识别逻辑以及下单交易逻辑。

# -*- coding: gbk -*-
import pandas as pd
import time
import datetime

def init(ContextInfo):
    # 1. 设置账户 (请替换为真实的资金账号)
    # 格式:'账号ID'
    ContextInfo.accID = 'YOUR_ACCOUNT_ID' 
    ContextInfo.set_account(ContextInfo.accID)
    
    # 2. 设置标的物,这里以 50ETF 为例
    ContextInfo.underlying = '510050.SH'
    
    # 3. 设置交易数量 (张)
    ContextInfo.trade_vol = 1
    
    # 4. 策略运行控制变量,防止重复下单
    ContextInfo.has_traded = False
    
    print("策略初始化完成,标的:{}".format(ContextInfo.underlying))

def handlebar(ContextInfo):
    # 仅在最后一根K线(实时行情)运行,或者是回测模式
    if not ContextInfo.is_last_bar():
        return

    # 每日只交易一次的简单控制逻辑
    if ContextInfo.has_traded:
        return

    # 1. 获取标的当前价格
    # 获取最新的一条tick数据
    tick_data = ContextInfo.get_full_tick([ContextInfo.underlying])
    if not tick_data:
        print("未获取到标的行情")
        return
        
    current_price = tick_data[ContextInfo.underlying]['lastPrice']
    print("当前标的价格: {}".format(current_price))

    # 2. 获取所有看涨期权合约列表
    # get_option_list(标的代码, 到期月, 类型),到期月为空表示取所有,类型为空表示取所有
    # 这里我们先取所有,然后自己筛选日期
    option_list = ContextInfo.get_option_list(ContextInfo.underlying, '', 'CALL', True)
    
    if not option_list:
        print("未获取到期权合约列表")
        return

    # 3. 筛选到期日
    # 我们需要找到“最近的两个到期日”来代表“下个月”和“下下个月”
    # 注意:实际交易中“下个月”通常指代最近的两个主力合约月份
    
    # 获取当前日期 YYYYMMDD
    current_date_str = datetime.datetime.now().strftime('%Y%m%d')
    
    # 存储结构:{到期日: [合约代码列表]}
    expiry_map = {}
    
    for opt_code in option_list:
        # 获取合约详细信息
        detail = ContextInfo.get_instrumentdetail(opt_code)
        expire_date = str(detail['ExpireDate'])
        
        # 过滤掉已经过期的合约
        if expire_date > current_date_str:
            if expire_date not in expiry_map:
                expiry_map[expire_date] = []
            expiry_map[expire_date].append(opt_code)
    
    # 对到期日进行排序
    sorted_expiry_dates = sorted(expiry_map.keys())
    
    if len(sorted_expiry_dates) < 2:
        print("可交易的期权到期月份不足两个,无法构建日历价差")
        return
        
    # 确定近月和远月到期日
    near_term_date = sorted_expiry_dates[0] # 近月(下个月)
    far_term_date = sorted_expiry_dates[1]  # 远月(下下个月)
    
    print("近月到期日: {}, 远月到期日: {}".format(near_term_date, far_term_date))

    # 4. 筛选平值合约 (ATM)
    # 分别在近月和远月合约列表中,寻找行权价最接近 current_price 的合约
    
    near_atm_contract = get_atm_contract(ContextInfo, expiry_map[near_term_date], current_price)
    far_atm_contract = get_atm_contract(ContextInfo, expiry_map[far_term_date], current_price)
    
    if not near_atm_contract or not far_atm_contract:
        print("未找到合适的平值合约")
        return
        
    print("选定近月合约(卖出): {}, 行权价: {}".format(near_atm_contract['code'], near_atm_contract['strike']))
    print("选定远月合约(买入): {}, 行权价: {}".format(far_atm_contract['code'], far_atm_contract['strike']))

    # 5. 执行交易
    # 腿1:卖出近月平值看涨期权 (卖出开仓)
    # opType: 52 (卖出开仓), orderType: 1101 (限价单), prType: 14 (对手价)
    passorder(52, 1101, ContextInfo.accID, near_atm_contract['code'], 14, -1, ContextInfo.trade_vol, ContextInfo)
    print("交易发出:卖出开仓 {}".format(near_atm_contract['code']))
    
    # 腿2:买入远月平值看涨期权 (买入开仓)
    # opType: 50 (买入开仓)
    passorder(50, 1101, ContextInfo.accID, far_atm_contract['code'], 14, -1, ContextInfo.trade_vol, ContextInfo)
    print("交易发出:买入开仓 {}".format(far_atm_contract['code']))

    # 标记已交易
    ContextInfo.has_traded = True

def get_atm_contract(ContextInfo, contract_list, current_price):
    """
    辅助函数:从合约列表中寻找行权价最接近标的价格的合约
    """
    best_contract = None
    min_diff = 999999.0
    
    for code in contract_list:
        detail = ContextInfo.get_instrumentdetail(code)
        strike_price = detail['OptExercisePrice']
        
        diff = abs(strike_price - current_price)
        
        if diff < min_diff:
            min_diff = diff
            best_contract = {
                'code': code,
                'strike': strike_price
            }
            
    return best_contract

代码关键点解析

  1. 合约筛选逻辑 (get_option_list & get_instrumentdetail):

    • 首先通过 get_option_list 获取标的下所有的 CALL(认购)合约。
    • 然后遍历合约,通过 get_instrumentdetail 获取 ExpireDate(到期日)。
    • 将合约按到期日分组,并按时间排序。
    • sorted_expiry_dates[0] 作为近月(通常是当月或下月,取决于当前是否临近行权日),取 sorted_expiry_dates[1] 作为远月。
  2. 平值识别 (get_atm_contract):

    • 平值(ATM)定义为行权价(Strike Price)与标的现价(Underlying Price)差值绝对值最小的合约。
    • 代码中遍历特定月份的所有合约,计算 abs(strike - current_price),取最小值对应的合约。
  3. 交易下单 (passorder):

    • 卖出近月:使用 opType=52 (卖出开仓)。这是日历价差的获利腿,利用近月时间价值衰减快。
    • 买入远月:使用 opType=50 (买入开仓)。这是保护腿,利用远月时间价值衰减慢。
    • 价格类型:示例中使用了 prType=14 (对手价) 以提高成交概率,实盘中也可以考虑使用 5 (最新价) 或 11 (指定价)。
  4. 注意事项:

    • 账号设置:请务必在 init 函数中将 'YOUR_ACCOUNT_ID' 替换为您实际的 QMT 资金账号。
    • 风控:期权卖方风险较大(虽然日历价差是风险有限策略,但仍需保证金),请确保账户有足够资金。
    • 运行频率:示例代码加了 ContextInfo.has_traded 锁,确保策略启动后只运行一次。如果是实盘持续运行,需要根据您的逻辑修改触发条件(例如:每天定时触发、价差达到特定阈值触发等)。

Q&A

Q: 如何修改代码以交易看跌期权(Put)的日历价差?
A: 在 handlebar 函数中,将 ContextInfo.get_option_list 的第三个参数从 'CALL' 改为 'PUT' 即可。

Q: 为什么使用 get_instrumentdetail 而不是直接解析合约代码?
A: 虽然合约代码(如 1000xxxx.SHO)有时包含信息,但直接调用 API 获取 ExpireDateOptExercisePrice(行权价)是最准确且兼容性最好的方法,不受交易所编码规则变更的影响。

Q: 如何在回测中查看成交情况?
A: 可以在 QMT 界面点击“回测”,系统会自动撮合 passorder 发出的委托。您也可以在代码中添加 get_trade_detail_data 函数来实时查询持仓和成交状态。