🚀 AI 一键生成 qmt 策略代码

QMT 策略编程技巧:如何设计无状态 (Stateless) 的交易策略?

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

问题描述

如何编写一个不使用全局变量来保存状态的交易策略?

解决方案

在 QMT (Quantitative Trading Platform) 中,避免使用 Python 原生 global 关键字来保存策略状态的最佳实践是利用平台提供的 ContextInfo 对象。

ContextInfo 是一个贯穿策略整个生命周期的对象。它不仅包含 API 方法,还是一个容器,允许你将自定义变量绑定为它的属性。这样,变量就可以在 inithandlebar 以及其他回调函数之间传递和持久化,起到类似“类实例属性”的作用,从而完全替代全局变量。

核心思路

  1. 初始化状态:在 init(ContextInfo) 中,通过 ContextInfo.变量名 = 初始值 的方式定义参数和状态变量。
  2. 读取与更新状态:在 handlebar(ContextInfo) 中,通过 ContextInfo.变量名 来读取或修改这些值。

示例代码:双均线策略(无全局变量版)

下面是一个完整的双均线策略示例。该策略将所有参数(如均线周期、股票代码、账号)和状态都保存在 ContextInfo 对象上。

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

def init(ContextInfo):
    """
    初始化函数:在这里定义所有需要的状态变量,绑定到 ContextInfo 上
    """
    print("策略初始化...")
    
    # 1. 绑定策略参数到 ContextInfo
    ContextInfo.stock_code = '600000.SH'  # 交易标的:浦发银行
    ContextInfo.short_period = 5          # 短周期
    ContextInfo.long_period = 20          # 长周期
    ContextInfo.account_id = '6000000248' # 资金账号 (请修改为实际账号)
    ContextInfo.trade_qty = 100           # 每次交易数量
    
    # 2. 设置股票池
    ContextInfo.set_universe([ContextInfo.stock_code])
    
    # 3. 绑定自定义状态变量 (替代全局变量)
    # 例如:记录上一次的交易信号,或者自定义的持仓状态
    ContextInfo.last_signal = 0  # 0:无信号, 1:金叉, -1:死叉
    
    # 4. 设置账号
    ContextInfo.set_account(ContextInfo.account_id)

