使用MQL5开发基于震荡区间突破策略的EA·进阶篇
📈

使用MQL5开发基于震荡区间突破策略的EA·进阶篇

(2/3)·从区间识别到回测验证,手把手把震荡突破逻辑落成可跑的MT5智能交易系统

实战向 第 2/3 篇
很多人把价格碰一下区间边界就当突破开仓,结果反复被洗。震荡区间突破真正要看的是收盘越过边界,而非影线虚晃。用错判定条件,EA回测再漂亮也扛不住实盘噪音。

「用矩形把震荡区间画出来」

在 MT5 上做价格行为分析,第一步往往是把一段收敛行情可视化。下面这段逻辑的核心,是先判断当前是否存在已标记的区间,若没有且来了新 K 线,就去抓取最近 rangeBars 根 K 线的高低点,再按点数阈值决定是否算作有效震荡。 关键判断在这一行:isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints。也就是说,区间净高度换算成点数后,必须小于等于你设定的 rangeSizePoints,才认为是可绘制的整固带。外汇与贵金属波动大,这个点数参数设太窄会漏掉真实整理,设太宽又容易把趋势中段也框进去,属于高风险调参。 一旦 isInRange 为真,就调用 plotConsolidationRange 在图上拉一个蓝色填充矩形,并把 isRangeExist 置真、向日志打印 RANGE PLOTTED。你直接把下面代码贴进 EA 或脚本,改一下 rangeBars 和 rangeSizePoints,就能在 EURUSD 的 M15 上验证框选效果。

MQL5 / C++
if (isRangeExist == class="kw">false && isNewBar){ class=class="str">"cmt">// If no range exists and a new bar has appeared
    ...
}
    TIME1_X1 = iTime(_Symbol,_Period,rangeBars); class=class="str">"cmt">// Get the start time of the range
    class="type">int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,class="num">1); class=class="str">"cmt">// Get the bar index with the highest high in the range
    PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); class=class="str">"cmt">// Get the highest high price in the range
    TIME2_Y2 = iTime(_Symbol,_Period,class="num">0); class=class="str">"cmt">// Get the current time
    class="type">int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,class="num">1); class=class="str">"cmt">// Get the bar index with the lowest low in the range
    PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); class=class="str">"cmt">// Get the lowest low price in the range
    isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; class=class="str">"cmt">// Check if the range size is within the allowed points
    if (isInRange){ class=class="str">"cmt">// If the range size is valid
       plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); class=class="str">"cmt">// Plot the consolidation range
       isRangeExist = true; class=class="str">"cmt">// Set the range exist flag to true
       Print("RANGE PLOTTED"); class=class="str">"cmt">// Print a message indicating the range is plotted
    }
