MQL5交易策略自动化(第十二部分):实现缓解型订单块(MOB)策略·进阶篇
📊

MQL5交易策略自动化(第十二部分):实现缓解型订单块(MOB)策略·进阶篇

(2/3)· 多数EA把订单块当静态支撑,却漏掉缓解验证,本文用代码补齐这块认知缺口

实战向 第 2/3 篇
把订单块画在图上就等价格回来,是常见的半吊子做法。真正的缓解型订单块要求价格重新触及并出现拒绝信号,才确认机构流动性已被吸收。本篇直接给你可编译的MQL5实现路径。

「先定参数与状态容器」

做订单块突破策略,第一件事是把可调参数和全局状态先声明清楚。下面这段代码把突破后等待K线数、冲动倍数、止损止盈点数(默认各 1500 点,贵金属 XAUUSD 上约 15 美元)以及牛熊订单块颜色都暴露成 input,方便直接在 MT5 属性框里改。 代码中用 struct PriceAndIndex 把价格和对应柱索引绑在一起,用来记震荡区间的最高高和最低低。全局变量里 isBreakoutDetected、lastImpulseLow/High、breakoutBarNumber 等构成一套市场状态机,后续 OnTick 里就靠这些 flag 判断是否已突破、冲动是否成形。 OnInit 只做了一件实在事:给交易对象设置魔术码(uniqueMagicNumber),确保 EA 只管自己的单子。开 MT5 加载这段,先别急着跑逻辑,确认 input 里 stopLossDistance 和 takeProfitDistance 的点数是否符合你品种的 tick 规模,外汇和贵金属杠杆高、点值差异大,参数不对可能一开盘就异常平仓。

MQL5 / C++
input class="type">int barsToWaitAfterBreakout = class="num">3;        class=class="str">"cmt">// Bars to wait after breakout before checking impulse
input class="type">class="kw">double impulseMultiplier = class="num">1.0;        class=class="str">"cmt">// Multiplier for detecting impulsive moves
input class="type">class="kw">double stopLossDistance = class="num">1500;        class=class="str">"cmt">// Stop loss distance in points
input class="type">class="kw">double takeProfitDistance = class="num">1500;      class=class="str">"cmt">// Take profit distance in points
input class="type">class="kw">color bullishOrderBlockColor = clrGreen;     class=class="str">"cmt">// Color for bullish order blocks
input class="type">class="kw">color bearishOrderBlockColor = clrRed;       class=class="str">"cmt">// Color for bearish order blocks
input class="type">class="kw">color mitigatedOrderBlockColor = clrGray;    class=class="str">"cmt">// Color for mitigated order blocks
input class="type">class="kw">color labelTextColor = clrBlack;            class=class="str">"cmt">// Color for text labels
class=class="str">"cmt">//--- Struct to store price and index for highs and lows
class="kw">struct PriceAndIndex {
   class="type">class="kw">double price;   class=class="str">"cmt">// Price value
   class="type">int    index;   class=class="str">"cmt">// Bar index where this price occurs
};
class=class="str">"cmt">//--- Global variables for tracking market state
PriceAndIndex rangeHighestHigh = {class="num">0, class="num">0};     class=class="str">"cmt">// Highest high in the consolidation range
PriceAndIndex rangeLowestLow = {class="num">0, class="num">0};       class=class="str">"cmt">// Lowest low in the consolidation range
class="type">bool isBreakoutDetected = false;             class=class="str">"cmt">// Flag for when a breakout occurs
class="type">class="kw">double lastImpulseLow = class="num">0.0;                 class=class="str">"cmt">// Low price after breakout for impulse check
class="type">class="kw">double lastImpulseHigh = class="num">0.0;                class=class="str">"cmt">// High price after breakout for impulse check
class="type">int breakoutBarNumber = -class="num">1;                 class=class="str">"cmt">// Bar index where breakout happened
class="type">class="kw">datetime breakoutTimestamp = class="num">0;             class=class="str">"cmt">// Time of the breakout
class="type">class="kw">string orderBlockNames[];                   class=class="str">"cmt">// Array of order block object names
class="type">class="kw">datetime orderBlockEndTimes[];              class=class="str">"cmt">// Array of order block end times
class="type">bool orderBlockMitigatedStatus[];           class=class="str">"cmt">// Array tracking if order blocks are mitigated
class="type">bool isBullishImpulse = false;              class=class="str">"cmt">// Flag for bullish impulsive move
class="type">bool isBearishImpulse = false;              class=class="str">"cmt">// Flag for bearish impulsive move
class="macro">#define OB_Prefix "OB REC "     class=class="str">"cmt">// Prefix for order block object names

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
   class=class="str">"cmt">//--- Set the magic number for the trade object to identify EA trades
   obj_Trade.SetExpertMagicNumber(uniqueMagicNumber);
   class="kw">return(INIT_SUCCEEDED);
}

