🚀 AI 一键生成 qmt 策略代码

QMT 期货交易:如何编写跨期套利(日历价差)策略?

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

问题描述

如何编写一个期货跨期套利策略,在两个不同到期日的合约价差扩大时建仓,价差缩小时平仓?

解决方案

这是一个基于 QMT 平台的期货跨期套利策略示例。

策略逻辑分析

跨期套利(Calendar Spread Arbitrage)的核心在于利用同一品种、不同到期月份合约之间的价差波动获利。

本策略的逻辑如下:

  1. 定义价差价差 = 近月合约价格 - 远月合约价格
  2. 建仓(价差扩大):当 价差 > 开仓阈值 时,认为近期合约过贵或远期合约过便宜。此时卖出近月合约(做空),买入远月合约(做多),即“卖出套利”。
  3. 平仓(价差缩小):当 价差 < 平仓阈值 时,认为价差回归正常。此时买入平仓近月合约,卖出平仓远月合约

策略代码实现

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

def init(ContextInfo):
    # ================= 策略参数设置 =================
    # 资金账号 (请修改为您的实际期货账号)
    ContextInfo.accID = 'YOUR_ACCOUNT_ID'
    
    # 套利合约对 (示例:沪深300股指期货)
    # 合约A:近月合约 (通常价格较高或波动较敏感)
    ContextInfo.contract_A = 'IF2406.IF' 
    # 合约B:远月合约
    ContextInfo.contract_B = 'IF2409.IF'
    
    # 交易手数
    ContextInfo.lots = 1
    
    # 价差阈值设置 (Spread = Price_A - Price_B)
    # 当价差大于 30 点时,认为价差过大,进行卖出套利(空A多B)
    ContextInfo.spread_open_threshold = 30 
    # 当价差回归到 10 点时,获利平仓
    ContextInfo.spread_close_threshold = 10
    
    # ================= 初始化设置 =================
    print("策略初始化启动...")
    # 绑定账号
    ContextInfo.set_account(ContextInfo.accID)
    # 设置股票池/合约池,确保能获取到行情
    ContextInfo.set_universe([ContextInfo.contract_A, ContextInfo.contract_B])
    
    # 记录当前持仓状态:0-空仓, 1-持有套利仓位
    ContextInfo.holding_state = 0 

def handlebar(ContextInfo):
    # 仅在最后一根K线(实时行情)运行,避免回测时重复发单或历史K线重复计算
    if not ContextInfo.is_last_bar():
        return

    # 获取最新行情数据 (Tick级或分时)
    # 使用 get_market_data_ex 获取最新的一条数据
    market_data = ContextInfo.get_market_data_ex(
        ['close'], 
        [ContextInfo.contract_A, ContextInfo.contract_B], 
        period='tick', 
        count=1,
        subscribe=True
    )
    
    # 数据有效性检查
    if ContextInfo.contract_A not in market_data or ContextInfo.contract_B not in market_data:
        return
        
    # 提取最新价格
    try:
        price_A = market_data[ContextInfo.contract_A].iloc[-1]['close']
        price_B = market_data[ContextInfo.contract_B].iloc[-1]['close']
    except:
        # 防止数据未准备好报错
        return

    # 计算当前价差
    current_spread = price_A - price_B
    
    # 在界面上画出价差曲线 (可选)
    ContextInfo.paint('Spread', current_spread, -1, 0)
    
    # 获取当前账户持仓信息,更新 holding_state
    # 注意:实盘中建议通过 get_trade_detail_data 实时核对持仓,这里简化处理
    check_position(ContextInfo)

    # ================= 交易逻辑 =================
    
    # 1. 开仓逻辑:价差扩大 -> 卖出套利 (空A 多B)
    if ContextInfo.holding_state == 0:
        if current_spread > ContextInfo.spread_open_threshold:
            print(f"触发开仓信号: 当前价差 {current_spread} > 阈值 {ContextInfo.spread_open_threshold}")
            
            # 卖出开仓 A (近月)
            sell_open(ContextInfo.contract_A, ContextInfo.lots, "LATEST", 0, ContextInfo, ContextInfo.accID)
            # 买入开仓 B (远月)
            buy_open(ContextInfo.contract_B, ContextInfo.lots, "LATEST", 0, ContextInfo, ContextInfo.accID)
            
            ContextInfo.holding_state = 1
            print(f"已执行: 卖开 {ContextInfo.contract_A}, 买开 {ContextInfo.contract_B}")

    # 2. 平仓逻辑:价差缩小 -> 平仓获利 (买平A 卖平B)
    elif ContextInfo.holding_state == 1:
        if current_spread < ContextInfo.spread_close_threshold:
            print(f"触发平仓信号: 当前价差 {current_spread} < 阈值 {ContextInfo.spread_close_threshold}")
            
            # 买入平仓 A (近月) - 优先平今
            buy_close_tdayfirst(ContextInfo.contract_A, ContextInfo.lots, "LATEST", 0, ContextInfo, ContextInfo.accID)
            # 卖出平仓 B (远月) - 优先平今
            sell_close_tdayfirst(ContextInfo.contract_B, ContextInfo.lots, "LATEST", 0, ContextInfo, ContextInfo.accID)
            
            ContextInfo.holding_state = 0
            print(f"已执行: 买平 {ContextInfo.contract_A}, 卖平 {ContextInfo.contract_B}")