class="type">void plotConsolidationRange(class="type">class="kw">string rangeName,class="type">class="kw">datetime time1_x1,class="type">class="kw">double price1_y1,
    class="type">class="kw">datetime time2_x2,class="type">class="kw">double price2_y2){
    if (ObjectFind(class="num">0,rangeName) < class="num">0){ class=class="str">"cmt">// If the range object does not exist
       ObjectCreate(class="num">0,rangeName,OBJ_RECTANGLE,class="num">0,time1_x1,price1_y1,time2_x2,price2_y2); class=class="str">"cmt">// Create the range object
       ObjectSetInteger(class="num">0,rangeName,OBJPROP_COLOR,clrBlue); class=class="str">"cmt">// Set the class="type">color of the range
       ObjectSetInteger(class="num">0,rangeName,OBJPROP_FILL,true); class=class="str">"cmt">// Enable fill for the range

◍ 用新K线触发区间重绘

在 MT5 里做整理区间标注,核心是不能每 tick 都重画,否则图表对象闪烁且占用主线程。上面这段逻辑用静态变量 prevBars 缓存上一 tick 的 bar 数,只有 iBars 返回值变了才把 isNewBar 置真,等于用「新柱出现」做节流开关。 新柱确认后,代码取最近 rangeBars 根 K 线:iHighest 拿最高价所在 bar 索引,iHigh 取该价;iLowest 与 iLow 对称取最低。两者差除以 _Point 若 ≤ rangeSizePoints,才判定为有效窄幅区间,调用 plotConsolidationRange 画矩形。外汇与贵金属波动随机,rangeSizePoints 设太小可能长期不触发,设太大则失去整理意义,需按品种点值回测。 若区间对象已存在,就不新建,而是用 ObjectSetInteger / ObjectSetDouble 改起止时间与高低价,最后 ChartRedraw(0) 刷新。下面这段是更新分支与 OnTick 节流的原文,可直接拷进 EA 验证。

MQL5 / C++
   ObjectSetInteger(class="num">0,rangeName,OBJPROP_WIDTH,class="num">5); class=class="str">"cmt">// Set the width of the range
   }
   else { class=class="str">"cmt">// If the range object exists
      ObjectSetInteger(class="num">0,rangeName,OBJPROP_TIME,class="num">0,time1_x1); class=class="str">"cmt">// Update the start time of the range
      ObjectSetDouble(class="num">0,rangeName,OBJPROP_PRICE,class="num">0,price1_y1); class=class="str">"cmt">// Update the high price of the range
      ObjectSetInteger(class="num">0,rangeName,OBJPROP_TIME,class="num">1,time2_x2); class=class="str">"cmt">// Update the end time of the range
      ObjectSetDouble(class="num">0,rangeName,OBJPROP_PRICE,class="num">1,price2_y2); class=class="str">"cmt">// Update the low price of the range
   }
   ChartRedraw(class="num">0); class=class="str">"cmt">// Redraw the chart to reflect changes
}

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(){
class=class="str">"cmt">//---

   class="type">int currBars = iBars(_Symbol,_Period); class=class="str">"cmt">// Get the current number of bars
   class="kw">static class="type">int prevBars = currBars; class=class="str">"cmt">// Static variable to store the previous number of bars
   class="kw">static class="type">bool isNewBar = class="kw">false; class=class="str">"cmt">// Static flag to check if a new bar has appeared
   if (prevBars == currBars){isNewBar = class="kw">false;} class=class="str">"cmt">// Check if the number of bars has not changed
   else {isNewBar = true; prevBars = currBars;} class=class="str">"cmt">// If the number of bars has changed, set isNewBar to true and update prevBars

   if (isRangeExist == class="kw">false && isNewBar){ class=class="str">"cmt">// If no range exists and a new bar has appeared
      TIME1_X1 = iTime(_Symbol,_Period,rangeBars); class=class="str">"cmt">// Get the start time of the range
      class="type">int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,class="num">1); class=class="str">"cmt">// Get the bar index with the highest high in the range
      PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); class=class="str">"cmt">// Get the highest high price in the range

      TIME2_Y2 = iTime(_Symbol,_Period,class="num">0); class=class="str">"cmt">// Get the current time
      class="type">int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,class="num">1); class=class="str">"cmt">// Get the bar index with the lowest low in the range
      PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); class=class="str">"cmt">// Get the lowest low price in the range

      isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; class=class="str">"cmt">// Check if the range size is within the allowed points

      if (isInRange){ class=class="str">"cmt">// If the range size is valid
         plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); class=class="str">"cmt">// Plot the consolidation range

区间突破的挂单触发与重绘逻辑

