🚀 AI 一键生成 qmt 策略代码

QMT 指标策略:简易波动指标 (EMV) 的计算与交易模型

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

问题描述

编写一个基于 EMV 指标的交易策略

解决方案

这是一个基于简易波动指标(Ease of Movement Value, EMV)的量化交易策略。

策略原理

EMV 指标由 Richard W. Arms, Jr. 开发,它结合了价格变动和成交量,用来衡量价格变动的“难易程度”。

  • EMV 上升:代表价格在较少的成交量下上涨,意味着上涨阻力小。
  • EMV 下降:代表价格在较少的成交量下下跌,意味着下跌阻力小。

本策略逻辑:

  1. 计算 EMV:计算 N 周期的 EMV 移动平均线(通常 N=14)。
  2. 买入信号:当 EMV 指标由下向上穿越 0 轴时,视为买入信号(多头趋势确立)。
  3. 卖出信号:当 EMV 指标由上向下穿越 0 轴时,视为卖出信号(空头趋势确立)。

策略代码

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

def init(ContextInfo):
    """
    初始化函数
    """
    # 设置策略参数
    ContextInfo.N = 14  # EMV的计算周期
    ContextInfo.volume_scale = 100000000 # 成交量缩放比例,用于调整Box Ratio的数量级
    
    # 设置股票池(此处示例为沪深300成分股,实际运行时可根据界面设置或此处修改)
    # ContextInfo.set_universe(['000300.SH']) 
    
    # 设置资金账号(请替换为您自己的资金账号)
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID' 
    ContextInfo.set_account(ContextInfo.account_id)
    
    print("EMV策略初始化完成")

def handlebar(ContextInfo):
    """
    K线周期运行函数
    """
    # 获取当前正在计算的股票代码
    stock_code = ContextInfo.stockcode
    
    # 获取当前K线索引
    index = ContextInfo.barpos
    
    # 确保有足够的数据进行计算 (N + 2 是为了计算移动平均和前一日对比)
    if index < ContextInfo.N + 2:
        return

    # 获取历史行情数据
    # 我们需要获取 N + 5 根K线以确保移动平均线的计算有值
    count = ContextInfo.N + 10
    
    # 使用 get_market_data_ex 获取数据 (推荐使用 ex 接口)
    # 字段需要:最高价(high), 最低价(low), 成交量(volume)
    market_data = ContextInfo.get_market_data_ex(
        ['high', 'low', 'volume'], 
        [stock_code], 
        period=ContextInfo.period, 
        count=count, 
        dividend_type='front', # 前复权
        fill_data=True, 
        subscribe=True
    )
    
    if stock_code not in market_data:
        return
        
    df = market_data[stock_code]
    
    # 如果数据长度不足,直接返回
    if len(df) < ContextInfo.N + 2:
        return

    # --- EMV 计算逻辑 ---
    
    # 1. 计算 Midpoint Move (MM) = (High + Low)/2 - (PrevHigh + PrevLow)/2
    # 当前最高最低均值
    hl_avg = (df['high'] + df['low']) / 2
    # 前一日最高最低均值
    prev_hl_avg = hl_avg.shift(1)
    mid_move = hl_avg - prev_hl_avg
    
    # 2. 计算 Box Ratio (BR) = (Volume / Scale) / (High - Low)
    # 高低价差
    hl_range = df['high'] - df['low']
    # 避免除以0的情况,将0替换为一个极小值
    hl_range = hl_range.replace(0, 0.00001)
    
    # Box Ratio
    box_ratio = (df['volume'] / ContextInfo.volume_scale) / hl_range
    
    # 3. 计算 EMV = MM / BR
    # 同样避免除以0
    box_ratio = box_ratio.replace(0, 0.00001)
    raw_emv = mid_move / box_ratio
    
    # 4. 计算 EMV 的移动平均线 (MAEMV)
    emv_ma = raw_emv.rolling(window=ContextInfo.N).mean()
    
    # 获取当前和上一根K线的 EMV 值
    current_emv = emv_ma.iloc[-1]
    prev_emv = emv_ma.iloc[-2]
    
    # 检查是否为 NaN (数据不足时可能产生)
    if np.isnan(current_emv) or np.isnan(prev_emv):
        return

    # --- 绘图 (可选,用于回测界面观察) ---
    # 在副图画出 EMV 线
    ContextInfo.paint('EMV', current_emv, -1, 0, 'yellow')
    # 画出 0 轴
    ContextInfo.paint('Zero', 0, -1, 0, 'gray')

    # --- 交易逻辑 ---
    
    # 获取当前持仓
    positions = ContextInfo.get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
    current_holding = 0
    for pos in positions:
        if pos.m_strInstrumentID == stock_code:
            current_holding = pos.m_nVolume
            break

    # 信号判断
    
    # 买入信号:EMV 上穿 0 轴 (前值 <= 0, 当前值 > 0)
    if prev_emv <= 0 and current_emv > 0:
        # 如果当前没有持仓,则全仓买入 (这里使用目标市值下单,1.0代表调整仓位至100%)
        # 注意:实盘中请根据风控需求调整仓位比例
        if current_holding == 0:
            print(f"{stock_code} - 触发买入信号: EMV上穿0轴 ({prev_emv:.4f} -> {current_emv:.4f})")
            order_target_percent(stock_code, 1.0, ContextInfo, ContextInfo.account_id)

    # 卖出信号:EMV 下穿 0 轴 (前值 >= 0, 当前值 < 0)
    elif prev_emv >= 0 and current_emv < 0:
        # 如果有持仓,则清仓
        if current_holding > 0:
            print(f"{stock_code} - 触发卖出信号: EMV下穿0轴 ({prev_emv:.4f} -> {current_emv:.4f})")
            order_target_percent(stock_code, 0.0, ContextInfo, ContextInfo.account_id)

