构建自优化型MQL5智能交易系统(EA)(第3部分):动态趋势跟踪与均值回归策略(基础篇)
「用动态通道替代均线裸跑」
均线策略在趋势行情里能跟住长期方向,但盘整阶段容易反复挨耳光。多数文章把市场切成「趋势」和「震荡」两块分别写策略,实际行情是两者持续交替,不会长期钉死在某一种状态。 我们想做的不是两套系统,而是一套能自己嗅出状态切换的单一策略。核心是一个滚动更新的交易通道:上轨 = 均线均值 + ATR倍数,下轨 = 均线均值 - ATR倍数,靠价格相对边界的位置来卡进出场。 先跑最朴素的版本验证基线:EURUSD M1、4年回测,收盘价上穿100周期均线做多、下穿做空,固定止损止盈。结果只有52%的交易盈利。 同样周期同样行情,叠加上面那个动态通道规则后,盈利交易占比跳到86%,没用任何过拟合或AI黑箱。外汇和高杠杆贵金属本身风险极高,这个胜率只说明「估准状态边界」比硬套分类值钱,不代表实盘必赢。 通道每天用MT5自带指标重算,代码不复杂,你可以直接把模板扒下来,按自己对节奏的理解改ATR倍数或均线周期。
◍ 均线策略在两种市况下的真实表现
以 EURUSD 日线图为例,2016年11月下旬至2018年4月走出一段强劲牛市,价格始终维持在移动平均线上方运行,均线清晰确认了长期上行趋势。这种市况下,收盘价上穿均线做多、下穿做空的规则能稳定捕获趋势收益。 但把时间轴换到震荡行情(如图2所示的横盘区间),简单均线穿越系统会反复出现假突破,进入长期亏损期。市场没有单边方向时,静态阈值跟不上价格来回抽刺。 本文要做的改造,是让均线系统不依赖额外指标或手动切换,就能在趋势与震荡间动态自适应。核心思路是把固定均线周期改为随波动调节,下一节直接给代码。
搭一个均线趋势系统的骨架
一套可回测的 MQL5 交易程序,先把模块边界划清:系统常量锁死回测行为,库文件只挂交易库管开仓平仓,全局变量存指标与价格,事件处理器(OnInit/OnDeinit/OnTick)承接终端与服务器推送,自定义函数写策略逻辑。常量在后续版本迭代中保持不变,这是避免主观偏差改乱交易逻辑的底线的做法。
交易库只引 <Trade\Trade.mqh>,用 CTrade Trade; 实例化后,持仓管理不用自己造轮子。全局变量放市场价格和指标计算结果,供各模块共享,比在函数内反复计算更省。
事件触发分两类:用户操作(如加载/移除 EA)和交易服务器推送(新报价)。OnInit 在启动时跑初始化函数;OnDeinit 在移除 EA 时释放内存;OnTick 由服务器每次推新价触发,无持仓就找开仓机会。
开仓规则很直白:价格站上 100 周期均线(代码中 TRADING_MA_PERIOD 定义为 100)做多,跌破做空,止损宽度由 SL_WIDTH=1.5 控制,固定手数 CURRENT_VOL=0.1。外汇与贵金属杠杆高,这类固定参数实盘前须在 MT5 策略测试器跑历史数据验证,参数失效概率不低。
下面这段是系统常量和库引用的当前实现,直接拷进 MQ5 文件就能编译:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Dynamic Moving Average Strategy.mq5 | class=class="str">"cmt">//| Gamuchirai Ndawana | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Gamuchirai Ndawana" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| System constants | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define MA_PRICE PRICE_CLOSE class=class="str">"cmt">//Moving average applied price class="macro">#define MA_MODE MODE_EMA class=class="str">"cmt">//Moving average type class="macro">#define MA_SHIFT class="num">0 class=class="str">"cmt">//Moving average shift class="macro">#define ATR_PERIOD class="num">14 class=class="str">"cmt">//Period for our ATR class="macro">#define TF_1 PERIOD_D1 class=class="str">"cmt">//Hihger order timeframe class="macro">#define TRADING_MA_PERIOD class="num">100 class=class="str">"cmt">//Moving average period class="macro">#define SL_WIDTH class="num">1.5 class=class="str">"cmt">//How wide should the stop loss be? class="macro">#define CURRENT_VOL class="num">0.1 class=class="str">"cmt">//Trading volume class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Libraries | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Trade\Trade.mqh> CTrade Trade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+------------------------------------------------------------------+
「EA 骨架:从指标句柄到回调动量的下单逻辑」
这段 EA 主结构把均线、ATR 两个句柄和价格数组挂在全局,初始化时只做 setup(),退出时释放三个指标句柄避免内存泄漏。OnTick 里先 update() 刷新数组,无持仓才去 find_setup() 找机会。 find_setup() 的规则极简:当前收盘价站上 trading_ma[0] 就 Buy,跌破就 Sell,止损止盈都用 atr[0] * SL_WIDTH 对称展开。外汇和贵金属杠杆高,这种裸动量回调策略在震荡市可能连续被打损,实盘前务必在 MT5 策略测试器跑至少 3 个月 Tick 数据。 setup() 中 atr_handler 用 iATR(Symbol(),TF_1,ATR_PERIOD),trading_ma_handler 用 iMA 取 PERIOD_CURRENT 下的 TRADING_MA_PERIOD 周期均线。把 TF_1 和 TRIOD 拆成不同周期,是后续做多周期过滤的预留接口。 别把句柄当普通变量 指标句柄在 OnInit 创建、OnDeinit 释放,若遗漏 IndicatorRelease 会导致 MT5 终端指标实例堆积,回测样本超过 200 次后可能报 handle limit 错误。
class="type">int ma_handler,trading_ma_handler,atr_handler; class="type">class="kw">double ma[],atr[],trading_ma[]; class="type">class="kw">double ask,bid; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- setup(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- IndicatorRelease(atr_handler); IndicatorRelease(ma_handler); IndicatorRelease(trading_ma_handler); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- update(); if(PositionsTotal() == class="num">0) find_setup(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Find setup | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void find_setup(class="type">void) { class=class="str">"cmt">//Buy on rallies if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > trading_ma[class="num">0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[class="num">0] * SL_WIDTH)),(bid + (atr[class="num">0] * SL_WIDTH)),""); if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < trading_ma[class="num">0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[class="num">0] * SL_WIDTH)),(ask - (atr[class="num">0] * SL_WIDTH)),""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Setup our global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void setup(class="type">void) { atr_handler = iATR(Symbol(),TF_1,ATR_PERIOD); trading_ma_handler = iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update | class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用时间戳卡住指标刷新节奏
在 EA 的 update 函数里,最忌讳每帧都去拉 ATR 和均线的缓冲区——那样既浪费资源又容易在跨根瞬间读到半截数据。下面这段逻辑用静态变量 time_stamp 记住上一根 K 线时间,只有当前柱时间变了才 CopyBuffer,相当于每根柱只采一次样。 具体看,current_time 取当前图表 0 号柱的开盘时间(iTime + PERIOD_CURRENT),ask/bid 则每次都刷新不算贵。当 time_stamp != current_time 成立,说明新柱刚成型,这时才把 atr_handler 和 trading_ma_handler 的 0 号缓冲各拷 1 根进数组,随后更新时间戳。 配套常量里把参数钉死了:ATR 周期 14、交易均线周期 100、日线做高阶趋势过滤(TF_1 = PERIOD_D1)、止损宽度 1.5 倍 ATR、固定手数 0.1。外汇和贵金属杠杆高,1.5 倍 ATR 止损在镑美可能扫得勤,在黄金可能不够宽,实盘前建议在 MT5 策略测试器按品种调 SL_WIDTH。 别把静态变量当万能锁 静态 time_stamp 只在同品种同周期有效;若 EA 被拖到别的周期,PERIOD_CURRENT 变了但 time_stamp 残留旧值,可能漏掉第一根更新。切图表后手动重置或加 Symbol 判断更稳。
class="type">void update(class="type">void) { class="kw">static class="type">class="kw">datetime time_stamp; class="type">class="kw">datetime current_time = iTime(Symbol(),PERIOD_CURRENT,class="num">0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(atr_handler,class="num">0,class="num">0,class="num">1,atr); CopyBuffer(trading_ma_handler,class="num">0,class="num">0,class="num">1,trading_ma); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//class="macro">#class="kw">property copyright "Gamuchirai Ndawana" class=class="str">"cmt">//class="macro">#class="kw">property link "[MQL5官方文档] class=class="str">"cmt">//class="macro">#class="kw">property version "class="num">1.00" class="macro">#define MA_PRICE PRICE_CLOSE class="macro">#define MA_MODE MODE_EMA class="macro">#define MA_SHIFT class="num">0 class="macro">#define ATR_PERIOD class="num">14 class="macro">#define TF_1 PERIOD_D1 class="macro">#define TRADING_MA_PERIOD class="num">100 class="macro">#define SL_WIDTH class="num">1.5 class="macro">#define CURRENT_VOL class="num">0.1 class="macro">#include <Trade\Trade.mqh> CTrade Trade;
把均线回调逻辑塞进 EA 骨架
上面这段是整套策略的运行时容器:全局句柄在 OnInit 里通过 setup() 绑定 ATR 与交易均线,OnDeinit 里统一释放,避免 MT5 重复加载指标时句柄泄漏。 OnTick 只做两件事——update() 刷新数组,无持仓时调 find_setup() 找信号。也就是说,每根新 tick 都会先确认当前没有未平仓位,才允许开单,天然屏蔽了加仓堆叠。 find_setup() 的规则很直白:当根收盘价站上 trading_ma[0] 就 Buy,跌破就 Sell,止损止盈均按 atr[0] * SL_WIDTH 对称展开。例如 SL_WIDTH=2、ATR 现值 15 点时,止损止盈各距进场 30 点,外汇与贵金属波动下这套距离可能被瞬间扫掉,属高风险玩法,参数需自行回测。 setup() 里 iATR 用外部 TF_1 周期、iMA 用当前周期,两者周期错配是刻意的——用更高周期 ATR 控风险、用本周期均线抓入场。开 MT5 把这段贴进 EA,改 CURRENT_VOL 和 SL_WIDTH 就能跑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int ma_handler,trading_ma_handler,atr_handler; class="type">class="kw">double ma[],atr[],trading_ma[]; class="type">class="kw">double ask,bid; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- setup(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- IndicatorRelease(atr_handler); IndicatorRelease(ma_handler); IndicatorRelease(trading_ma_handler); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- update(); if(PositionsTotal() == class="num">0) find_setup(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Find setup | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void find_setup(class="type">void) { class=class="str">"cmt">//Buy on rallies if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > trading_ma[class="num">0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[class="num">0] * SL_WIDTH)),(bid + (atr[class="num">0] * SL_WIDTH)),""); if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < trading_ma[class="num">0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[class="num">0] * SL_WIDTH)),(ask - (atr[class="num">0] * SL_WIDTH))); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Setup our global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void setup(class="type">void) { atr_handler = iATR(Symbol(),TF_1,ATR_PERIOD); trading_ma_handler = iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); } class=class="str">"cmt">//+------------------------------------------------------------------+