区间被标出后,真正的交易动作发生在价格触碰边界外延的那一刻。代码里用 rangeSizePoints 个 _Point 作为缓冲,算出 R_HighBreak_Prc 与 R_LowBreak_Prc 两条突破价:上破看 Ask 是否大于 PRICE2_Y2 加缓冲,下破看 Bid 是否小于 PRICE1_Y1 减缓冲。 一旦 Ask 越过上破价,脚本先打印 BUY NOW 并复位 isInRange、isRangeExist 两个标志,若已有持仓(PositionsTotal()>0)直接 return 不重复开仓;否则以 0.01 手买入,SL 设在 Bid 减 sl_points*_Point,TP 设在 Bid 加 tp_points*_Point。下破对称处理,Bid 跌破下破价则卖出 0.01 手,止损止盈反向推算。 区间内不是死等,高低边会被实时推。若 Ask 刷新了 PRICE1_Y1(原区间上沿),就把 TIME2_Y2 更新为当前 K 线时间 iTime(_Symbol,_Period,0),调用 plotConsolidationRange 重绘区间——这意味着盘整通道会随新高自动上移。外汇与贵金属杠杆高,这类突破单在 data 跳空时可能滑点放大,实盘前建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍验证触发频率。

MQL5 / C++
isRangeExist = true; class=class="str">"cmt">// Set the range exist flag to true
Print("RANGE PLOTTED"); class=class="str">"cmt">// Print a message indicating the range is plotted
}
  }
}
ObjectSetInteger(class="num">0,rangeName,OBJPROP_FILL,class="kw">false); class=class="str">"cmt">// Disable fill for the range
 class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class=class="str">"cmt">// Get and normalize the current Ask price
 class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); class=class="str">"cmt">// Get and normalize the current Bid price

 if (isRangeExist && isInRange){ class=class="str">"cmt">// If the range exists and we are in range
  ...
  }
   class="type">class="kw">double R_HighBreak_Prc = (PRICE2_Y2+rangeSizePoints*_Point); class=class="str">"cmt">// Calculate the high breakout price
   class="type">class="kw">double R_LowBreak_Prc = (PRICE1_Y1-rangeSizePoints*_Point); class=class="str">"cmt">// Calculate the low breakout price

   if (Ask > R_HighBreak_Prc){ class=class="str">"cmt">// If the Ask price breaks the high breakout price
     Print("BUY NOW, ASK = ",Ask,", L = ",PRICE2_Y2,", H BREAK = ",R_HighBreak_Prc); class=class="str">"cmt">// Print a message to buy
     isInRange = class="kw">false; isRangeExist = class="kw">false; class=class="str">"cmt">// Reset range flags
     if (PositionsTotal() > class="num">0){class="kw">return;} class=class="str">"cmt">// Exit the function
     obj_Trade.Buy(class="num">0.01,_Symbol,Ask,Bid-sl_points*_Point,Bid+tp_points*_Point);
     class="kw">return; class=class="str">"cmt">// Exit the function
   }

   else if (Bid < R_LowBreak_Prc){ class=class="str">"cmt">// If the Bid price breaks the low breakout price
     Print("SELL NOW"); class=class="str">"cmt">// Print a message to sell
     isInRange = class="kw">false; isRangeExist = class="kw">false; class=class="str">"cmt">// Reset range flags
     if (PositionsTotal() > class="num">0){class="kw">return;} class=class="str">"cmt">// Exit the function
     obj_Trade.Sell(class="num">0.01,_Symbol,Bid,Ask+sl_points*_Point,Ask-tp_points*_Point);
     class="kw">return; class=class="str">"cmt">// Exit the function
   }
   if (Ask > PRICE1_Y1){ class=class="str">"cmt">// If the Ask price is higher than the current high price
     PRICE1_Y1 = Ask; class=class="str">"cmt">// Update the high price to the Ask price
     TIME2_Y2 = iTime(_Symbol,_Period,class="num">0); class=class="str">"cmt">// Update the end time to the current time
     Print("UPDATED RANGE PRICE1_Y1 TO ASK, NEEDS REPLOT"); class=class="str">"cmt">// Print a message indicating the range needs to be replotted
     plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); class=class="str">"cmt">// Replot the consolidation range
   }
   else if (Bid < PRICE2_Y2){ class=class="str">"cmt">// If the Bid price is lower than the current low price

「整理区间的末端时间与突破框架初始化」

当价格仍在区间内运行且未出现新K线时,脚本会把 PRICE2_Y2 直接刷成当前 Bid,并将 TIME2_Y2 取为当前 Bar 时间(iTime 偏移 0),随后重绘区间。若判定为新 Bar,则末端时间改为上一根 Bar 时间(iTime 偏移 1),区间向右延伸而不动低点,这种写法保证了整理形态在换柱时不会被错误截断。 下面这段是 EA 头部的全局定义,直接决定区间识别与风控尺度:rangeBars=10 表示只统计最近 10 根 Bar 作为潜在整理区;rangeSizePoints=400 限定区间高度上限为 400 点;sl_points 与 tp_points 均设为 500.0 点。外汇与贵金属杠杆高、滑点大,400 点区间在 XAUUSD 上可能只是一次常态回调,实盘前务必在 MT5 策略测试器按品种重算这些常数。 初始化函数 OnInit 目前为空实现,仅返回 INIT_SUCCEEDED,说明区间捕捉逻辑全放在 tick 或定时器里,而非启动阶段。想验证的话,把下面代码贴进 MT5 的 EA 模板,改 _Symbol 为你盯的货币对,编译后开一分钟图就能看到区间对象随价重绘。

MQL5 / C++
PRICE2_Y2 = Bid; class=class="str">"cmt">// Update the low price to the Bid price
TIME2_Y2 = iTime(_Symbol,_Period,class="num">0); class=class="str">"cmt">// Update the end time to the current time
Print("UPDATED RANGE PRICE2_Y2 TO BID, NEEDS REPLOT"); class=class="str">"cmt">// Print a message indicating the range needs to be replotted
plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); class=class="str">"cmt">// Replot the consolidation range
}
else{
   if (isNewBar){ class=class="str">"cmt">// If a new bar has appeared
      TIME2_Y2 = iTime(_Symbol,_Period,class="num">1); class=class="str">"cmt">// Update the end time to the previous bar time
      Print("EXTEND THE RANGE TO PREV BAR TIME"); class=class="str">"cmt">// Print a message indicating the range is extended
      plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); class=class="str">"cmt">// Replot the consolidation range
   }
}

