构建自优化型MQL5智能交易系统(EA)(第3部分):动态趋势跟踪与均值回归策略·进阶篇
📈

构建自优化型MQL5智能交易系统(EA)(第3部分):动态趋势跟踪与均值回归策略·进阶篇

(2/3)·均线在盘整期反复打脸?这篇进阶篇教你用ATR通道让EA自己判别趋势与震荡边界

案例拆解 第 2/3 篇

多数EA把市场硬切成「趋势」或「震荡」两套逻辑,结果在状态切换的灰区频繁止损。本文承接上篇,继续深挖如何用单一动态策略替代分体式范式,让系统自主识别边界迁移。

◍ 用静态时间戳卡住重算节拍

这段 update 函数的核心目的,是把 ATR 和交易均线的缓冲区拷贝动作,限制在一根 K 线只跑一次,而不是每个 tick 都去捞数据。 它用 static datetime time_stamp 记住上一次处理过的柱头时间,再用 iTime(Symbol(),PERIOD_CURRENT,0) 取当前柱时间做比对。两者不等才进 if 块,重新写 time_stamp 并 CopyBuffer 拿 atr 与 trading_ma。 ask 和 bid 则放在判断外,每个 tick 都刷新,保证报价始终最新。外汇与贵金属杠杆高,这种「慢指标 + 快报价」的拆法,能压住不必要的计算开销,你在 MT5 里接自己 EA 时可以直接套这个骨架。

MQL5 / C++
class=class="str">"cmt">//| Update                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
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">//+------------------------------------------------------------------+

「用五年 Tick 数据给趋势策略定基准」

拿 EURUSD 的 M1 历史数据跑一遍 2020-01-01 到 2025-01-06 的回测,策略参数不做优化,Forward 模式直接关成 No。回测模型选真实 Tick 并开了随机延迟,目的就是把网络抖动那点事模拟进生产环境,不然曲线好看也没参考意义。 外汇与贵金属属高风险品种,回测贴近实盘不等于实盘能复现,这点心里要有数。 五年跑下来资金曲线整体是正收益,但峰值资金和终值差得明显,盈利段短、回撤段差不多长,碰上无趋势行情就一直吐。胜率接近 50/50,平均连盈 2 次、连亏也 2 次,说明这系统盈亏概率基本对半开。 没人盯盘别直接上实盘,这套东西目前只是个基准线,后面要干的活是过滤掉那一半亏损交易。

用四区划分让均线策略自识别趋势与震荡

初始回测暴露了原策略的软肋:趋势市能盈利,无趋势市持续亏损。改进的核心不是换指标,而是让系统先判定市场处于趋势还是震荡,再决定顺均线还是逆均线操作。 同一事件——价格上穿100周期均线——在震荡模式下去做空,在趋势模式下去做多。解读完全由系统对市场模式的判定驱动,而不是固定信号。 具体落地是把价格区间切成4个离散区域:区域1、4可能走真实趋势,区域2、3倾向区间震荡。区域1只看收盘价高于100周期均线后的做多机会,禁空;区域2价格跌入后只做空,上穿均线即卖,止盈放在区域2-3分界中轴;区域3对称只做多,下破均线买,止盈同设中轴;区域4只做空,价格持续位于均线下方才出手。 每个区域强制单方向,避免原策略在震荡里反复挨耳光。要跑这套,需在原趋势跟踪代码里加通道变量、开放通道宽度与计算K线数给用户、补全局变量并改函数——通道算法下节展开。外汇与贵金属波动剧烈,区域误判会带来连续止损,实盘前务必在MT5用历史数据验证。

◍ 多周期通道的区域判定与代码拆解

