🚀 AI 一键生成 qmt 策略代码

如何在 QMT 中为整个投资组合设置基于最大回撤的动态止损? (附 Python 代码)

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

问题描述

如何根据整个策略账户的最大回撤来实现止损?

解决方案

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

核心逻辑

要实现基于账户最大回撤的止损,需要维护一个“高水位线”(High Water Mark),即策略运行以来的最高账户总资产。

  1. 获取当前总资产:在每个 handlebar 周期(或定时器中),通过 get_trade_detail_data 获取账户的动态权益(总资产)。
  2. 更新高水位线:如果当前总资产高于历史记录的最高资产,则更新高水位线。
  3. 计算回撤当前回撤 = 1 - (当前总资产 / 高水位线)
  4. 触发止损:如果当前回撤超过设定的阈值(例如 10%),则执行清仓操作,并停止策略开新仓。

代码实现

以下是完整的策略代码示例。该代码适用于 QMT 的实盘或模拟盘模式(Run Mode)。

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

def init(ContextInfo):
    # ================= 策略参数设置 =================
    # 设置资金账号 (请修改为您实际的资金账号)
    ContextInfo.accID = '6000000000' 
    # 账号类型: 'STOCK'股票, 'FUTURE'期货, 'CREDIT'信用
    ContextInfo.accType = 'STOCK'
    
    # 最大回撤止损阈值 (例如 0.10 代表 10%)
    ContextInfo.drawdown_limit = 0.10
    
    # ================= 全局变量初始化 =================
    # 记录账户历史最高权益 (高水位线)
    ContextInfo.max_balance = 0.0
    # 标记是否已经触发止损
    ContextInfo.is_stop_triggered = False
    
    # 绑定账号,用于接收回报
    ContextInfo.set_account(ContextInfo.accID)
    print("策略初始化完成,账户回撤止损阈值设置为: {:.2%}".format(ContextInfo.drawdown_limit))

def handlebar(ContextInfo):
    # 如果已经触发了止损,不再执行任何逻辑 (或者可以在这里只执行平仓确认逻辑)
    if ContextInfo.is_stop_triggered:
        return

    # 1. 获取账户资金信息
    # get_trade_detail_data 返回的是一个列表,包含账户对象
    account_list = get_trade_detail_data(ContextInfo.accID, ContextInfo.accType, 'account')
    
    if not account_list:
        print("未获取到账户信息,跳过本次检查")
        return

    # 获取账户对象
    account_obj = account_list[0]
    # m_dBalance 为总资产(动态权益)
    current_balance = account_obj.m_dBalance

    # 2. 初始化或更新高水位线 (Max Balance)
    if ContextInfo.max_balance == 0:
        ContextInfo.max_balance = current_balance
    elif current_balance > ContextInfo.max_balance:
        ContextInfo.max_balance = current_balance

    # 3. 计算当前回撤
    # 回撤 = 1 - (当前净值 / 最高净值)
    current_drawdown = 0.0
    if ContextInfo.max_balance > 0:
        current_drawdown = 1 - (current_balance / ContextInfo.max_balance)

    # 打印当前状态 (可选,用于调试)
    # print(f"当前资产: {current_balance}, 历史最高: {ContextInfo.max_balance}, 当前回撤: {current_drawdown:.2%}")

    # 4. 判断是否超过阈值
    if current_drawdown >= ContextInfo.drawdown_limit:
        print("【警告】当前回撤 {:.2%} 已超过阈值 {:.2%},触发账户级止损!开始清仓...".format(
            current_drawdown, ContextInfo.drawdown_limit))
        
        # 执行清仓操作
        clean_all_positions(ContextInfo)
        
        # 标记止损已触发,防止重复下单或继续开仓
        ContextInfo.is_stop_triggered = True

