问题描述
解决方案
EMV (简易波动指标) 交易策略
EMV(Ease of Movement Value,简易波动指标)是由 Richard W. Arms Jr. 开发的一种技术分析指标。它通过结合价格变动和成交量来衡量价格运动的“难易程度”。
策略原理
EMV 的核心理念是:
- 价格上涨且成交量低:表示上涨阻力小,价格能够“轻松”移动,EMV 为正值。
- 价格下跌且成交量低:表示下跌支撑弱,价格能够“轻松”下跌,EMV 为负值。
- 成交量大但价格变动小:表示多空争夺激烈,移动困难,EMV 趋向于 0。
交易信号:
- 买入信号:EMV 指标(通常是 EMV 的移动平均线,如 EMV14)由下向上穿越 0 轴。
- 卖出信号:EMV 指标由上向下穿越 0 轴。
PTrade 策略代码实现
以下是一个完整的 PTrade 策略代码。该策略计算 14 日 EMV 均线,并在金叉 0 轴时买入,死叉 0 轴时卖出。
import numpy as np
import pandas as pd
def initialize(context):
"""
初始化函数,设置股票池和全局变量
"""
# 设置要操作的股票,这里以恒生电子为例
g.security = '600570.SS'
set_universe(g.security)
# EMV指标参数
g.N = 14 # EMV的平滑周期,通常取14天
g.scale = 10000 # 成交量缩放比例,用于调整数值大小,不影响正负方向
# 设置手续费(可选,回测时建议设置)
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
def handle_data(context, data):
"""
盘中运行函数,每日运行一次(日线级别)
"""
security = g.security
# 1. 获取历史数据
# 我们需要计算N日的MAEMV,且计算EMV需要用到前一日数据(diff),
# 所以获取 N + 10 天的数据以确保有足够的窗口进行计算
hist_len = g.N + 10
df = get_history(hist_len, frequency='1d', field=['high', 'low', 'volume'], security_list=security, fq='pre')
# 如果数据不足,直接返回
if len(df) < hist_len:
return
# 2. 计算 EMV 指标
# 提取数据序列
high = df['high']
low = df['low']
volume = df['volume']
# 2.1 计算 MidPoint Move (中间价变动)
# 公式: ((今日最高+今日最低)/2 - (昨日最高+昨日最低)/2)
mid_point = (high + low) / 2
mid_move = mid_point.diff() # 计算与前一日的差值
# 2.2 计算 Box Ratio (箱体比率)
# 公式: (成交量 / Scale) / (今日最高 - 今日最低)
# 注意:为了防止除以0(当最高价等于最低价时),将0替换为一个极小值
price_range = high - low
price_range = price_range.replace(0, 0.0001)
box_ratio = (volume / g.scale) / price_range
# 2.3 计算 1日 EMV
# 公式: MidPoint Move / Box Ratio
emv_1d = mid_move / box_ratio
# 2.4 计算 MAEMV (EMV的移动平均线)
# 通常使用 N 周期的简单移动平均
ma_emv = emv_1d.rolling(window=g.N).mean()
# 3. 获取当前和前一日的指标值用于判断交叉
# iloc[-1] 为今日值(回测中为当前回测日),iloc[-2] 为昨日值
curr_emv = ma_emv.iloc[-1]
prev_emv = ma_emv.iloc[-2]
# 检查是否计算出有效值(剔除NaN)
if np.isnan(curr_emv) or np.isnan(prev_emv):
return
# 4. 交易逻辑
# 获取当前持仓
position = get_position(security).amount
cash = context.portfolio.cash
# 买入信号:EMV 均线向上突破 0 轴
if prev_emv < 0 and curr_emv > 0:
if cash > 0:
# 全仓买入
order_value(security, cash)
log.info("EMV上穿0轴,买入 %s,当前EMV: %.4f" % (security, curr_emv))
# 卖出信号:EMV 均线向下跌破 0 轴
elif prev_emv > 0 and curr_emv < 0:
if position > 0:
# 清仓卖出
order_target(security, 0)
log.info("EMV下穿0轴,卖出 %s,当前EMV: %.4f" % (security, curr_emv))
代码解析
-
数据获取 (
get_history):- 我们需要
high(最高价),low(最低价), 和volume(成交量) 来计算 EMV。 - 获取长度设置为
g.N + 10,是为了确保在使用rolling(window=g.N)计算移动平均时,前面有足够的数据填充,避免结果为 NaN。
- 我们需要
-
指标计算逻辑:
- MidPoint Move (中间价移动):衡量价格重心的移动方向。
- Box Ratio (箱体比率):衡量成交量相对于价格波动范围的比率。如果成交量很大但价格波动很小(Box Ratio 大),说明移动困难;反之则移动容易。
- Scale (缩放因子):代码中
g.scale = 10000。这是为了让 Box Ratio 的数值不至于过大,从而使最终的 EMV 数值更易读。这个参数不改变指标的正负方向,只改变数值的绝对大小。 - 防除零处理:在计算
price_range时,如果当日最高价等于最低价(如一字涨跌停),分母为 0 会导致报错或无穷大。代码使用了.replace(0, 0.0001)进行保护。
-
交易信号:
- 使用
prev_emv < 0 and curr_emv > 0判断上穿 0 轴。 - 使用
prev_emv > 0 and curr_emv < 0判断下穿 0 轴。
- 使用
策略优化建议
EMV 是一个趋势跟随指标,在震荡市中可能会频繁发出虚假信号。在实际应用中,可以考虑以下优化方向:
- 结合趋势指标:例如,只有当股价在 60 日均线之上时,才执行 EMV 的买入信号(过滤掉下跌趋势中的反弹)。
- 调整周期:默认 14 天是中期参数。如果是短线交易,可以尝试缩短周期(如 9 天);如果是长线,可以延长周期。
- 止损机制:EMV 反应的是“轻松程度”,并不直接指示价格支撑位。建议加入固定比例止损(如亏损 5% 止损)或跟踪止损。
Q&A
Q1: 为什么计算 Box Ratio 时要除以 Scale (10000)?
A1: 在股票市场中,成交量(Volume)通常是数百万甚至上亿股,而价格波动(High - Low)通常只有几块钱。直接相除会导致 Box Ratio 数值极大,进而导致 EMV 数值极小(接近 0)。除以 Scale 是为了调整量纲,让 EMV 的读数更符合人类阅读习惯,它不会改变指标的形态和交叉信号。
Q2: EMV 指标适合什么类型的股票?
A2: EMV 比较适合股性活跃、成交量变化明显的股票。对于长期横盘、成交量极度低迷或者被高度控盘(成交量失真)的股票,EMV 的参考意义较小。
Q3: 代码中的 fq='pre' 是什么意思?
A3: fq='pre' 表示使用前复权数据。在量化回测中,为了消除分红、配股、拆股等对价格造成的跳空影响,必须使用复权数据。前复权可以保证当前价格与实际盘面一致,是回测中最常用的复权方式。