做通道策略时,若直接在交易周期(如 M1)上算均线,通道会随同周期噪声剧烈抖动,策略稳定性差。实操中先把 20 周期 MA 挂到日线,用它的数值当基准中轴,再叠加 ATR 倍数往外扩——本例通道宽度锁为 2 倍 ATR,因多数品种单日波动接近 1 倍 ATR,这样能滤掉短期毛刺。 区域划分靠四个边界:区域1在中轴上界之上无顶;区域2介于中轴与上界之间;区域3介于中轴与下界之间;区域4在下界之下无底。全局变量 range_zone_mid / ub / lb 实时存这三根线,active_zone 标记价格落点。 回测对照下,新结构把交易次数从 300 笔拉到 1301 笔,盈利率由 52% 升到 86%,连亏次数从 2 次降到 1 次。外汇与贵金属属高风险品种,这类参数优化只代表历史样本表现,实盘可能偏离。 下面这段 MQL5 是区域识别核心,逐行看逻辑: #define MA_PERIOD 20 // 定义日线中轴用的均线周期常量 double range_zone_mid,range_zone_ub,range_zone_lb; // 中轴、上界、下界三个全局双精度变量 int active_zone; // 当前区域编号 1~4 input double atr_multiple = 2; // 用户输入:ATR 倍数,决定通道宽度 input int bars_used = 30; // 用户输入:计算通道取多少根 K 线 void get_active_zone(void) // 无参函数:刷新 active_zone if(iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_ub) // 收盘价高于上界 active_zone = 1; return; // 判为区域1并退出 if(收盘价<上界 && 收盘价>中轴) // 夹在中轴和上界间 active_zone = 2; return; // 区域2 if(收盘价<中轴 && 收盘价>下界) // 夹在中轴和下界间 active_zone = 3; return; // 区域3 if(收盘价<下界) // 跌破下界 active_zone = 4; return; // 区域4 开 MT5 把这段塞进 EA,改 atr_multiple 从 2 调到 1.5,能直观看到通道变窄、区域2/3 触发更频繁,可验证自己对区域边界的理解。

MQL5 / C++
class="macro">#define MA_PERIOD class="num">20 class=class="str">"cmt">//Moving Average period for calculating the mid point of our range zone
class="type">class="kw">double range_zone_mid,range_zone_ub,range_zone_lb;
class="type">int active_zone;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| User inputs |
class=class="str">"cmt">//+------------------------------------------------------------------+
input group "Technical Analysis"
input class="type">class="kw">double atr_multiple = class="num">2 ; class=class="str">"cmt">//ATR Multiple
input class="type">int bars_used = class="num">30; class=class="str">"cmt">//How Many Bars should we use to calculate the channel?
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get our current active zone |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void get_active_zone(class="type">void)
 {
 if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > range_zone_ub)
 {
 active_zone = class="num">1;
 class="kw">return;
 }
 if((iClose(Symbol(),PERIOD_CURRENT,class="num">0) < range_zone_ub) && (iClose(Symbol(),PERIOD_CURRENT,class="num">0) > range_zone_mid))
 {
 active_zone = class="num">2;
 class="kw">return;
 }
 if((iClose(Symbol(),PERIOD_CURRENT,class="num">0) < range_zone_mid) && (iClose(Symbol(),PERIOD_CURRENT,class="num">0) > range_zone_lb))
 {
 active_zone = class="num">3;
 class="kw">return;
 }
 if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < range_zone_lb)
 {
 active_zone = class="num">4;
 class="kw">return;
 }
 }

「用均线中轨和 ATR 画出波动区间」

这套逻辑把「区间中轨」锚在 MA 均值上,上下边界再用 ATR 倍数外扩,实盘里能直接看到价格被夹在三条横线之间。 setup() 只在初始化时拿一次 iMA 句柄,周期、偏移、均价类型都由外部宏控制,后面 update() 不再重复申请指标资源。 update() 用 static datetime 做时间戳闸门:只有当前 K 线时间变了才重算区间,避免每 tick 都跑向量运算。ma_average 取 1 行向量、CopyIndicatorBuffer 从 buffer0 拷 bars_used 根,再 Mean() 出中轨;上界 = 中轨 + ATR[0]*atr_multiple,下界对称减。 画线部分先 ObjectDelete 清掉旧名,再 ObjectCreate 三条 OBJ_HLINE,名字固定为 RANGE HIGH/MID/LOW,图表上就能肉眼确认区间是否合理。外汇和贵金属波动剧烈,ATR 倍数设太大容易踏空、太小则假突破频发,建议先在 MT5 用 1.5~2.5 倍回看 EURUSD 的 M15。 find_setup() 里 active_zone==1 视为顺区,只在收盘价高于 trading_ma[0] 时 Buy,止损放在 bid - atr*1.5、止盈 bid + atr*SL_WIDTH,属于「区间内逢回调做多」的雏形;active_zone==2 的反向分支原文未截完,但结构已经清楚。

