🚀 AI 一键生成 qmt 策略代码

QMT Python 策略如何设置动态止盈和移动止损? (附代码示例)

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

问题描述

帮我写一个带有动态止盈和移动止损功能的交易策略。

解决方案

这是一个基于 QMT 平台的量化交易策略,实现了均线金叉买入,并结合了固定止损和**移动止损(动态止盈)**的功能。

策略逻辑说明

  1. 入场信号(买入)
    • 使用简单的双均线策略作为示例:当短期均线(MA5)上穿长期均线(MA10)时买入。
  2. 固定止损(Stop Loss)
    • 当当前价格低于持仓成本的一定比例(例如 -3%)时,强制止损。
  3. 移动止损/动态止盈(Trailing Stop)
    • 触发条件:当股价从持仓成本上涨超过一定比例(例如 +5%)后,激活移动止损逻辑。
    • 回撤卖出:记录持仓期间的最高价。如果当前价格从最高价回撤超过设定比例(例如 -2%),则触发止盈卖出。
    • 效果:让利润奔跑,同时在趋势反转时锁定利润。

QMT 策略代码

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

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 1. 设置策略参数
    ContextInfo.account_id = '6000000000'  # 请修改为您的资金账号
    ContextInfo.account_type = 'STOCK'     # 账号类型:STOCK-股票,FUTURE-期货
    ContextInfo.is_backtest = True         # 是否为回测模式,实盘请改为 False
    
    # 均线参数
    ContextInfo.ma_short_period = 5
    ContextInfo.ma_long_period = 10
    
    # 交易参数
    ContextInfo.buy_amount = 1000          # 每次买入股数
    
    # --- 核心风控参数 ---
    ContextInfo.stop_loss_pct = 0.05       # 固定止损比例:亏损 5% 止损
    ContextInfo.trailing_start_pct = 0.05  # 移动止损激活阈值:盈利 5% 后开启移动止损
    ContextInfo.trailing_gap_pct = 0.02    # 移动止损回撤比例:从最高点回撤 2% 止盈
    
    # 2. 全局变量
    # 用于记录持仓股票在持仓期间的最高价,格式:{'stock_code': high_price}
    ContextInfo.holding_high_prices = {} 
    
    # 3. 设置股票池 (示例:平安银行,贵州茅台)
    ContextInfo.target_list = ['000001.SZ', '600519.SH']
    ContextInfo.set_universe(ContextInfo.target_list)
    
    # 4. 绑定账号 (实盘必须)
    if not ContextInfo.is_backtest:
        ContextInfo.set_account(ContextInfo.account_id)