class="macro">#include <Trade/Trade.mqh> class=class="str">"cmt">// Include the trade library
CTrade obj_Trade; class=class="str">"cmt">// Create an instance of the CTrade class
class="macro">#define rangeNAME "CONSOLIDATION RANGE" class=class="str">"cmt">// Define the name of the consolidation range
class="type">class="kw">datetime TIME1_X1, TIME2_Y2; class=class="str">"cmt">// Declare class="type">class="kw">datetime variables to hold range start and end times
class="type">class="kw">double PRICE1_Y1, PRICE2_Y2; class=class="str">"cmt">// Declare class="type">class="kw">double variables to hold range high and low prices
class="type">bool isRangeExist = class="kw">false; class=class="str">"cmt">// Flag to check if the range exists
class="type">bool isInRange = class="kw">false; class=class="str">"cmt">// Flag to check if we are currently within the range
class="type">int rangeBars = class="num">10; class=class="str">"cmt">// Number of bars to consider for the range
class="type">int rangeSizePoints = class="num">400; class=class="str">"cmt">// Maximum range size in points
class="type">class="kw">double sl_points = class="num">500.0; class=class="str">"cmt">// Stop loss points
class="type">class="kw">double tp_points = class="num">500.0; class=class="str">"cmt">// Take profit points

class="type">int OnInit(){
   class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">// Return initialization success
}

◍ 用新K线触发来锁定盘整区间

