🚀 AI 一键生成 qmt 策略代码

QMT 平台如何实现期货主力合约的自动识别与移仓换月?

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

问题描述

请编写一个期货策略,可以自动跟踪主力合约,并在合约快到期时自动平仓并开仓到新的主力合约上。

解决方案

这是一个基于 QMT 平台的期货主力合约自动移仓换月策略。

策略逻辑说明

  1. 主力合约识别:使用 ContextInfo.get_main_contract() 实时获取指定品种(如沪深300股指期货 IF)的当前主力合约代码。
  2. 持仓监控:通过 get_trade_detail_data() 获取当前账户的持仓情况。
  3. 移仓条件
    • 条件 A(主力切换):当持仓的合约代码与当前系统判定的主力合约代码不一致时,触发移仓。
    • 条件 B(临近到期):作为双重保险,检查持仓合约的到期日。如果距离到期日小于设定阈值(如 2 天),强制平仓并开仓到新的主力合约(防止进入交割月风险)。
  4. 执行动作
    • 平掉旧合约的持仓(保持原有方向)。
    • 在新的主力合约上开出等量的仓位(保持原有方向)。

策略代码

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

def init(ContextInfo):
    # ================= 策略参数设置 =================
    # 设置资金账号 (请修改为您自己的实盘或模拟账号)
    ContextInfo.accID = '6000000000' 
    # 设置账号类型:'FUTURE' 为期货
    ContextInfo.accType = 'FUTURE'
    
    # 设置要跟踪的品种代码 (格式:品种.市场,例如 IF.IF 代表中金所IF品种)
    ContextInfo.target_product = 'IF.IF'
    
    # 临近到期强制移仓阈值 (天)
    # 如果持仓合约距离到期日小于此天数,强制移仓
    ContextInfo.expire_threshold = 2 
    
    # 绑定账号,用于接收交易回报
    ContextInfo.set_account(ContextInfo.accID)
    
    print("策略初始化完成,监控品种: {}, 账号: {}".format(ContextInfo.target_product, ContextInfo.accID))

def handlebar(ContextInfo):
    # 获取当前 K 线的时间,用于判断是否在交易时间
    # 如果是回测模式,barpos 代表回测进度;如果是实盘,代表最新行情
    if not ContextInfo.is_last_bar():
        return

    # 1. 获取当前市场上的主力合约代码
    current_main_contract = ContextInfo.get_main_contract(ContextInfo.target_product)
    if not current_main_contract:
        print("未获取到主力合约信息")
        return

    # 2. 获取当前账户的持仓信息
    positions = get_trade_detail_data(ContextInfo.accID, ContextInfo.accType, 'POSITION')
    
    # 3. 遍历持仓,检查是否需要移仓
    for pos in positions:
        # 过滤出属于目标品种的持仓 (例如只处理 IF 开头的合约)
        # pos.m_strInstrumentID 是具体的合约代码,如 IF2312.IF
        # ContextInfo.target_product 是 IF.IF
        product_code = ContextInfo.target_product.split('.')[0] # 获取 'IF'
        
        if not pos.m_strInstrumentID.startswith(product_code):
            continue
            
        # 获取持仓合约的到期日 (int格式,如 20231215)
        expire_date_int = ContextInfo.get_contract_expire_date(pos.m_strInstrumentID)
        
        # 获取当前日期 (int格式)
        current_timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
        current_date_str = timetag_to_datetime(current_timetag, '%Y%m%d')
        current_date_int = int(current_date_str)
        
        # 计算距离到期还有几天 (简单估算)
        days_to_expire = days_between(current_date_int, expire_date_int)
        
        # === 移仓逻辑判断 ===
        need_rollover = False
        reason = ""

        # 情况1: 持仓合约已经不是主力合约了
        if pos.m_strInstrumentID != current_main_contract:
            need_rollover = True
            reason = "主力合约已切换 (旧: {}, 新: {})".format(pos.m_strInstrumentID, current_main_contract)
        
        # 情况2: 持仓合约临近到期 (双重保险)
        elif days_to_expire <= ContextInfo.expire_threshold:
            need_rollover = True
            reason = "合约临近到期 (剩余 {} 天)".format(days_to_expire)

        # === 执行移仓 ===
        if need_rollover and pos.m_nVolume > 0:
            print("触发移仓: {}".format(reason))
            perform_rollover(ContextInfo, pos, current_main_contract)