def handlebar(ContextInfo):
    """
    行情驱动函数:通过 ContextInfo 获取参数和更新状态
    """
    # 获取当前K线位置
    index = ContextInfo.barpos
    
    # 获取当前主图代码(或者使用我们在 init 中定义的 ContextInfo.stock_code)
    code = ContextInfo.stock_code
    
    # 获取历史行情数据 (使用 get_market_data_ex)
    # 获取足够计算长周期均线的数据量
    data_len = ContextInfo.long_period + 5 
    
    # 获取日线数据
    market_data = ContextInfo.get_market_data_ex(
        fields=['close'], 
        stock_code=[code], 
        period='1d', 
        count=data_len, 
        dividend_type='front'
    )
    
    # 检查数据是否获取成功
    if code not in market_data or market_data[code].empty:
        return

    df = market_data[code]
    close_prices = df['close']
    
    # 确保数据长度足够
    if len(close_prices) < ContextInfo.long_period:
        return

    # 计算均线
    # 注意:这里直接使用 ContextInfo.short_period 读取参数
    ma_short = close_prices.rolling(window=ContextInfo.short_period).mean()
    ma_long = close_prices.rolling(window=ContextInfo.long_period).mean()
    
    # 获取最新和上一个时间点的均线值
    current_short = ma_short.iloc[-1]
    current_long = ma_long.iloc[-1]
    prev_short = ma_short.iloc[-2]
    prev_long = ma_long.iloc[-2]
    
    # --- 交易逻辑 ---
    
    # 1. 金叉判断 (短线上穿长线)
    is_golden_cross = (prev_short <= prev_long) and (current_short > current_long)
    
    # 2. 死叉判断 (短线下穿长线)
    is_death_cross = (prev_short >= prev_long) and (current_short < current_long)
    
    # 获取当前持仓 (为了演示,这里通过 ContextInfo 获取实际持仓,而不是依赖全局变量记录)
    positions = get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
    current_holding = 0
    for pos in positions:
        if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == code:
            current_holding = pos.m_nVolume
            break
            
    # 执行买入
    if is_golden_cross:
        # 只有当 ContextInfo.last_signal 不为 1 时才打印,避免重复日志
        if ContextInfo.last_signal != 1:
            print(f"[{timetag_to_datetime(ContextInfo.get_bar_timetag(index), '%Y-%m-%d')}] 金叉出现,尝试买入")
        
        if current_holding == 0:
            # 使用 ContextInfo.trade_qty 读取交易数量
            passorder(23, 1101, ContextInfo.account_id, code, 5, -1, ContextInfo.trade_qty, ContextInfo)
            print(f"买入 {code} {ContextInfo.trade_qty} 股")
            
            # 更新状态:记录当前信号状态到 ContextInfo
            ContextInfo.last_signal = 1

    # 执行卖出
    elif is_death_cross:
        if ContextInfo.last_signal != -1:
            print(f"[{timetag_to_datetime(ContextInfo.get_bar_timetag(index), '%Y-%m-%d')}] 死叉出现,尝试卖出")
            
        if current_holding > 0:
            passorder(24, 1101, ContextInfo.account_id, code, 5, -1, current_holding, ContextInfo)
            print(f"卖出 {code} {current_holding} 股")
            
            # 更新状态:记录当前信号状态到 ContextInfo
            ContextInfo.last_signal = -1

代码解析

  1. ContextInfo 的角色

    • 在 Python 的类(Class)编程中,我们使用 self.variable 来在方法间共享数据。
    • 在 QMT 的脚本模式中,ContextInfo 扮演了类似 self 的角色。
    • 代码中没有出现 global short_periodglobal last_signal。所有的变量都挂载在 ContextInfo 下(例如 ContextInfo.short_period)。
  2. 状态持久化

    • ContextInfo.last_signal 被用来记录上一次的信号状态。即使 handlebar 函数执行完毕,下一次行情到来再次触发 handlebar 时,ContextInfo.last_signal 依然保留着上一次赋值的内容。
  3. 参数管理

    • stock_codeaccount_id 等硬编码的参数全部放在 init 中并绑定到 ContextInfo,使得 handlebar 逻辑更加纯粹,修改参数只需在 init 中进行。

总结

在 QMT 中编写策略时,应当始终优先使用 ContextInfo 来存储状态。这不仅避免了全局变量带来的命名空间污染和潜在的逻辑混乱,也使得策略代码结构更清晰,更符合 QMT 框架的设计规范。


Q&A

Q: 为什么在 QMT 中不推荐使用 Python 的 global 关键字?
A: 虽然技术上可行,但使用 global 会导致变量散落在模块层级,难以管理。在进行回测或实盘切换,或者策略重置时,全局变量的状态管理可能会变得复杂。使用 ContextInfo 可以确保所有状态都封装在策略实例的上下文中,生命周期由平台管理,更加安全规范。

Q: ContextInfo 中可以存储什么类型的数据?
A: ContextInfo 是一个 Python 对象,你可以将任何 Python 数据类型绑定给它,包括整数、浮点数、字符串、列表、字典,甚至是 Pandas DataFrame 或自定义类的实例。

Q: 如果我在 handlebar 中修改了 ContextInfo.my_var,下一次 handlebar 调用时它会重置吗?
A: 不会。ContextInfo 对象在策略启动后一直存在,直到策略停止。你在 handlebar 中对 ContextInfo 属性所做的任何修改都会保留到下一次调用。只有在 init 函数重新运行时(例如重启策略),这些值才会被重置。