MQL5交易策略自动化(第十二部分):实现缓解型订单块(MOB)策略·进阶篇
(2/3)· 多数EA把订单块当静态支撑,却漏掉缓解验证,本文用代码补齐这块认知缺口
「先定参数与状态容器」
做订单块突破策略,第一件事是把可调参数和全局状态先声明清楚。下面这段代码把突破后等待K线数、冲动倍数、止损止盈点数(默认各 1500 点,贵金属 XAUUSD 上约 15 美元)以及牛熊订单块颜色都暴露成 input,方便直接在 MT5 属性框里改。 代码中用 struct PriceAndIndex 把价格和对应柱索引绑在一起,用来记震荡区间的最高高和最低低。全局变量里 isBreakoutDetected、lastImpulseLow/High、breakoutBarNumber 等构成一套市场状态机,后续 OnTick 里就靠这些 flag 判断是否已突破、冲动是否成形。 OnInit 只做了一件实在事:给交易对象设置魔术码(uniqueMagicNumber),确保 EA 只管自己的单子。开 MT5 加载这段,先别急着跑逻辑,确认 input 里 stopLossDistance 和 takeProfitDistance 的点数是否符合你品种的 tick 规模,外汇和贵金属杠杆高、点值差异大,参数不对可能一开盘就异常平仓。
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 能直接编译验证:
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) 过滤,至少要求一根棒线收定,能砍掉相当一部分噪音假信号。
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 点以上才认作多头脉冲,向下对称。
随后在冷却窗内逐根检查收盘价,一旦触碰阈值就置位 isBullishImpulse 或 isBearishImpulse 并 break,没碰到就打印“未检测到脉冲”。这一步直接决定后面订单块画不画——isOrderBlockValid 就是这两个布尔的或运算。
确认有效后,矩形起点取 consolidationBars + barsToWaitAfterBreakout + 1 号 K 线时间,顶底沿用 lastImpulseHigh/Low;右边界偷懒用了图表可见 Bar 数乘周期秒数,等于把区块拉长到屏外。颜色与标签按方向切换,用 OBJ_RECTANGLE 画到主图,重名则跳过。
实盘里把 impulseMultiplier 从 0.5 调到 0.3,订单块触发会更频繁但假信号可能增多;外汇与贵金属波动剧烈,这种绘制仅作区域参考,不代表后续必然反转。
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 后的标注位置为准。
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);