def perform_rollover(ContextInfo, pos, new_contract):
    """
    执行移仓操作:平旧仓,开新仓
    """
    # 获取持仓方向和数量
    # m_nDirection: 48(多头), 49(空头) -> QMT常量
    # 为了方便,我们使用 passorder 的 opType 逻辑
    # 多头持仓(48) -> 需要卖平(opType=6 平多优先平今) -> 新开多(opType=0)
    # 空头持仓(49) -> 需要买平(opType=8 平空优先平今) -> 新开空(opType=3)
    
    vol = pos.m_nVolume
    old_contract = pos.m_strInstrumentID
    
    # 48 代表多头持仓 (Buy)
    if pos.m_nDirection == 48: 
        print("平多头旧合约: {}, 数量: {}".format(old_contract, vol))
        # 6: 平多,优先平今
        passorder(6, 1101, ContextInfo.accID, old_contract, 5, -1, vol, ContextInfo)
        
        print("开多头新合约: {}, 数量: {}".format(new_contract, vol))
        # 0: 开多
        passorder(0, 1101, ContextInfo.accID, new_contract, 5, -1, vol, ContextInfo)
        
    # 49 代表空头持仓 (Sell)
    elif pos.m_nDirection == 49:
        print("平空头旧合约: {}, 数量: {}".format(old_contract, vol))
        # 8: 平空,优先平今
        passorder(8, 1101, ContextInfo.accID, old_contract, 5, -1, vol, ContextInfo)
        
        print("开空头新合约: {}, 数量: {}".format(new_contract, vol))
        # 3: 开空
        passorder(3, 1101, ContextInfo.accID, new_contract, 5, -1, vol, ContextInfo)

def days_between(date1_int, date2_int):
    """
    计算两个日期整数之间的天数差
    """
    try:
        d1 = datetime.datetime.strptime(str(date1_int), '%Y%m%d')
        d2 = datetime.datetime.strptime(str(date2_int), '%Y%m%d')
        return (d2 - d1).days
    except:
        return 999 # 如果日期格式解析错误,返回一个大数值防止误触发

def timetag_to_datetime(timetag, format_str):
    """
    将时间戳转换为指定格式的字符串
    """
    import time
    return time.strftime(format_str, time.localtime(timetag / 1000))

代码关键点解析

  1. get_main_contract: 这是 QMT 提供的核心函数,它会根据交易所或数据商的规则(通常是持仓量或成交量最大)返回当前的主力合约代码(例如从 IF2311.IF 变为 IF2312.IF)。
  2. passorder 参数:
    • 1101: 代表单股/单合约、单账号、普通模式、按股/手下单。
    • 5: 代表使用最新价下单(prType)。
    • opType: 使用了 6 (平多) 和 8 (平空) 以及 0 (开多) 和 3 (开空),这是期货交易的标准操作码。
  3. is_last_bar(): 确保策略只在最新的行情切片上运行,避免在历史 K 线上重复触发交易信号(除非你在做回测)。
  4. days_between: 辅助函数,用于计算当前日期距离合约交割日还有多少天。

如何使用

  1. 修改账号:将 ContextInfo.accID 修改为您实际的资金账号。
  2. 选择品种:修改 ContextInfo.target_product 为您想跟踪的品种(如 rb.SF 螺纹钢, m.DF 豆粕等)。
  3. 运行模式
    • 在 QMT 界面中,将此策略加载到模型交易实盘交易中。
    • 设置运行周期(建议设置为 1分钟5分钟,甚至 Tick 级,以便及时捕捉主力切换)。
    • 点击运行。

Q&A

Q: 这个策略会自动开仓吗?
A: 不会。这个策略是一个移仓(Rollover)策略。它假设你手里已经持有某个合约的仓位。如果你的账户是空仓,它什么都不会做。你需要配合其他策略开仓,或者手动开仓后挂载此策略进行维护。

Q: 为什么使用“最新价”下单?
A: 代码中 passorder 的第 5 个参数设为 5 (最新价)。在实盘中,为了保证成交,通常建议使用 对手价 (14) 或 超价 (指定价模式下给予一定滑点)。如果需要更激进的成交,可以将参数 5 改为 14 (对手价)。

Q: 什么是 ContextInfo.expire_threshold
A: 这是一个安全阈值。有时候主力合约切换得比较晚,但旧合约马上就要交割了(个人投资者通常不能进入交割月)。设置这个参数(比如 2 天),意味着如果距离交割日只剩 2 天了,不管主力合约有没有切过来,都强制平掉旧合约并开到新主力上,防止违规持仓。