🚀 AI 一键生成 qmt 策略代码

QMT 日内交易策略:如何编写一个分钟级别的均值反转策略?

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

问题描述

如何编写一个分钟级别的日内均值反转交易策略?

解决方案

这是一个非常经典的量化交易需求。日内均值反转策略(Intraday Mean Reversion)的核心逻辑是:价格围绕均值波动,当价格偏离均值过大(超买或超卖)时,大概率会回归均值。

通常我们使用 布林带(Bollinger Bands) 来实现这一逻辑:

  1. 中轨:移动平均线(MA)。
  2. 上轨:MA + K倍标准差(压力位)。
  3. 下轨:MA - K倍标准差(支撑位)。

策略逻辑:

  • 开空仓:价格突破上轨(超买)。
  • 开多仓:价格跌破下轨(超卖)。
  • 平仓:价格回归中轨(均值回归)。
  • 尾盘风控:收盘前强制平仓,不持仓过夜。

以下是基于 QMT Python API 编写的完整策略代码。

QMT 日内均值反转策略代码

# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import time

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 1. 设置交易标的(此处以沪深300股指期货主力合约为例,因为期货支持T+0,适合日内策略)
    # 如果是股票,需要有底仓才能做日内T+0,或者只能做T+1
    ContextInfo.stock_code = 'IF00.IF' 
    ContextInfo.set_universe([ContextInfo.stock_code])
    
    # 2. 设置资金账号 (请替换为您真实的资金账号)
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
    ContextInfo.account_type = 'FUTURE' # FUTURE:期货, STOCK:股票
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 3. 策略参数设置
    ContextInfo.period = '1m'       # 运行周期:1分钟
    ContextInfo.lookback = 20       # 均线周期 (N)
    ContextInfo.k_std = 2.0         # 标准差倍数 (K)
    ContextInfo.trade_vol = 1       # 每次交易手数
    
    # 4. 变量初始化
    ContextInfo.position = 0        # 当前持仓状态:0空仓,1多头,-1空头

def get_current_position(ContextInfo):
    """
    获取当前持仓状态的辅助函数
    返回: 0-无持仓, 1-多头, -1-空头
    """
    # 获取持仓明细
    positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
    
    net_vol = 0
    for pos in positions:
        if pos.m_strInstrumentID == ContextInfo.stock_code.split('.')[0]: # 简单的代码匹配
            # 期货持仓方向:48-多头(ENTRUST_BUY), 49-空头(ENTRUST_SELL)
            # 注意:不同柜台返回的Direction可能不同,需根据实际情况调试
            # 这里简化处理:多单量 - 空单量
            if pos.m_nDirection == 48: 
                net_vol += pos.m_nVolume
            elif pos.m_nDirection == 49:
                net_vol -= pos.m_nVolume
                
    if net_vol > 0:
        return 1
    elif net_vol < 0:
        return -1
    else:
        return 0