def check_position(ContextInfo):
    """
    辅助函数:检查账户实际持仓,防止逻辑状态与实际账户不符
    """
    # 获取持仓列表
    positions = get_trade_detail_data(ContextInfo.accID, 'future', 'position')
    
    pos_A = 0 # A合约净持仓 (空单为负)
    pos_B = 0 # B合约净持仓
    
    for pos in positions:
        if pos.m_strInstrumentID + "." + pos.m_strExchangeID == ContextInfo.contract_A:
            # 期货持仓方向:48为多,49为空
            if pos.m_nDirection == 48:
                pos_A += pos.m_nVolume
            elif pos.m_nDirection == 49:
                pos_A -= pos.m_nVolume
        
        if pos.m_strInstrumentID + "." + pos.m_strExchangeID == ContextInfo.contract_B:
            if pos.m_nDirection == 48:
                pos_B += pos.m_nVolume
            elif pos.m_nDirection == 49:
                pos_B -= pos.m_nVolume

    # 简单的状态判断:如果持有A的空单且持有B的多单,认为在套利持仓中
    if pos_A < 0 and pos_B > 0:
        ContextInfo.holding_state = 1
    else:
        ContextInfo.holding_state = 0

代码关键点说明

  1. get_market_data_ex

    • 这是 QMT 获取行情的推荐接口。
    • period='tick' 表示获取最新的分笔数据,对于套利策略,价格敏感度高,通常使用 Tick 数据或 1分钟数据。
    • subscribe=True 确保在实盘运行时订阅实时推送。
  2. 价差计算

    • 代码中定义 current_spread = price_A - price_B
    • 如果 price_A (近月) 通常高于 price_B (远月)(即 Backwardation 结构),价差为正。
    • 如果市场结构相反(Contango),价差可能为负,您需要根据实际品种调整阈值(例如设置为 -10 和 -30)。
  3. 交易函数

    • sell_open (卖开) 和 buy_open (买开) 用于建立套利头寸。
    • buy_close_tdayfirstsell_close_tdayfirst 用于平仓。这里使用了“平今优先”,这对于日内交易或特定交易所(如上期所)非常重要,可以节省手续费或符合交易所规则。
  4. 持仓检查 (check_position)

    • 在量化交易中,仅仅依赖变量 ContextInfo.holding_state 是不安全的(例如程序重启后变量会重置)。
    • 代码增加了一个辅助函数,通过 get_trade_detail_data 读取账户真实的持仓情况来更新策略状态。

实盘注意事项

  1. 滑点风险:代码中使用 "LATEST" (最新价) 下单。在流动性不足时,可能导致单边成交(即A成交了,B没成交),从而暴露在单边风险中。实盘中通常需要更复杂的算法(如发单后检查成交,未成交则追单)。
  2. 合约代码:请务必将 IF2406.IF 等代码修改为当前市场上实际存在的合约代码。
  3. 账号ID:必须在 init 中填入真实的资金账号。
  4. 运行模式:此策略需要在“行情主图”下运行,或者在策略交易界面设置好运行周期(建议设置为 Tick 或 1分钟)。

Q&A

Q: 如何将策略改为“买入套利”(价差缩小时建仓,扩大时平仓)?
A: 只需要反转逻辑:

  • current_spread < 低阈值 时:buy_open(A) (买入近月) 和 sell_open(B) (卖出远月)。
  • current_spread > 高阈值 时:sell_close(A)buy_close(B)

Q: 为什么使用 buy_close_tdayfirst 而不是 buy_close
A: 在中国期货市场(特别是上期所),平今仓(平掉今日开的仓位)和平昨仓(平掉昨日以前的仓位)的手续费和指令是不同的。tdayfirst 会自动优先平掉今日的仓位,这在策略逻辑中通常更符合预期,也能避免因交易所规则导致的废单。

Q: 如何在回测中查看效果?
A: 在 QMT 策略编辑器中,点击“回测”。注意回测时需要补充对应的历史 Tick 或 分钟数据。回测模式下,ContextInfo.accID 可以是虚拟账号,但在 init 中仍需调用 set_account