MQL5 / C++
class=class="str">"cmt">//| Setup our global variables                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void setup(class="type">void)
  {
  class=class="str">"cmt">//We have omitted parts of the code that have not changed
  ma_handler  = iMA(Symbol(),TF_1,MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
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)
    {
    class=class="str">"cmt">//Omitted parts of the function that remained unchanged
    vector ma_average = vector::Zeros(class="num">1);
    ma_average.CopyIndicatorBuffer(ma_handler,class="num">0,class="num">1,bars_used);
    range_zone_mid = ma_average.Mean();
    range_zone_ub = (range_zone_mid + (atr[class="num">0] * atr_multiple));
    range_zone_lb = (range_zone_mid - (atr[class="num">0] * atr_multiple));
    get_active_zone();
    Comment("Zone: ",active_zone);
    ObjectDelete(class="num">0,"RANGE HIGH");
    ObjectDelete(class="num">0,"RANGE LOW");
    ObjectDelete(class="num">0,"RANGE MID");
    ObjectCreate(class="num">0,"RANGE MID",OBJ_HLINE,class="num">0,class="num">0,range_zone_mid);
    ObjectCreate(class="num">0,"RANGE LOW",OBJ_HLINE,class="num">0,class="num">0,range_zone_lb);
    ObjectCreate(class="num">0,"RANGE HIGH",OBJ_HLINE,class="num">0,class="num">0,range_zone_ub);
    }
  }
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">// Follow the trend
  if(active_zone == class="num">1)
    {
    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] * class="num">1.5)),(bid + (atr[class="num">0] * SL_WIDTH)),"");
    }
class=class="str">"cmt">// Go against the trend
  if(active_zone == class="num">2)
    {

逆势与顺势区的挂单触发逻辑

当价格进入第 3 区(逆势区)且当前收盘价跌破 100 周期交易均线时,系统倾向反向买入博反弹;此时止损挂在买价下方 1.5 倍 ATR 处,止盈则看向震荡区中轨。 第 4 区(顺势区)的触发条件相反:收盘价低于交易均线时执行顺势做空,即「sell the dip」。这里止损宽度由 SL_WIDTH 控制,止盈挂在 ask 减去 ATR 乘 SL_WIDTH 的位置,ATR 倍数则由 atr_multiple 参数决定,而非固定 1.5。 第 2 区(原文前段遗留的反弹做空逻辑)用固定 1.5 倍 ATR 做止损、震荡区中轨做止盈,与第 4 区的可变倍数形成对照。外汇与贵金属杠杆高,这类区间策略在趋势切换时可能连续触发逆势单,实盘前务必在 MT5 用策略测试器跑一遍参数敏感性。 代码里 ATR 周期定为 14、交易均线周期 100、区间中轨用 20 周期 EMA,这些常数直接写在预处理指令里,改起来只需动 #define 不必碰主逻辑。

MQL5 / C++
class=class="str">"cmt">//Sell on rallies
   if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > trading_ma[class="num">0])
      Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[class="num">0] * class="num">1.5)),range_zone_mid);
   }
class=class="str">"cmt">// Go against the trend
   if(active_zone == class="num">3)
   {
   class=class="str">"cmt">//Buy the dip
      if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < trading_ma[class="num">0])
         Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[class="num">0] * class="num">1.5)),range_zone_mid,"");
   }
class=class="str">"cmt">// Follow the trend
   if(active_zone == class="num">4)
   {
   class=class="str">"cmt">//Sell the dip
      if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < trading_ma[class="num">0])
         Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[class="num">0] * atr_multiple)),(ask - (atr[class="num">0] * SL_WIDTH)));
   }
}
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 shift
class="macro">#define MA_MODE     MODE_EMA            class=class="str">"cmt">//Moving average shift
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 MA_PERIOD   class="num">20                  class=class="str">"cmt">//Moving Average period for calculating the mid point of our range zone
class="macro">#define TRADING_MA_PERIOD   class="num">100          class=class="str">"cmt">//Moving average period
把状态切换监控交给小布
通道上下轨随ATR滚动更新的计算过程,小布盯盘已内置为品种页的背景诊断,打开EURUSD即可看到当前处于趋势带还是震荡带,你只管定规则。

常见问题

均线对长期趋势贴合好,但价格无序来回穿越均线时会产生大量假突破信号,触发无效开仓,导致亏损单堆积。
上轨等于移动平均线均值加ATR的若干倍,下轨等于均值减ATR的若干倍,倍数与均线周期可按品种波动特征调整,通道每日重算。
原文回测仅用MT5内置指标与简洁结构,未引入机器学习或参数寻优,属于规则显式、可解释的动态边界判定,过拟合概率低。
小布盯盘目前提供通道状态可视化与告警,EA本体需在MT5自行编译部署;把重复劳动交给小布,你专注决策与参数微调。
同一周期同段行情对比,仅加入通道过滤就改变胜率,说明边界识别有效;但外汇高风险,实盘仍可能因点差滑点衰减,倾向降低预期。
重点监测价格相对通道边界的位置与ATR倍数收敛程度,当收盘价频繁回踩中轨且通道宽度收窄,市场倾向进入震荡机制。