def clean_all_positions(ContextInfo):
    """
    清空所有持仓
    """
    # 获取当前持仓列表
    position_list = get_trade_detail_data(ContextInfo.accID, ContextInfo.accType, 'position')
    
    if not position_list:
        print("当前无持仓,无需平仓。")
        return

    for pos in position_list:
        # 获取持仓数量 (m_nVolume 为当前拥股)
        volume = pos.m_nVolume
        # 获取证券代码
        stock_code = pos.m_strInstrumentID
        
        # 如果持仓量为0,跳过
        if volume <= 0:
            continue
            
        print(f"正在平仓: {stock_code}, 数量: {volume}")
        
        # 下单平仓
        # opType: 24 (股票卖出)
        # orderType: 1101 (单股、单账号、普通、股/手方式下单)
        # accountID: 资金账号
        # orderCode: 股票代码
        # prType: 14 (对手价,保证尽快成交) 或者 5 (最新价)
        # price: -1 (对手价模式下价格无效,填-1即可)
        # volume: 数量
        passorder(24, 1101, ContextInfo.accID, stock_code, 14, -1, volume, ContextInfo)
        
    print("清仓指令发送完毕。")

# 必须实现的空函数,防止报错(如果策略中没有用到)
def stop(ContextInfo):
    pass

代码详解

  1. init 函数

    • 设置了 ContextInfo.drawdown_limit(回撤阈值)。
    • 初始化 ContextInfo.max_balance 为 0,用于记录策略启动后的最高资产。
    • 设置 ContextInfo.is_stop_triggered 标志位,一旦止损触发,将其置为 True,防止策略在后续的 K 线中继续运行开仓逻辑或重复发送平仓单。
  2. handlebar 函数

    • 获取资产:使用 get_trade_detail_data(..., 'account') 获取账户详情。m_dBalance 属性代表当前的总资产(现金+持仓市值)。
    • 计算回撤:逻辑是 1 - (当前资产 / 历史最高资产)
    • 触发条件:当回撤大于等于设定的阈值时,调用 clean_all_positions 函数。
  3. clean_all_positions 函数

    • 使用 get_trade_detail_data(..., 'position') 获取当前所有持仓。
    • 遍历持仓列表,读取 m_strInstrumentID(代码)和 m_nVolume(持仓量)。
    • 使用 passorder 函数发送卖出指令。
    • 注意:这里使用了 prType=14(对手价),目的是在止损时尽快成交,而不是挂单等待。

注意事项

  1. 实盘与回测的区别
    • 上述代码主要针对实盘/模拟盘(Run Mode),因为它依赖 get_trade_detail_data 来获取真实的账户资金。
    • 如果是回测模式,通常使用 ContextInfo.capital 加上累计盈亏来计算净值,或者使用 ContextInfo.get_net_value(barpos) 接口。
  2. 高水位线的起点
    • 代码中 max_balance 是从策略启动那一刻开始记录的。如果你希望基于账户昨天的静态权益开始计算,可以在 init 中尝试获取 m_dPreBalance(期初权益)作为初始高水位线。
  3. 风控后的恢复
    • 当前逻辑是触发止损后 is_stop_triggered 变为 True,策略实际上就“死”了(不再执行逻辑)。如果需要人工干预后重启,需要重新运行策略或添加手动重置标志位的逻辑。

Q&A

Q: 为什么使用 get_trade_detail_data 而不是 ContextInfo.capital?
A: ContextInfo.capital 通常用于回测时的初始资金设置。在实盘或模拟交易中,资金是动态变化的,且可能包含非策略产生的资金变动(如出入金),get_trade_detail_data 能获取柜台实时的总资产数据,更加准确。

Q: passorder 中的 prType=14 是什么意思?
A: prType=14 代表“对手价”。在股票卖出时,对手价通常指“买一价”。使用对手价下单可以提高成交概率,适合止损这种需要快速离场的场景。

Q: 如何防止因为数据瞬间波动导致的误止损?
A: 可以在判断回撤时增加一个确认机制,例如连续 3 个 tick(分笔)都检测到回撤超标才触发,或者在计算回撤时使用最近几分钟的资产均值。但在极端行情下,立即止损通常是更安全的选择。