3 分钟快速生成代码
输入想法,AI 即刻生成可运行代码
这是一个基于简易波动指标(Ease of Movement Value, EMV)的量化交易策略。
EMV 指标由 Richard W. Arms, Jr. 开发,它结合了价格变动和成交量,用来衡量价格变动的“难易程度”。
本策略逻辑:
# -*- 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 计算细节:
ContextInfo.volume_scale = 100000000。这是因为 A 股成交量通常较大,如果不进行缩放,Box Ratio 会非常大,导致 EMV 数值极小,不方便观察。Box Ratio 和 EMV 时,分母(High-Low 或 Box Ratio)可能为 0(例如一字涨停板),代码使用了 .replace(0, 0.00001) 来防止程序报错。交易信号:
prev_emv <= 0 and current_emv > 0:金叉,买入。prev_emv >= 0 and current_emv < 0:死叉,卖出。下单函数:
order_target_percent。这是一个非常方便的函数,它会自动计算需要买入或卖出的数量,将该股票的持仓调整到账户总资产的指定比例(代码中买入设为 1.0 即 100%,卖出设为 0.0 即清仓)。init 函数中的 ContextInfo.account_id 为你实际的资金账号。Q: 为什么 EMV 指标有时候数值非常小?
A: EMV 的数值大小取决于价格单位和成交量单位的比例。如果成交量很大(如 A 股按手或股计算),Box Ratio 会很大,导致 EMV 很小。通常需要除以一个大的 Scale(如 1亿)来标准化。
Q: 这个策略适合什么周期的 K 线?
A: EMV 主要是设计用于日线(Daily)周期的。在分钟级别上,由于成交量和价格波动的不稳定性,EMV 的噪音可能会比较大,建议优先在日线上进行回测。
Q: 如何优化这个策略?
A: 单纯的 EMV 0 轴交叉可能产生较多假信号。可以考虑: