🚀 AI 一键生成 qmt 策略代码

Python 量化:在 QMT 中编写 Vortex Indicator (VI) 趋势策略

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

问题描述

请帮我写一个基于 Vortex 指标的趋势策略

解决方案

这是一个基于 Vortex Indicator (VI, 旋涡指标) 的趋势跟踪策略。

策略原理

Vortex 指标由两条线组成:$VI^+$(正向趋势)和 $VI^-$(负向趋势)。

  1. $VI^+$ (Vortex Positive):衡量上升趋势的强度。
  2. $VI^-$ (Vortex Negative):衡量下降趋势的强度。

交易逻辑:

  • 买入信号(金叉):当 $VI^+$ 上穿 $VI^-$ 时,表明上升趋势确立,执行买入。
  • 卖出信号(死叉):当 $VI^+$ 下穿 $VI^-$ 时,表明下降趋势确立,执行卖出(平仓)。

策略代码

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

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 设置策略参数:Vortex指标的周期,通常为14
    ContextInfo.vi_period = 14
    
    # 设置股票池,这里示例使用 沪深300ETF
    ContextInfo.stock_code = '510300.SH'
    ContextInfo.set_universe([ContextInfo.stock_code])
    
    # 设置资金账号(请替换为您的真实账号)
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
    ContextInfo.account_type = 'STOCK'
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 设置回测参数(仅回测有效,实盘需在界面设置)
    ContextInfo.set_commission(0, [0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 5]) # 设置费率
    
    print("Vortex 策略初始化完成")

def handlebar(ContextInfo):
    """
    K线逐笔运行函数
    """
    # 获取当前正在处理的股票代码
    stock_code = ContextInfo.stock_code
    
    # 获取当前K线索引
    index = ContextInfo.barpos
    
    # 获取历史行情数据
    # 我们需要足够的历史数据来计算 rolling sum,取 period * 2 确保数据充足
    data_count = ContextInfo.vi_period * 2 + 5
    
    # 使用 get_market_data_ex 获取数据 (开高低收)
    # 注意:period='1d' 表示日线,可根据需要修改为 '15m', '30m' 等
    market_data = ContextInfo.get_market_data_ex(
        ['high', 'low', 'close', 'open'], 
        [stock_code], 
        period='1d', 
        count=data_count, 
        dividend_type='front' # 前复权
    )
    
    if stock_code not in market_data:
        return
        
    df = market_data[stock_code]
    
    # 如果数据量不足以计算指标,直接返回
    if len(df) < ContextInfo.vi_period + 2:
        return

    # --- Vortex 指标计算逻辑 ---
    
    # 1. 计算 True Range (TR)
    # TR = Max(High - Low, |High - PrevClose|, |Low - PrevClose|)
    high = df['high']
    low = df['low']
    close = df['close']
    prev_close = close.shift(1)
    
    tr1 = high - low
    tr2 = abs(high - prev_close)
    tr3 = abs(low - prev_close)
    
    # 逐元素取最大值
    true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
    
    # 2. 计算 VM+ 和 VM- (Vortex Movement)
    # VM+ = |Current High - Previous Low|
    # VM- = |Current Low - Previous High|
    prev_low = low.shift(1)
    prev_high = high.shift(1)
    
    vm_plus = abs(high - prev_low)
    vm_minus = abs(low - prev_high)
    
    # 3. 对 TR, VM+, VM- 进行周期求和 (Sum over period)
    tr_sum = true_range.rolling(window=ContextInfo.vi_period).sum()
    vm_plus_sum = vm_plus.rolling(window=ContextInfo.vi_period).sum()
    vm_minus_sum = vm_minus.rolling(window=ContextInfo.vi_period).sum()
    
    # 4. 计算最终的 VI+ 和 VI-
    vi_plus = vm_plus_sum / tr_sum
    vi_minus = vm_minus_sum / tr_sum
    
    # 获取当前K线和上一根K线的指标值(用于判断交叉)
    # iloc[-1] 是当前bar,iloc[-2] 是上一根bar
    current_vi_plus = vi_plus.iloc[-1]
    current_vi_minus = vi_minus.iloc[-1]
    prev_vi_plus = vi_plus.iloc[-2]
    prev_vi_minus = vi_minus.iloc[-2]
    
    # 检查是否计算出有效值 (排除NaN)
    if np.isnan(current_vi_plus) or np.isnan(current_vi_minus):
        return

    # --- 交易信号判断 ---
    
    # 获取当前持仓
    positions = ContextInfo.get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
    current_holding = 0
    for pos in positions:
        if pos.m_strInstrumentID == stock_code:
            current_holding = pos.m_nVolume
            break

    # 信号 1: 金叉 (VI+ 上穿 VI-) -> 买入
    buy_signal = (prev_vi_plus < prev_vi_minus) and (current_vi_plus > current_vi_minus)
    
    # 信号 2: 死叉 (VI+ 下穿 VI-) -> 卖出
    sell_signal = (prev_vi_plus > prev_vi_minus) and (current_vi_plus < current_vi_minus)
    
    # --- 执行交易 ---
    
    # 如果是最后一根K线(实时行情或回测的当前步)
    if ContextInfo.is_last_bar():
        timetag = ContextInfo.get_bar_timetag(index)
        date_str = timetag_to_datetime(timetag, '%Y-%m-%d %H:%M:%S')
        
        if buy_signal:
            # 如果没有持仓,则全仓买入
            if current_holding == 0:
                print(f"[{date_str}] 金叉出现 (VI+={current_vi_plus:.4f}, VI-={current_vi_minus:.4f}),执行买入: {stock_code}")
                # 目标市值下单:调整仓位到 100% (即全仓)
                # 注意:实盘中请谨慎使用 order_target_percent,建议根据资金管理逻辑调整
                order_target_percent(stock_code, 1.0, "FIX", df['close'].iloc[-1], ContextInfo, ContextInfo.account_id)
        
        elif sell_signal:
            # 如果有持仓,则清仓
            if current_holding > 0:
                print(f"[{date_str}] 死叉出现 (VI+={current_vi_plus:.4f}, VI-={current_vi_minus:.4f}),执行卖出: {stock_code}")
                # 目标市值下单:调整仓位到 0% (即清仓)
                order_target_percent(stock_code, 0.0, "FIX", df['close'].iloc[-1], ContextInfo, ContextInfo.account_id)

    # --- 绘图 (可选,用于回测界面观察) ---
    # 绘制 VI+ 和 VI- 曲线
    ContextInfo.paint('VI_Plus', current_vi_plus, -1, 0, 'red')   # 红色代表多头趋势
    ContextInfo.paint('VI_Minus', current_vi_minus, -1, 0, 'green') # 绿色代表空头趋势