用新K线触发来框定整理区间

EA 只在每根 K 线收盘后跑一次逻辑,能避开 Tick 级冗余计算。用 iBars 计数比对,当前根数和上根不同就置 isNewBar=true,否则直接 return,这样整理判定不会在一根没走完时被反复触发。 整理区识别从 startBarIndex=1 开始(跳过正在形成的 0 号 K 线)。若最高/最低价缓存还是 0,就进入判定:拿 consolidationBars-1 根里的相邻 high、low 互减,绝对值超过 maxConsolidationSpread*Point() 就判为不收敛,break 退出。 收敛通过后,在 consolidationBars 根里扫最高 high 写进 rangeHighestHigh,扫最低 low 写进 rangeLowestLow,同时记录各自 index。外汇与贵金属波动跳变频繁,maxConsolidationSpread 设太小容易漏掉真实蓄势,设太大又会把趋势中的回撤误判为箱体,建议先在 M5 黄金上用 15 点(1.5*Point 的 10 倍量级)回测观察。 下方代码即新 K 线判定与整理扫描骨架,复制进 MT5 能直接编译验证:

MQL5 / C++
class="type">void OnTick() {
   class=class="str">"cmt">//--- Check for a new bar to process logic only once per bar
   class="kw">static class="type">bool isNewBar = false;
   class="type">int currentBarCount = iBars(_Symbol, _Period);
   class="kw">static class="type">int previousBarCount = currentBarCount;
   if (previousBarCount == currentBarCount) {
      isNewBar = false;
   } else if (previousBarCount != currentBarCount) {
      isNewBar = true;
      previousBarCount = currentBarCount;
   }
   class=class="str">"cmt">//--- Exit if not a new bar to avoid redundant processing
   if (!isNewBar)
      class="kw">return;
   class=class="str">"cmt">//---
}
class=class="str">"cmt">//--- Define the starting bar index for consolidation checks
class="type">int startBarIndex = class="num">1;
class=class="str">"cmt">//--- Check for consolidation or extend the existing range
if (!isBreakoutDetected) {
   if (rangeHighestHigh.price == class="num">0 && rangeLowestLow.price == class="num">0) {
      class=class="str">"cmt">//--- Check if bars are in a tight consolidation range
      class="type">bool isConsolidated = true;
      for (class="type">int i = startBarIndex; i < startBarIndex + consolidationBars - class="num">1; i++) {
         if (MathAbs(high(i) - high(i + class="num">1)) > maxConsolidationSpread * Point()) {
            isConsolidated = false;
            break;
         }
         if (MathAbs(low(i) - low(i + class="num">1)) > maxConsolidationSpread * Point()) {
            isConsolidated = false;
            break;
         }
      }
      if (isConsolidated) {
         class=class="str">"cmt">//--- Find the highest high in the consolidation range
         rangeHighestHigh.price = high(startBarIndex);
         rangeHighestHigh.index = startBarIndex;
         for (class="type">int i = startBarIndex + class="num">1; i < startBarIndex + consolidationBars; i++) {
            if (high(i) > rangeHighestHigh.price) {
               rangeHighestHigh.price = high(i);
               rangeHighestHigh.index = i;
            }
         }
         class=class="str">"cmt">//--- Find the lowest low in the consolidation range
         rangeLowestLow.price = low(startBarIndex);
         rangeLowestLow.index = startBarIndex;
         for (class="type">int i = startBarIndex + class="num">1; i < startBarIndex + consolidationBars; i++) {
            if (low(i) < rangeLowestLow.price) {
               rangeLowestLow.price = low(i);
               rangeLowestLow.index = i;
            }
         }

◍ 用收盘价判定箱体突破与状态重置

盘整区间一旦被记录,后续每根已完成 K 线(索引 1)都要拿它的高、低和区间边界比:高不超过区间最高、低不低于区间最低,才算区间延续;否则直接打印「Bar outside range」,说明这根棒线已戳出边界,可能酝酿变盘。 突破判定只看收盘价而非影线。若 close(1) 大于区间最高价,打印向上突破并置 isBreakoutDetected=true;低于区间最低价则判向下突破。外汇与贵金属杠杆高,假突破概率不低,实盘须配合成交量或更高周期过滤。 突破确认后立刻复位:breakoutBarNumber 归 1、记录 TimeCurrent() 时间戳,把区间高低存进 lastImpulseHigh / Low,随后清空 rangeHighestHigh 与 rangeLowestLow 的价格和索引,等待下一轮箱体。下面这段是区间延伸检查与突破检测的核心片段。 别把影线当突破信号 许多 EA 用最高价碰线就平仓,结果在黄金 5 分钟图上被毛刺扫掉。用 close(1) 过滤,至少要求一根棒线收定,能砍掉相当一部分噪音假信号。

MQL5 / C++
class=class="str">"cmt">//--- Check if the current bar extends the existing range
class="type">class="kw">double currentHigh = high(class="num">1);
class="type">class="kw">double currentLow = low(class="num">1);
if (currentHigh <= rangeHighestHigh.price && currentLow >= rangeLowestLow.price) {
    Print("Range extended: High = ", currentHigh, ", Low = ", currentLow);
} else {
    Print("No extension: Bar outside range.");
}
class=class="str">"cmt">//--- Detect a breakout from the consolidation range
if (rangeHighestHigh.price > class="num">0 && rangeLowestLow.price > class="num">0) {
  class="type">class="kw">double currentClosePrice = close(class="num">1);
  if (currentClosePrice > rangeHighestHigh.price) {
    Print("Upward breakout at ", currentClosePrice, " > ", rangeHighestHigh.price);
    isBreakoutDetected = true;
  } else if (currentClosePrice < rangeLowestLow.price) {
    Print("Downward breakout at ", currentClosePrice, " < ", rangeLowestLow.price);
    isBreakoutDetected = true;
  }
}
class=class="str">"cmt">//--- Reset state after a breakout is detected
if (isBreakoutDetected) {
  Print("Breakout detected. Resetting for the next range.");
  breakoutBarNumber = class="num">1;
  breakoutTimestamp = TimeCurrent();
  lastImpulseHigh = rangeHighestHigh.price;
  lastImpulseLow = rangeLowestLow.price;
  isBreakoutDetected = false;
  rangeHighestHigh.price = class="num">0;
  rangeHighestHigh.index = class="num">0;
  rangeLowestLow.price = class="num">0;
  rangeLowestLow.index = class="num">0;
}

「突破后如何用脉冲幅度圈定订单块」

突破发生后,系统不会立刻画订单块,而是先等一段冷却时间。代码里用 barsToWaitAfterBreakout * PeriodSeconds() 算出从突破时间戳起要空跑的秒数,只有当前时间越过这条线,才进入脉冲判定逻辑。 脉冲幅度由突破前的高低差决定:impulseRange = lastImpulseHigh - lastImpulseLow,再乘 impulseMultiplier 得到阈值。比如高低差 50 点、乘子 0.5,则向上需收在突破高上方 25 点以上才认作多头脉冲,向下对称。 随后在冷却窗内逐根检查收盘价,一旦触碰阈值就置位 isBullishImpulseisBearishImpulse 并 break,没碰到就打印“未检测到脉冲”。这一步直接决定后面订单块画不画——isOrderBlockValid 就是这两个布尔的或运算。 确认有效后,矩形起点取 consolidationBars + barsToWaitAfterBreakout + 1 号 K 线时间,顶底沿用 lastImpulseHigh/Low;右边界偷懒用了图表可见 Bar 数乘周期秒数,等于把区块拉长到屏外。颜色与标签按方向切换,用 OBJ_RECTANGLE 画到主图,重名则跳过。 实盘里把 impulseMultiplier 从 0.5 调到 0.3,订单块触发会更频繁但假信号可能增多;外汇与贵金属波动剧烈,这种绘制仅作区域参考,不代表后续必然反转。

MQL5 / C++
if (breakoutBarNumber >= class="num">0 && TimeCurrent() > breakoutTimestamp + barsToWaitAfterBreakout * PeriodSeconds()) {
   class="type">class="kw">double impulseRange = lastImpulseHigh - lastImpulseLow;
   class="type">class="kw">double impulseThresholdPrice = impulseRange * impulseMultiplier;
   isBullishImpulse = false;
   isBearishImpulse = false;
   for (class="type">int i = class="num">1; i <= barsToWaitAfterBreakout; i++) {
      class="type">class="kw">double closePrice = close(i);
      if (closePrice >= lastImpulseHigh + impulseThresholdPrice) {
         isBullishImpulse = true;
         Print("Impulsive upward move: ", closePrice, " >= ", lastImpulseHigh + impulseThresholdPrice);
         break;
      } else if (closePrice <= lastImpulseLow - impulseThresholdPrice) {
         isBearishImpulse = true;
         Print("Impulsive downward move: ", closePrice, " <= ", lastImpulseLow - impulseThresholdPrice);
         break;
      }
   }
   if (!isBullishImpulse && !isBearishImpulse) {
      Print("No impulsive movement detected.");
   }
}
class="type">bool isOrderBlockValid = isBearishImpulse || isBullishImpulse;
if (isOrderBlockValid) {
   class="type">class="kw">datetime blockStartTime = iTime(_Symbol, _Period, consolidationBars + barsToWaitAfterBreakout + class="num">1);
   class="type">class="kw">double blockTopPrice = lastImpulseHigh;
   class="type">int visibleBarsOnChart = (class="type">int)ChartGetInteger(class="num">0, CHART_VISIBLE_BARS);
   class="type">class="kw">datetime blockEndTime = blockStartTime + (visibleBarsOnChart / class="num">1) * PeriodSeconds();
   class="type">class="kw">double blockBottomPrice = lastImpulseLow;
   class="type">class="kw">string orderBlockName = OB_Prefix + "(" + TimeToString(blockStartTime) + ")";
   class="type">class="kw">color orderBlockColor = isBullishImpulse ? bullishOrderBlockColor : bearishOrderBlockColor;
   class="type">class="kw">string orderBlockLabel = isBullishImpulse ? "Bullish OB" : "Bearish OB";
   if (ObjectFind(class="num">0, orderBlockName) < class="num">0) {
      ObjectCreate(class="num">0, orderBlockName, OBJ_RECTANGLE, class="num">0, blockStartTime, blockTopPrice, blockEndTime, blockBottomPrice);
      ObjectSetInteger(class="num">0, orderBlockName, OBJPROP_TIME, class="num">0, blockStartTime);
      ObjectSetDouble(class="num">0, orderBlockName, OBJPROP_PRICE, class="num">0, blockTopPrice);
      ObjectSetInteger(class="num">0, orderBlockName, OBJPROP_TIME, class="num">1, blockEndTime);
      ObjectSetDouble(class="num">0, orderBlockName, OBJPROP_PRICE, class="num">1, blockBottomPrice);
      ObjectSetInteger(class="num">0, orderBlockName, OBJPROP_FILL, true);
      ObjectSetInteger(class="num">0, orderBlockName, OBJPROP_COLOR, orderBlockColor);
      ObjectSetInteger(class="num">0, orderBlockName, OBJPROP_BACK, false);

把订单块标注和后续跟踪写进图表与数组

在订单块中段挂文字标签时,先取 blockStartTime 与 blockEndTime 的中点作为时间轴定位,再取 blockTopPrice 与 blockBottomPrice 均值作价格轴定位,这样标签恒在区块几何中心,不会随刷新漂走。 动态字号依赖 ChartGetInteger(0, CHART_SCALE) 的返回值,该缩放等级在 MT5 中固定为 0~5 的整数;代码里用 8 + chartScale*2 映射,实际字号落在 8(最缩小)到 18(最放大)之间,比写死 10 号字在缩放时更易读。 每新建一个订单块,就把名称、结束时间、未 mitigated 状态分别追加进三个动态数组,ArrayResize 每次只加 1,逻辑简单但高频 new block 时有一定重分配开销,实盘若块数过百可考虑预分配。 处理存量块时用倒序 for 循环从最新往旧扫,先通过 ObjectGetDouble 拿 OBJPROP_PRICE 的 0/1 索引还原高低价,再用 OBJPROP_TIME 的 0 索引取起始时间;这套读取方式让你在后续判断价格是否回踩块体时有确定坐标可用。 外汇与贵金属杠杆高、滑点突兀,订单块被触碰不代表方向必现,仅提高概率倾斜,一切以实盘 MT5 加载 EA 后观察 ChartRedraw 后的标注位置为准。

MQL5 / C++
class="type">class="kw">datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / class="num">2;
class="type">class="kw">double labelPrice = (blockTopPrice + blockBottomPrice) / class="num">2;
class="type">class="kw">string labelObjectName = orderBlockName + orderBlockLabel;
if (ObjectFind(class="num">0, labelObjectName) < class="num">0) {
   ObjectCreate(class="num">0, labelObjectName, OBJ_TEXT, class="num">0, labelTime, labelPrice);
   ObjectSetString(class="num">0, labelObjectName, OBJPROP_TEXT, orderBlockLabel);
   ObjectSetInteger(class="num">0, labelObjectName, OBJPROP_COLOR, labelTextColor);
   ObjectSetInteger(class="num">0, labelObjectName, OBJPROP_FONTSIZE, dynamicFontSize);
   ObjectSetInteger(class="num">0, labelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER);
}
ChartRedraw(class="num">0);
ArrayResize(orderBlockNames, ArraySize(orderBlockNames) + class="num">1);
orderBlockNames[ArraySize(orderBlockNames) - class="num">1] = orderBlockName;
ArrayResize(orderBlockEndTimes, ArraySize(orderBlockEndTimes) + class="num">1);
orderBlockEndTimes[ArraySize(orderBlockEndTimes) - class="num">1] = blockEndTime;
ArrayResize(orderBlockMitigatedStatus, ArraySize(orderBlockMitigatedStatus) + class="num">1);
orderBlockMitigatedStatus[ArraySize(orderBlockMitigatedStatus) - class="num">1] = false;
Print("Order Block created: ", orderBlockName);
}
}
class="type">int chartScale = (class="type">int)ChartGetInteger(class="num">0, CHART_SCALE);
class="type">int dynamicFontSize = class="num">8 + (chartScale * class="num">2);
breakoutBarNumber = -class="num">1;
breakoutTimestamp = class="num">0;
lastImpulseHigh = class="num">0;
lastImpulseLow = class="num">0;
isBullishImpulse = false;
isBearishImpulse = false;
for (class="type">int j = ArraySize(orderBlockNames) - class="num">1; j >= class="num">0; j--) {
   class="type">class="kw">string currentOrderBlockName = orderBlockNames[j];
   class="type">bool doesOrderBlockExist = false;
   class="type">class="kw">double orderBlockHigh = ObjectGetDouble(class="num">0, currentOrderBlockName, OBJPROP_PRICE, class="num">0);
   class="type">class="kw">double orderBlockLow = ObjectGetDouble(class="num">0, currentOrderBlockName, OBJPROP_PRICE, class="num">1);
   class="type">class="kw">datetime orderBlockStartTime = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0, currentOrderBlockName, OBJPROP_TIME, class="num">0);
让小布替你跑这套识别
小布盯盘的AIGC已内置MOB区域标注,打开对应品种页即可看到缓解事件是否被触发,你只需核对EA逻辑是否与之吻合。

常见问题

普通订单块只标记机构介入区域,缓解型订单块额外要求价格回触该区并产生拒绝信号(如长影线或动量反转),确认流动性吸收完成才视作有效。
目前小布提供MOB区域的可视化与触发提示,代码仍需在MetaEditor中按本文结构自行编写,但可对照小布标注验证历史命中率。
重点看趋势方向、流动性扫荡是否发生,以及价格是否在高位或低位先扫掉止损再回到订单块,确保交易方向与整体流向一致。
基于订单块结构本身的空间,例如块体边缘或拒绝影线端点,再叠加用户设定的风险回报比来推导止盈,而非固定点数。