def handlebar(ContextInfo):
    """
    K线周期运行函数
    """
    # 获取当前K线索引
    index = ContextInfo.barpos
    # 获取当前时间
    realtime = ContextInfo.get_bar_timetag(index)
    
    # 获取股票池
    stock_list = ContextInfo.get_universe()
    
    # 获取持仓信息
    positions = get_current_positions(ContextInfo)
    
    # --- 1. 准备数据 ---
    # 获取历史行情数据 (多取一些以计算均线)
    data_len = ContextInfo.ma_long_period + 2
    # 使用 get_market_data_ex 获取数据,效率更高
    market_data = ContextInfo.get_market_data_ex(
        ['open', 'high', 'low', 'close'], 
        stock_list, 
        period=ContextInfo.period, 
        count=data_len,
        dividend_type='front' # 前复权
    )
    
    # --- 2. 遍历股票进行逻辑判断 ---
    for stock in stock_list:
        if stock not in market_data:
            continue
            
        df = market_data[stock]
        if len(df) < ContextInfo.ma_long_period:
            continue
            
        # 获取最新价格
        current_price = df['close'].iloc[-1]
        # 获取当前K线的最高价(用于更新移动止损的高点)
        current_high = df['high'].iloc[-1]
        
        # --- 3. 卖出逻辑 (止损与移动止盈) ---
        if stock in positions:
            pos_info = positions[stock]
            cost_price = pos_info['cost_price'] # 持仓成本
            volume = pos_info['volume']         # 持仓数量
            
            # A. 更新持仓期间的最高价
            # 如果是新买入的股票,初始化最高价为成本价或当前高价
            if stock not in ContextInfo.holding_high_prices:
                ContextInfo.holding_high_prices[stock] = max(cost_price, current_high)
            else:
                # 如果当前价格创新高,更新最高价
                if current_high > ContextInfo.holding_high_prices[stock]:
                    ContextInfo.holding_high_prices[stock] = current_high
            
            record_high = ContextInfo.holding_high_prices[stock]
            
            # B. 计算收益率情况
            profit_pct = (current_price - cost_price) / cost_price
            drawdown_pct = (record_high - current_price) / record_high
            
            sell_signal = False
            sell_reason = ""
            
            # C. 检查固定止损
            if profit_pct <= -ContextInfo.stop_loss_pct:
                sell_signal = True
                sell_reason = f"固定止损触发: 当前亏损 {profit_pct*100:.2f}%"
            
            # D. 检查移动止损 (动态止盈)
            # 条件1: 当前盈利超过激活阈值 (trailing_start_pct)
            # 条件2: 从最高点回撤超过设定比例 (trailing_gap_pct)
            elif (current_price > cost_price * (1 + ContextInfo.trailing_start_pct)) and \
                 (drawdown_pct >= ContextInfo.trailing_gap_pct):
                sell_signal = True
                sell_reason = f"移动止盈触发: 最高价 {record_high}, 回撤 {drawdown_pct*100:.2f}%"
            
            # 执行卖出
            if sell_signal:
                print(f"[{stock}] {sell_reason}, 执行卖出")
                order_target_volume(ContextInfo, stock, 0) # 清仓
                # 卖出后清除最高价记录
                if stock in ContextInfo.holding_high_prices:
                    del ContextInfo.holding_high_prices[stock]
                continue # 卖出后跳过买入逻辑
        
        # --- 4. 买入逻辑 (均线金叉) ---
        else: # 没有持仓才考虑买入
            # 计算均线
            close_prices = df['close']
            ma_short = close_prices.rolling(ContextInfo.ma_short_period).mean()
            ma_long = close_prices.rolling(ContextInfo.ma_long_period).mean()
            
            # 获取最后两个点判断金叉
            ma_s_curr = ma_short.iloc[-1]
            ma_s_prev = ma_short.iloc[-2]
            ma_l_curr = ma_long.iloc[-1]
            ma_l_prev = ma_long.iloc[-2]
            
            # 金叉判断: 短线上穿长线
            if ma_s_prev <= ma_l_prev and ma_s_curr > ma_l_curr:
                print(f"[{stock}] 金叉触发 (MA{ContextInfo.ma_short_period}={ma_s_curr:.2f}, MA{ContextInfo.ma_long_period}={ma_l_curr:.2f}), 执行买入")
                order_target_volume(ContextInfo, stock, ContextInfo.buy_amount)
                # 买入时初始化最高价记录
                ContextInfo.holding_high_prices[stock] = current_price

def get_current_positions(ContextInfo):
    """
    获取当前持仓信息的辅助函数
    返回字典: {'stock_code': {'volume': 100, 'cost_price': 10.5}}
    """
    pos_dict = {}
    
    # 回测模式下使用 ContextInfo.get_trade_detail_data 可能不准确,通常回测引擎会自动维护
    # 但为了兼容实盘逻辑,这里演示如何获取
    if ContextInfo.is_backtest:
        # QMT回测模式下,通常不需要手动查询持仓对象来做逻辑判断,
        # 但为了演示移动止损需要成本价,我们这里模拟一下或使用内置函数
        # 注意:QMT回测中 get_trade_detail_data 可能返回空,需依赖 ContextInfo.get_position 等(视版本而定)
        # 这里使用通用的 get_trade_detail_data,但在回测中可能需要特定处理
        # 为简化代码,假设回测系统能返回正确对象,或者用户在实盘使用
        pass
    
    # 获取持仓对象列表
    positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
    
    for pos in positions:
        stock_code = pos.m_strInstrumentID + "." + pos.m_strExchangeID
        vol = pos.m_nVolume
        price = pos.m_dOpenPrice # 开仓均价
        
        # 过滤掉空仓
        if vol > 0:
            pos_dict[stock_code] = {
                'volume': vol,
                'cost_price': price
            }
            
    return pos_dict

