问题描述
解决方案
这是一个基于简易波动指标(Ease of Movement Value, EMV)的量化交易策略。
策略原理
EMV 指标由 Richard W. Arms, Jr. 开发,它结合了价格变动和成交量,用来衡量价格变动的“难易程度”。
- EMV 上升:代表价格在较少的成交量下上涨,意味着上涨阻力小。
- EMV 下降:代表价格在较少的成交量下下跌,意味着下跌阻力小。
本策略逻辑:
- 计算 EMV:计算 N 周期的 EMV 移动平均线(通常 N=14)。
- 买入信号:当 EMV 指标由下向上穿越 0 轴时,视为买入信号(多头趋势确立)。
- 卖出信号:当 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)
代码关键点说明
-
数据获取 (
get_market_data_ex):- 我们获取了
high,low,volume三个字段。 count设置为N + 10,是为了确保在使用rolling(window=N)计算移动平均时,最新的 K 线能有有效值。dividend_type='front'使用前复权数据,这在回测中非常重要,可以避免因分红派息导致的价格跳空影响指标计算。
- 我们获取了
-
EMV 计算细节:
- Volume Scale:代码中设置了
ContextInfo.volume_scale = 100000000。这是因为 A 股成交量通常较大,如果不进行缩放,Box Ratio 会非常大,导致 EMV 数值极小,不方便观察。 - 除零保护:在计算
Box Ratio和EMV时,分母(High-Low 或 Box Ratio)可能为 0(例如一字涨停板),代码使用了.replace(0, 0.00001)来防止程序报错。
- Volume Scale:代码中设置了
-
交易信号:
- 使用了经典的 0 轴交叉策略。
prev_emv <= 0 and current_emv > 0:金叉,买入。prev_emv >= 0 and current_emv < 0:死叉,卖出。
-
下单函数:
- 使用了
order_target_percent。这是一个非常方便的函数,它会自动计算需要买入或卖出的数量,将该股票的持仓调整到账户总资产的指定比例(代码中买入设为 1.0 即 100%,卖出设为 0.0 即清仓)。
- 使用了
如何使用
- 打开 QMT 客户端,进入“模型研究”或“策略编辑器”。
- 新建一个 Python 策略。
- 将上述代码复制粘贴进去。
- 修改
init函数中的ContextInfo.account_id为你实际的资金账号。 - 在右侧设置好回测参数(如起止时间、基准、费率等)。
- 点击“运行”或“回测”即可。
Q&A
Q: 为什么 EMV 指标有时候数值非常小?
A: EMV 的数值大小取决于价格单位和成交量单位的比例。如果成交量很大(如 A 股按手或股计算),Box Ratio 会很大,导致 EMV 很小。通常需要除以一个大的 Scale(如 1亿)来标准化。
Q: 这个策略适合什么周期的 K 线?
A: EMV 主要是设计用于日线(Daily)周期的。在分钟级别上,由于成交量和价格波动的不稳定性,EMV 的噪音可能会比较大,建议优先在日线上进行回测。
Q: 如何优化这个策略?
A: 单纯的 EMV 0 轴交叉可能产生较多假信号。可以考虑:
- 加入趋势过滤器(如只有在股价位于 60 日均线上方时才做多)。
- 结合其他指标(如 RSI 或 KDJ)进行共振确认。
- 调整 N 的参数(如改为 20 或 10)以适应不同波动率的个股。