def handlebar(ContextInfo):
    """
    K线周期回调函数
    """
    # 跳过历史K线,只在最后根K线(实时行情)交易
    if not ContextInfo.is_last_bar():
        return

    # 1. 获取当前时间,处理尾盘清仓逻辑
    # get_bar_timetag 返回的是毫秒时间戳
    timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
    current_time_str = timetag_to_datetime(timetag, '%H:%M:%S')
    
    # 假设 14:55 之后不再开新仓,并强制平仓
    if current_time_str >= '14:55:00':
        curr_pos = get_current_position(ContextInfo)
        if curr_pos == 1:
            print(f"尾盘强平:卖出平仓 {ContextInfo.stock_code}")
            # 24: 卖出, 6: 平多(优先平今) - 期货常用
            passorder(6, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
        elif curr_pos == -1:
            print(f"尾盘强平:买入平仓 {ContextInfo.stock_code}")
            # 23: 买入, 8: 平空(优先平今) - 期货常用
            passorder(8, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
        return

    # 2. 获取行情数据
    # 多取一些数据以保证计算均线时数据足够
    count = ContextInfo.lookback + 5 
    data_map = ContextInfo.get_market_data_ex(
        ['close'], 
        [ContextInfo.stock_code], 
        period=ContextInfo.period, 
        count=count, 
        dividend_type='none'
    )
    
    if ContextInfo.stock_code not in data_map:
        return
        
    df = data_map[ContextInfo.stock_code]
    
    # 数据长度不足,无法计算指标
    if len(df) < ContextInfo.lookback:
        return

    # 3. 计算布林带指标
    # 计算滚动均值 (中轨)
    df['mean'] = df['close'].rolling(window=ContextInfo.lookback).mean()
    # 计算滚动标准差
    df['std'] = df['close'].rolling(window=ContextInfo.lookback).std()
    # 计算上轨
    df['upper'] = df['mean'] + ContextInfo.k_std * df['std']
    # 计算下轨
    df['lower'] = df['mean'] - ContextInfo.k_std * df['std']

    # 获取最新一根K线的数据
    current_close = df['close'].iloc[-1]
    current_upper = df['upper'].iloc[-1]
    current_lower = df['lower'].iloc[-1]
    current_mean = df['mean'].iloc[-1]
    
    # 4. 获取当前持仓状态
    # 注意:实盘中建议使用 get_trade_detail_data 获取真实持仓,回测可用 ContextInfo.position 模拟
    # 这里为了演示逻辑,使用 ContextInfo.position 变量进行简化控制
    # 实盘请替换为 get_current_position(ContextInfo)
    
    # 5. 交易逻辑
    
    # --- 开仓逻辑 ---
    if ContextInfo.position == 0:
        # 价格突破上轨,认为超买,开空单
        if current_close > current_upper:
            print(f"信号触发:价格({current_close}) > 上轨({current_upper}) -> 开空")
            # opType=3: 开空 (期货)
            passorder(3, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
            ContextInfo.position = -1
            
        # 价格跌破下轨,认为超卖,开多单
        elif current_close < current_lower:
            print(f"信号触发:价格({current_close}) < 下轨({current_lower}) -> 开多")
            # opType=0: 开多 (期货)
            passorder(0, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
            ContextInfo.position = 1

    # --- 平仓逻辑 (均值回归) ---
    elif ContextInfo.position == 1: # 持有多单
        # 价格回归到中轨上方,平多
        if current_close >= current_mean:
            print(f"信号触发:价格({current_close}) 回归中轨({current_mean}) -> 平多")
            # opType=6: 平多(优先平今)
            passorder(6, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
            ContextInfo.position = 0
            
    elif ContextInfo.position == -1: # 持有空单
        # 价格回归到中轨下方,平空
        if current_close <= current_mean:
            print(f"信号触发:价格({current_close}) 回归中轨({current_mean}) -> 平空")
            # opType=8: 平空(优先平今)
            passorder(8, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
            ContextInfo.position = 0

    # (可选) 在界面画图方便观察
    ContextInfo.paint('Upper', current_upper, -1, 0, 'red')
    ContextInfo.paint('Middle', current_mean, -1, 0, 'white')
    ContextInfo.paint('Lower', current_lower, -1, 0, 'green')

代码关键点解析

  1. 编码格式

    • 第一行 # -*- coding: gbk -*- 是必须的,因为 QMT 的 Python 环境默认使用 GBK 编码,否则中文字符会乱码或报错。
  2. 数据获取 (get_market_data_ex)

    • 我们使用 get_market_data_ex 获取历史数据。
    • count 设置为 lookback + 5 是为了确保 Pandas 在计算 rolling(滚动窗口)时有足够的数据,避免计算结果前几行为 NaN
  3. 指标计算

    • 利用 pandasrolling(window=20).mean()std() 快速计算布林带。这是 Python 量化最常用的方式,比循环计算效率高得多。
  4. 交易函数 (passorder)

    • 本策略演示的是期货交易逻辑(因为日内反转通常需要 T+0)。
    • opType 参数说明:
      • 0: 开多
      • 3: 开空
      • 6: 平多(优先平今,适合日内)
      • 8: 平空(优先平今,适合日内)
    • orderType 参数 1101 代表:单股、单账号、普通、股/手方式下单。
    • prType 参数 5 代表:最新价下单。
  5. 日内风控

    • 代码中包含了一个时间判断 if current_time_str >= '14:55:00':
    • 这是日内策略的关键,确保在收盘前强制平掉所有仓位,避免隔夜风险。

如何在 QMT 中运行

  1. 打开 QMT 客户端,进入【模型研究】或【策略交易】界面。
  2. 新建一个 Python 策略。
  3. 将上述代码复制粘贴进去。
  4. 修改账号:将 YOUR_ACCOUNT_ID 替换为你实际的资金账号。
  5. 修改标的:代码中默认是 IF00.IF (股指期货主力),如果你想跑股票(需有底仓)或 ETF,请修改 ContextInfo.stock_code
  6. 点击【编译】,确保无误后,点击【运行】或【回测】。

常见问题与优化建议

  • 股票 T+1 问题:如果你将此策略用于 A 股股票,由于 T+1 限制,当天买入的无法当天卖出。股票日内策略通常需要“底仓做 T”,即先持有股票,日内高抛低吸。如果是这种情况,需要修改 passorder 的逻辑,并检查可用持仓。
  • 滑点控制:代码中使用的是 最新价 下单,实盘中为了保证成交,通常会使用 对手价 (prType=14) 或者在最新价基础上增加一定的滑点(例如 price 参数传 -1,然后在算法交易参数里设置超价)。
  • 信号闪烁:本策略在 handlebar 中运行,如果是实时行情,handlebar 会在每个 Tick 更新时触发。为了避免同一根 K 线反复开平仓,可以使用 ContextInfo.is_new_bar() 判断,或者记录上一次交易的 Bar 索引。

Q&A

Q: 如何将此策略改为股票交易?
A: 股票是 T+1 制度。若要实现日内反转,必须持有底仓。逻辑需改为:信号触发做多时,买入;信号触发做空时,卖出持有的底仓。同时 passorderopType 需改为 23 (买入) 和 24 (卖出)。

Q: 为什么使用 get_market_data_ex 而不是 get_market_data
A: get_market_data_ex 是 QMT 较新的接口,返回的数据结构更清晰(字典包含 DataFrame),且处理多标的数据时性能更好,官方推荐使用。

Q: 如何调整策略的灵敏度?
A: 修改 ContextInfo.lookback (均线周期) 和 ContextInfo.k_std (标准差倍数)。lookback 越小越灵敏但噪点多;k_std 越小交易越频繁,越大交易越少但信号质量可能更高。