🚀 AI 一键生成 qmt 策略代码

QMT 策略风控:如何实现基于 ATR 的动态跟踪止损?(附源码)

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

问题描述

如何编写一个动态止损的策略,让止损幅度可以根据市场波动大小自动调整?

解决方案

这是一个非常实用的量化交易需求。要实现“根据市场波动大小自动调整止损幅度”,最经典且有效的方法是使用 ATR(平均真实波幅,Average True Range) 指标。

策略核心逻辑

  1. 衡量波动率 (ATR)
    • ATR 指标可以量化当前市场的波动程度。
    • 当市场波动剧烈时,ATR 值变大;市场平稳时,ATR 值变小。
  2. 动态计算止损价
    • 止损距离 = ATR × 倍数(例如 2.0)
    • 止损价 = 当前价格 - 止损距离
    • 这样,在波动大时,止损线会自动放宽,避免被正常的市场噪音震出局;在波动小时,止损线会自动收紧,保护利润。
  3. 移动止损 (Trailing Stop)
    • 为了锁定利润,我们通常会让止损线“只升不降”。如果价格上涨,止损线跟随上移;如果价格下跌,止损线保持不变,直到价格触及止损线触发卖出。

QMT 策略代码实现

以下是一个完整的 Python 策略代码。该策略使用 双均线(金叉) 作为入场信号,使用 ATR 动态移动止损 作为出场信号。

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

def init(ContextInfo):
    # 1. 设置策略参数
    ContextInfo.stock_list = ['600000.SH', '000001.SZ'] # 示例股票池
    ContextInfo.set_universe(ContextInfo.stock_list)
    
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID' # 请替换为您的资金账号
    ContextInfo.account_type = 'STOCK'         # 账号类型:STOCK股票,FUTURE期货
    
    # 策略参数
    ContextInfo.ma_short_period = 5    # 短期均线周期
    ContextInfo.ma_long_period = 20    # 长期均线周期
    ContextInfo.atr_period = 14        # ATR计算周期
    ContextInfo.atr_multiplier = 2.0   # ATR倍数,用于控制止损宽度
    
    # 全局变量:用于存储每只股票当前的动态止损价
    # 格式:{ 'stock_code': stop_price }
    ContextInfo.dynamic_stop_prices = {} 

def handlebar(ContextInfo):
    # 获取当前K线位置
    index = ContextInfo.barpos
    # 获取当前时间
    realtime = ContextInfo.get_bar_timetag(index)
    
    # 遍历股票池
    for stock in ContextInfo.stock_list:
        # 2. 获取历史行情数据 (多取一些数据以保证指标计算准确)
        # 我们需要 High, Low, Close 来计算 ATR
        data_len = 100 
        market_data = ContextInfo.get_market_data_ex(
            ['high', 'low', 'close', 'open'], 
            [stock], 
            period='1d', 
            start_time='', 
            end_time='', 
            count=data_len, 
            dividend_type='front', 
            fill_data=True, 
            subscribe=True
        )
        
        if stock not in market_data or len(market_data[stock]) < ContextInfo.atr_period + 5:
            continue
            
        df = market_data[stock]
        
        # 3. 计算技术指标
        # 转换数据类型为 float,talib 需要
        high_prices = df['high'].values.astype(float)
        low_prices = df['low'].values.astype(float)
        close_prices = df['close'].values.astype(float)
        
        # 计算 ATR
        atr = talib.ATR(high_prices, low_prices, close_prices, timeperiod=ContextInfo.atr_period)
        current_atr = atr[-1] # 获取最新的ATR值
        
        # 计算均线
        ma_short = talib.SMA(close_prices, timeperiod=ContextInfo.ma_short_period)
        ma_long = talib.SMA(close_prices, timeperiod=ContextInfo.ma_long_period)
        
        # 获取最新价格
        current_price = close_prices[-1]
        prev_price = close_prices[-2]
        
        # 4. 获取当前持仓信息
        positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
        holding_vol = 0
        for pos in positions:
            if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == stock:
                holding_vol = pos.m_nVolume
                break
        
        # ================= 交易逻辑 =================
        
        # --- 场景 A: 持有仓位,检查是否触发动态止损 ---
        if holding_vol > 0:
            # 获取该股票记录的止损价
            stop_price = ContextInfo.dynamic_stop_prices.get(stock, 0)
            
            # 1. 检查是否触发止损
            if current_price < stop_price:
                print(f"【止损卖出】{stock} 当前价: {current_price} < 止损价: {stop_price:.2f} (波动率ATR: {current_atr:.3f})")
                passorder(24, 1101, ContextInfo.account_id, stock, 5, -1, holding_vol, ContextInfo)
                # 清除止损价记录
                del ContextInfo.dynamic_stop_prices[stock]
            
            else:
                # 2. 动态调整止损价 (Trailing Stop)
                # 逻辑:如果价格上涨,止损价跟随上涨;如果价格下跌,止损价保持不变
                # 新的潜在止损价 = 当前价格 - (ATR * 倍数)
                new_potential_stop = current_price - (current_atr * ContextInfo.atr_multiplier)
                
                # 如果新的止损价高于旧的止损价,则上移止损线(锁定利润)
                if new_potential_stop > stop_price:
                    ContextInfo.dynamic_stop_prices[stock] = new_potential_stop
                    # print(f"【止损上移】{stock} 新止损价: {new_potential_stop:.2f}")

        # --- 场景 B: 无仓位,检查开仓信号 ---
        elif holding_vol == 0:
            # 简单的均线金叉策略:短期上穿长期
            if ma_short[-1] > ma_long[-1] and ma_short[-2] <= ma_long[-2]:
                print(f"【买入开仓】{stock} 均线金叉,当前价: {current_price}")
                passorder(23, 1101, ContextInfo.account_id, stock, 5, -1, 1000, ContextInfo) # 买入1000股
                
                # *** 关键步骤:开仓时初始化止损价 ***
                # 初始止损价 = 开仓价 - (当前ATR * 倍数)
                initial_stop_price = current_price - (current_atr * ContextInfo.atr_multiplier)
                ContextInfo.dynamic_stop_prices[stock] = initial_stop_price
                print(f"--> 初始动态止损价已设定: {initial_stop_price:.2f} (ATR: {current_atr:.3f})")

    # 绘图 (可选,用于回测观察)
    # ContextInfo.paint('ATR', current_atr, -1, 0)