EA 退出时不需要做资源清理,OnDeinit 里留空即可,MT5 不会因此报错,但习惯上别在里面挂指标句柄释放逻辑,否则后续加功能容易漏。 真正干活的是 OnTick。每笔报价进来先拿 iBars(_Symbol,_Period) 算当前柱数,再用 static 变量 prevBars 做前后对比:柱数没变说明还在同一根 bar,isNewBar 置 false;柱数变了说明新 bar 成型,isNewBar 置 true 并刷新 prevBars。这套写法比直接比时间更稳,尤其跨周末跳空时不易误判。 只在「尚无区间 + 刚出新 bar」这两个条件同时满足时,才去回看最近 rangeBars 根 bar:iHighest 取最高价所在 bar 索引、iLowest 取最低价索引,分别用 iHigh / iLow 读出边界价。若 (高-低)/_Point 小于等于 rangeSizePoints,就判定为有效压缩区,调 plotConsolidationRange 画框并把 isRangeExist 锁 true,同时 Print("RANGE PLOTTED") 方便在日志里核对。外汇与贵金属波动随机,rangeSizePoints 设太小可能长时间不触发,设太大则假突破概率上升,建议先在 XAUUSD 的 M15 上用 200 点试。 取价末尾用 NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits) 规整卖价,避免浮点精度在后续下单判断里出问题。

MQL5 / C++
class=class="str">"cmt">//| Expert deinitialization function                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason){
class=class="str">"cmt">//---
   class=class="str">"cmt">// Deinitialization code here(we don&class="macro">#x27;t deinitialize anything)
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(){
class=class="str">"cmt">//---

   class="type">int currBars = iBars(_Symbol,_Period); class=class="str">"cmt">// 获取当前柱数
   class="kw">static class="type">int prevBars = currBars; class=class="str">"cmt">// 静态变量存上一次柱数
   class="kw">static class="type">bool isNewBar = class="kw">false; class=class="str">"cmt">// 静态标志判断新柱是否出现
   if (prevBars == currBars){isNewBar = class="kw">false;} class=class="str">"cmt">// 柱数没变则不是新柱
   else {isNewBar = true; prevBars = currBars;} class=class="str">"cmt">// 柱数变了则标记新柱并更新

   if (isRangeExist == class="kw">false && isNewBar){ class=class="str">"cmt">// 无区间且出新柱时才处理
      TIME1_X1 = iTime(_Symbol,_Period,rangeBars); class=class="str">"cmt">// 取区间起始时间
      class="type">int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,class="num">1); class=class="str">"cmt">// 取区间最高价bar索引
      PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); class=class="str">"cmt">// 取区间最高价

      TIME2_Y2 = iTime(_Symbol,_Period,class="num">0); class=class="str">"cmt">// 取当前时间
      class="type">int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,class="num">1); class=class="str">"cmt">// 取区间最低价bar索引
      PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); class=class="str">"cmt">// 取区间最低价

      isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; class=class="str">"cmt">// 判断区间点数是否在允许内

      if (isInRange){ class=class="str">"cmt">// 区间有效才画
         plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); class=class="str">"cmt">// 绘制盘整区
         isRangeExist = true; class=class="str">"cmt">// 标记区间已存在
         Print("RANGE PLOTTED"); class=class="str">"cmt">// 日志提示已绘制
      }
   }

   class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class=class="str">"cmt">// 获取并规整卖价
}
让小布替你跑这套区间扫描
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时震荡区间与边界突破提示,把重复劳动交给小布,你专注决策。

常见问题

可用iHigh/iLow配合循环或CopyHigh/CopyLow批量取数,按窗口长度滚动更新上下边界,注意处理首根不足N根的边界情况。
直接追突破逻辑简单、延迟低;等回调需额外记录突破位并监听回测,代码分支更多,但可能过滤假突破,概率上各有取舍。
贵金属点差和滑点在消息时段可能放大,建议在MT5回测中按真实账户滑点区间设参,否则外汇贵金属高风险下结果易失真。
可以,小布盯盘的AIGC模块会按你打开的品种页自动识别近期波动收敛区并画出边界,省去手动框区间的步骤。
倾向做成输入参数让用户自选,不同周期震荡幅度差异大,写死会限制策略适应性,也可能在切换品种时失效。