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

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

第 1/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 文件就能编译:

MQL5 / C++
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 错误。

MQL5 / C++
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 判断更稳。

MQL5 / C++
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 就能跑。

MQL5 / C++
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">//+------------------------------------------------------------------+

常见问题

均线裸跑在趋势市能跟住,但在震荡市会频繁假突破来回打脸;原文回测显示震荡段胜率明显低于趋势段,建议加通道过滤。
用动态通道包住价格,只在价格触及通道上轨且均线同向时才认趋势,回调到通道内再找入场,比裸均线少很多噪音单。
小布可自动标注品种当前处于趋势还是震荡,并给出通道参数参考,省去你手动切周期试错的功夫。
不加时间戳每个 tick 都重算指标会拖慢执行甚至卡单;用时间过滤后每根K线只算一次,回调动量逻辑更稳。
可以,原文用句柄分开挂趋势和回归指标,靠市况判断切换下单分支,同一套回调入场逻辑复用即可。