代码关键点说明

  1. 数据获取 (get_market_data_ex)

    • 我们获取了 high, low, volume 三个字段。
    • count 设置为 N + 10,是为了确保在使用 rolling(window=N) 计算移动平均时,最新的 K 线能有有效值。
    • dividend_type='front' 使用前复权数据,这在回测中非常重要,可以避免因分红派息导致的价格跳空影响指标计算。
  2. EMV 计算细节

    • Volume Scale:代码中设置了 ContextInfo.volume_scale = 100000000。这是因为 A 股成交量通常较大,如果不进行缩放,Box Ratio 会非常大,导致 EMV 数值极小,不方便观察。
    • 除零保护:在计算 Box RatioEMV 时,分母(High-Low 或 Box Ratio)可能为 0(例如一字涨停板),代码使用了 .replace(0, 0.00001) 来防止程序报错。
  3. 交易信号

    • 使用了经典的 0 轴交叉策略。
    • prev_emv <= 0 and current_emv > 0:金叉,买入。
    • prev_emv >= 0 and current_emv < 0:死叉,卖出。
  4. 下单函数

    • 使用了 order_target_percent。这是一个非常方便的函数,它会自动计算需要买入或卖出的数量,将该股票的持仓调整到账户总资产的指定比例(代码中买入设为 1.0 即 100%,卖出设为 0.0 即清仓)。

如何使用

  1. 打开 QMT 客户端,进入“模型研究”或“策略编辑器”。
  2. 新建一个 Python 策略。
  3. 将上述代码复制粘贴进去。
  4. 修改 init 函数中的 ContextInfo.account_id 为你实际的资金账号。
  5. 在右侧设置好回测参数(如起止时间、基准、费率等)。
  6. 点击“运行”或“回测”即可。

Q&A

Q: 为什么 EMV 指标有时候数值非常小?
A: EMV 的数值大小取决于价格单位和成交量单位的比例。如果成交量很大(如 A 股按手或股计算),Box Ratio 会很大,导致 EMV 很小。通常需要除以一个大的 Scale(如 1亿)来标准化。

Q: 这个策略适合什么周期的 K 线?
A: EMV 主要是设计用于日线(Daily)周期的。在分钟级别上,由于成交量和价格波动的不稳定性,EMV 的噪音可能会比较大,建议优先在日线上进行回测。

Q: 如何优化这个策略?
A: 单纯的 EMV 0 轴交叉可能产生较多假信号。可以考虑:

  1. 加入趋势过滤器(如只有在股价位于 60 日均线上方时才做多)。
  2. 结合其他指标(如 RSI 或 KDJ)进行共振确认。
  3. 调整 N 的参数(如改为 20 或 10)以适应不同波动率的个股。