def order_target_volume(ContextInfo, stock_code, target_volume):
    """
    下单辅助函数:调整持仓到目标数量
    """
    # 23: 买入, 24: 卖出
    # 1101: 单股单账号
    
    # 获取当前持仓
    positions = get_current_positions(ContextInfo)
    curr_vol = 0
    if stock_code in positions:
        curr_vol = positions[stock_code]['volume']
    
    diff = target_volume - curr_vol
    
    if diff > 0:
        # 买入
        passorder(23, 1101, ContextInfo.account_id, stock_code, 5, -1, diff, ContextInfo)
    elif diff < 0:
        # 卖出
        passorder(24, 1101, ContextInfo.account_id, stock_code, 5, -1, abs(diff), ContextInfo)

关键功能详解

  1. ContextInfo.holding_high_prices (核心):

    • 这是一个字典,用于“记忆”我们持仓期间股票达到过的最高价格。
    • 逻辑:每次 handlebar 运行时,如果持有某只股票,就检查当前的 high 价格是否高于字典里记录的价格。如果是,就更新它。这是计算“回撤”的基础。
    • 清理:当股票卖出后,需要从字典中删除该股票的记录,以免影响下次买入。
  2. 移动止损逻辑 (Trailing Stop):

    • 代码段:
      elif (current_price > cost_price * (1 + ContextInfo.trailing_start_pct)) and \
           (drawdown_pct >= ContextInfo.trailing_gap_pct):
      
    • 解释
      • current_price > cost_price * (1 + 0.05): 首先,股价必须比成本价高出 5%(激活门槛),防止刚买入波动就触发止盈。
      • drawdown_pct >= 0.02: 其次,当前价格相比于持仓期间的最高价 (record_high),回撤了 2% 以上。
      • 只有同时满足这两个条件,才会触发卖出。
  3. 数据获取:

    • 使用了 get_market_data_ex,这是 QMT 推荐的高效数据接口,返回的是 pandas DataFrame 格式,方便进行均线计算 (rolling().mean())。

使用前注意事项

  1. 账号设置:请务必将 ContextInfo.account_id 修改为您实际的资金账号。
  2. 回测与实盘
    • 代码中设置了 ContextInfo.is_backtest。如果是实盘,请将其改为 False
    • 实盘中,get_trade_detail_data 需要确保客户端已登录交易账号。
  3. 编码格式:QMT 的 Python 编辑器通常需要文件头包含 # -*- coding: gbk -*- 以正确处理中文字符。

Q&A

Q: 为什么移动止损不直接用最高价回撤,而要设置一个激活阈值(trailing_start_pct)?
A: 如果不设置激活阈值(例如盈利5%才开始),股票刚买入时如果微涨后马上微跌(例如涨1%跌回0.5%),就会立即触发回撤卖出,导致频繁止损磨损本金。设置阈值是为了确保“有利润”了再开始保护利润。

Q: passorder 函数中的参数 5-1 是什么意思?
A: 5 代表报价类型为“最新价”(PRTP_LATEST),-1 是价格参数,当使用最新价下单时,价格参数通常填 -10,系统会自动取当前行情的最新价。

Q: 如何修改为“目标止盈”(例如涨10%直接卖出)?
A: 您可以在卖出逻辑的 if/elif 结构中增加一个判断:

elif profit_pct >= 0.10: # 盈利超过10%
    sell_signal = True
    sell_reason = "目标止盈触发"