代码详细解析

  1. 初始化 (init):

    • 设定了 vi_period = 14,这是 Vortex 指标的标准参数。
    • 设定了交易标的(如 510300.SH)和账户信息。
    • 注意:请务必将 YOUR_ACCOUNT_ID 替换为您实际的 QMT 资金账号。
  2. 数据获取 (handlebar):

    • 使用 get_market_data_ex 获取历史数据。
    • count 设置为 vi_period * 2 + 5。这是因为计算 rolling(14).sum() 需要前14天的数据,而计算 shift(1) 又需要多一天,为了保证计算稳定,多取一些数据是安全的做法。
  3. 指标计算:

    • TR (真实波幅):取 High-Low|High-PrevClose||Low-PrevClose| 三者中的最大值。
    • VM (旋涡移动量)
      • VM+ 是当前最高价与前一最低价之差的绝对值。
      • VM- 是当前最低价与前一最高价之差的绝对值。
    • 归一化:将 VMTR 进行 14 周期求和,然后相除得到最终的 VI+VI-
  4. 信号生成:

    • 利用 pandasiloc[-1] (当前) 和 iloc[-2] (前一周期) 来检测交叉。
    • 金叉:前一刻 $VI^+ < VI^-$ 且 当前 $VI^+ > VI^-$。
    • 死叉:前一刻 $VI^+ > VI^-$ 且 当前 $VI^+ < VI^-$。
  5. 交易执行:

    • 使用 order_target_percent 函数。
    • 买入时设置目标仓位为 1.0(100% 持仓)。
    • 卖出时设置目标仓位为 0.0(0% 持仓,即清仓)。
    • 这种方式比直接用 passorder 更容易管理仓位,避免重复下单导致超仓。

Q&A

Q: 如何修改策略的运行周期(例如从日线改为15分钟线)?
A: 在 get_market_data_ex 函数中,将 period='1d' 修改为 period='15m' 即可。同时,请确保在 QMT 界面运行策略时,主图周期也调整为对应的周期,或者确保下载了对应的分钟级历史数据。

Q: 为什么使用 get_market_data_ex 而不是 get_market_data
A: get_market_data_ex 是 QMT 较新的接口,返回的数据结构(字典包含 DataFrame)更规范,处理多只股票时更方便,且在处理复权和停牌填充方面表现更好。

Q: 可以在实盘中直接使用这个代码吗?
A: 代码逻辑是完整的,但在实盘前建议:

  1. 填写真实的 account_id
  2. 在 QMT 的“模型研究”中进行回测,验证参数(如周期14)是否适合当前市场。
  3. 实盘建议增加风控逻辑(如止损、单笔最大下单金额限制),目前的 order_target_percent(..., 1.0) 是全仓买入,风险较高。