代码详解

  1. 引入 talib

    • QMT 内置了 talib 库,这是计算 ATR 最快、最标准的方法。
    • talib.ATR(high, low, close, timeperiod) 函数直接返回 ATR 序列。
  2. 全局变量 ContextInfo.dynamic_stop_prices

    • 这是一个字典,用于在内存中记录每只持仓股票当前的止损线。
    • 因为止损线是动态变化的,我们需要在每次 handlebar 运行时读取并更新它。
  3. 开仓时的初始化

    • 当触发买入信号时,立即计算初始止损价:当前价 - (ATR * 倍数)
    • 如果当前市场波动大(ATR大),止损线会离价格比较远,防止被震荡洗盘。
  4. 持仓时的动态调整 (Trailing Logic)

    • 这是策略的灵魂所在。代码中有一行逻辑:
      if new_potential_stop > stop_price:
          ContextInfo.dynamic_stop_prices[stock] = new_potential_stop
      
    • 这意味着:止损线只能上移,不能下移
    • 当股价上涨,current_price 变大,算出来的 new_potential_stop 也会变大,于是止损线跟着往上走,保护浮盈。
    • 当股价下跌,new_potential_stop 会变小,但因为我们只取最大值,所以止损线保持在原位不动,等待价格跌破。

如何使用

  1. 打开 QMT 客户端,进入“模型研究” -> “策略编辑器”。
  2. 新建一个 Python 策略。
  3. 将上述代码复制进去。
  4. 修改 ContextInfo.account_id 为你的实际账号。
  5. 点击“回测”查看效果,或者在实盘模式下运行。

常见问题 Q&A

Q: 为什么使用 ATR 而不是百分比(比如固定 5% 止损)?
A: 固定百分比止损忽略了市场环境。在牛市高波动期,5% 可能一天就跌到了,容易被误杀;在低波动期,5% 可能太宽了,导致亏损过大。ATR 能根据当前市场的“脾气”来决定安全距离。

Q: ContextInfo.atr_multiplier 设置多少合适?
A: 通常设置为 1.5 到 3.0 之间。

  • 如果是短线激进策略,可以设为 1.5 或 2.0。
  • 如果是中长线趋势策略,建议设为 2.5 或 3.0,以容忍更大的回撤。

Q: 这个策略在实盘中重启后,止损价会丢失吗?
A: 是的,上述代码中的 ContextInfo.dynamic_stop_prices 存储在内存中。如果 QMT 软件关闭重启,这个变量会清空。

  • 解决方法:在实盘中,通常建议将止损价写入本地文件(如 txt 或 json)或使用 QMT 的 ContextInfo.set_dict_data (如果支持持久化) / 数据库。简单的做法是在 init 函数里加一段逻辑:如果持有仓位但字典里没有止损价,则按 当前价 - ATR 重新初始化一个保护性止损。