
Automating Trading Strategies in MQL5 (Part 12): Implementing the Mitigation Order Blocks (MOB) Strategy
Introduction
In our previous article (Part 11), we built a multi-level grid trading system in MetaQuotes Language 5 (MQL5) to capitalize on market fluctuations. Now, in Part 12, we focus on implementing the Mitigation Order Blocks (MOB) Strategy, a Smart Money concept that identifies key price zones where institutional orders are mitigated before significant market moves. We will cover the following topics:
By the end of this article, you will have a fully automated mitigation order blocks trading system ready for trading. Let’s get started!
Strategy Blueprint
To implement the Mitigation Order Blocks Strategy, we will develop an automated system that detects, validates, and executes trades based on order block mitigation events. The strategy will focus on identifying institutional price zones where liquidity is absorbed before trend continuation. Our system will incorporate precise conditions for entry, stop-loss placement, and trade management to ensure efficiency and accuracy. We will structure the development as follows:
- Order Block Identification – The system will scan historical price action to detect bullish and bearish order blocks, filtering out weak zones based on volatility, liquidity grabs, and price imbalance.
- Mitigation Validation – We will program conditions that confirm a valid mitigation event, ensuring that the price revisits the order block and reacts with rejection signals such as wicks or momentum shifts.
- Market Structure Confirmation – The EA will analyze higher-timeframe trends and liquidity sweeps to ensure that the identified mitigation aligns with the broader market flow.
- Trade Execution Rules – Once mitigation is confirmed, the system will define precise entry points, dynamically calculate stop-loss levels based on order block structure, and set take-profit targets based on risk-reward parameters.
- Risk and Money Management – The strategy will integrate position sizing, drawdown protection, and exit strategies to manage trade risk effectively.
In a nutshell, here is a general visualization of the strategy.
Implementation in MQL5
To create the program in MQL5, open the MetaEditor, go to the Navigator, locate the Indicators folder, click on the "New" tab, and follow the prompts to create the file. Once it is made, in the coding environment, we will need to declare some global variables that we will use throughout the program.
//+------------------------------------------------------------------+ //| Copyright 2025, Forex Algo-Trader, Allan. | //| "https://t.me/Forex_Algo_Trader" | //+------------------------------------------------------------------+ #property copyright "Forex Algo-Trader, Allan" #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property description "This EA trades based on Mitigation Order Blocks Strategy" #property strict //--- Include the trade library for managing positions #include <Trade/Trade.mqh> CTrade obj_Trade;
We begin the implementation by including the trade library with "#include <Trade/Trade.mqh>", which provides built-in functions for managing trade operations. We then initialize the trade object "obj_Trade" using the "CTrade" class, allowing the Expert Advisor to execute buy and sell orders programmatically. This setup will ensure that trade execution is handled efficiently without requiring manual intervention. Then we can provide some inputs so the user can change and control the behavior from the user interface (UI).
//+------------------------------------------------------------------+ //| Input Parameters | //+------------------------------------------------------------------+ input double tradeLotSize = 0.01; // Trade size for each position input bool enableTrading = true; // Toggle to allow or disable trading input bool enableTrailingStop = true; // Toggle to enable or disable trailing stop input double trailingStopPoints = 30; // Distance in points for trailing stop input double minProfitToTrail = 50; // Minimum profit in points before trailing starts (not used yet) input int uniqueMagicNumber = 12345; // Unique identifier for EA trades input int consolidationBars = 7; // Number of bars to check for consolidation input double maxConsolidationSpread = 50; // Maximum allowed spread in points for consolidation input int barsToWaitAfterBreakout = 3; // Bars to wait after breakout before checking impulse input double impulseMultiplier = 1.0; // Multiplier for detecting impulsive moves input double stopLossDistance = 1500; // Stop loss distance in points input double takeProfitDistance = 1500; // Take profit distance in points input color bullishOrderBlockColor = clrGreen; // Color for bullish order blocks input color bearishOrderBlockColor = clrRed; // Color for bearish order blocks input color mitigatedOrderBlockColor = clrGray; // Color for mitigated order blocks input color labelTextColor = clrBlack; // Color for text labels
Here, we define input parameters to configure the program's behavior. "tradeLotSize" sets the position size, while "enableTrading" and "enableTrailingStop" control execution and trailing stops, with "trailingStopPoints" and "minProfitToTrail" refining stop logic. "uniqueMagicNumber" identifies trades, and consolidation is detected using "consolidationBars" and "maxConsolidationSpread". Breakouts are confirmed with "barsToWaitAfterBreakout" and "impulseMultiplier". "stopLossDistance" and "takeProfitDistance" manage risk, while "bullishOrderBlockColor", "bearishOrderBlockColor", "mitigatedOrderBlockColor", and "labelTextColor" handle chart visuals.
Lastly, we need to define some global variables that we will use for the overall system control.
//--- Struct to store price and index for highs and lows struct PriceAndIndex { double price; // Price value int index; // Bar index where this price occurs }; //--- Global variables for tracking market state PriceAndIndex rangeHighestHigh = {0, 0}; // Highest high in the consolidation range PriceAndIndex rangeLowestLow = {0, 0}; // Lowest low in the consolidation range bool isBreakoutDetected = false; // Flag for when a breakout occurs double lastImpulseLow = 0.0; // Low price after breakout for impulse check double lastImpulseHigh = 0.0; // High price after breakout for impulse check int breakoutBarNumber = -1; // Bar index where breakout happened datetime breakoutTimestamp = 0; // Time of the breakout string orderBlockNames[]; // Array of order block object names datetime orderBlockEndTimes[]; // Array of order block end times bool orderBlockMitigatedStatus[]; // Array tracking if order blocks are mitigated bool isBullishImpulse = false; // Flag for bullish impulsive move bool isBearishImpulse = false; // Flag for bearish impulsive move #define OB_Prefix "OB REC " // Prefix for order block object names
We first define the "PriceAndIndex" struct, which stores a "price" value and the "index" of the bar where this price occurs. This structure will be useful for tracking specific price points within a defined range. The global variables manage key aspects of market structure and breakout detection. "rangeHighestHigh" and "rangeLowestLow" will store the highest and lowest prices in the consolidation range, respectively, helping to define the boundaries of potential order blocks. "isBreakoutDetected" will act as a flag to indicate when a breakout has occurred, while "lastImpulseLow" and "lastImpulseHigh" will store the first low and high after a breakout, used to confirm impulsive moves.
"breakoutBarNumber" will record the bar index where the breakout happened, and "breakoutTimestamp" store the exact time of the breakout event. The arrays "orderBlockNames", "orderBlockEndTimes", and "orderBlockMitigatedStatus" will handle the identification, lifespan, and mitigation tracking of order blocks. The boolean flags "isBullishImpulse" and "isBearishImpulse" determine whether the breakout move qualifies as a bullish or bearish impulse. Finally, "OB_Prefix" is a predefined string prefix defined by the macro #define used when naming order block objects, ensuring consistency in graphical representation. With the variables, we are all set to begin the program logic.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the magic number for the trade object to identify EA trades obj_Trade.SetExpertMagicNumber(uniqueMagicNumber); return(INIT_SUCCEEDED); }
Here, we initialize the Expert Advisor in the OnInit event handler. We set the expert magic number using the "SetExpertMagicNumber" method, ensuring that all trades executed by our EA are uniquely tagged, preventing conflicts with other trades. This step is crucial for tracking and managing only the trades opened by our strategy. Once the initialization is complete, we return INIT_SUCCEEDED, confirming that our program is ready to operate. We can then graduate to the main OnTick event handler to our main control logic.
//+------------------------------------------------------------------+ //| Expert OnTick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check for a new bar to process logic only once per bar static bool isNewBar = false; int currentBarCount = iBars(_Symbol, _Period); static int previousBarCount = currentBarCount; if (previousBarCount == currentBarCount) { isNewBar = false; } else if (previousBarCount != currentBarCount) { isNewBar = true; previousBarCount = currentBarCount; } //--- Exit if not a new bar to avoid redundant processing if (!isNewBar) return; //--- }
To ensure we process data on every bar and not on every tick, on the OnTick function, which executes on every new tick received, we use the iBars function to get the total number of bars on the chart and store it in "currentBarCount". We then compare it with "previousBarCount", and if they are equal, "isNewBar" remains false, preventing redundant processing. If a new bar is detected, we update "previousBarCount" and set "isNewBar" to true, allowing the strategy logic to execute. Finally, if "isNewBar" is false, we return early, optimizing performance by skipping unnecessary computations. If it is a new bar, we continue looking for consolidations.
//--- Define the starting bar index for consolidation checks int startBarIndex = 1; //--- Check for consolidation or extend the existing range if (!isBreakoutDetected) { if (rangeHighestHigh.price == 0 && rangeLowestLow.price == 0) { //--- Check if bars are in a tight consolidation range bool isConsolidated = true; for (int i = startBarIndex; i < startBarIndex + consolidationBars - 1; i++) { if (MathAbs(high(i) - high(i + 1)) > maxConsolidationSpread * Point()) { isConsolidated = false; break; } if (MathAbs(low(i) - low(i + 1)) > maxConsolidationSpread * Point()) { isConsolidated = false; break; } } if (isConsolidated) { //--- Find the highest high in the consolidation range rangeHighestHigh.price = high(startBarIndex); rangeHighestHigh.index = startBarIndex; for (int i = startBarIndex + 1; i < startBarIndex + consolidationBars; i++) { if (high(i) > rangeHighestHigh.price) { rangeHighestHigh.price = high(i); rangeHighestHigh.index = i; } } //--- Find the lowest low in the consolidation range rangeLowestLow.price = low(startBarIndex); rangeLowestLow.index = startBarIndex; for (int i = startBarIndex + 1; i < startBarIndex + consolidationBars; i++) { if (low(i) < rangeLowestLow.price) { rangeLowestLow.price = low(i); rangeLowestLow.index = i; } } //--- Log the established consolidation range Print("Consolidation range established: Highest High = ", rangeHighestHigh.price, " at index ", rangeHighestHigh.index, " and Lowest Low = ", rangeLowestLow.price, " at index ", rangeLowestLow.index); } } else { //--- Check if the current bar extends the existing range double currentHigh = high(1); double currentLow = low(1); if (currentHigh <= rangeHighestHigh.price && currentLow >= rangeLowestLow.price) { Print("Range extended: High = ", currentHigh, ", Low = ", currentLow); } else { Print("No extension: Bar outside range."); } } }
Here, we define and establish the consolidation range by analyzing recent price movements. We begin by setting "startBarIndex" to 1, determining the starting point for our consolidation checks. If we have not yet detected a breakout, as indicated by "isBreakoutDetected", we proceed to assess whether the market is in a tight consolidation phase. We iterate through the last "consolidationBars" count of bars, using the MathAbs function to measure the absolute differences between consecutive highs and lows. If all differences remain within "maxConsolidationSpread", we confirm consolidation.
Once consolidation is detected, we determine the highest high and lowest low within the range. We initialize "rangeHighestHigh" and "rangeLowestLow" with the high and low of "startBarIndex", then iterate through the range to update these values whenever we encounter a new highest high or lowest low. These values define our consolidation boundaries.
If the consolidation range is already established, we check whether the current bar extends the existing range. We retrieve "currentHigh" and "currentLow" using the "high" and "low" functions and compare them to "rangeHighestHigh.price" and "rangeLowestLow.price". If the price remains within the range, we print a message indicating the range extension using the Print function. Otherwise, we print that no extension occurred, signaling a potential breakout scenario. The custom price functions are as below.
//+------------------------------------------------------------------+ //| Price data accessors | //+------------------------------------------------------------------+ double high(int index) { return iHigh(_Symbol, _Period, index); } //--- Get high price of a bar double low(int index) { return iLow(_Symbol, _Period, index); } //--- Get low price of a bar double open(int index) { return iOpen(_Symbol, _Period, index); } //--- Get open price of a bar double close(int index) { return iClose(_Symbol, _Period, index); } //--- Get close price of a bar datetime time(int index) { return iTime(_Symbol, _Period, index); } //--- Get time of a bar
These custom functions help us retrieve price data. The "high" function uses iHigh to return the high price of a bar at a specified "index", while the "low" function calls iLow to obtain the corresponding low price. The "open" function fetches the opening price using iOpen, and the "close" function retrieves the closing price via iClose. Additionally, the "time" function employs iTime to return the timestamp of the given bar. Upon running the program, we have the following result.
From the image, we can see that once the price range is established and price revolves within the range, we extend it until we have a range breach. So now, we need to detect a breakout on the confirmed price lag range. We achieve this using the following logic.
//--- Detect a breakout from the consolidation range if (rangeHighestHigh.price > 0 && rangeLowestLow.price > 0) { double currentClosePrice = close(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; } } //--- Reset state after a breakout is detected if (isBreakoutDetected) { Print("Breakout detected. Resetting for the next range."); breakoutBarNumber = 1; breakoutTimestamp = TimeCurrent(); lastImpulseHigh = rangeHighestHigh.price; lastImpulseLow = rangeLowestLow.price; isBreakoutDetected = false; rangeHighestHigh.price = 0; rangeHighestHigh.index = 0; rangeLowestLow.price = 0; rangeLowestLow.index = 0; }
To detect and handle breakouts from a previously identified consolidation range, we first verify that the "rangeHighestHigh.price" and "rangeLowestLow.price" values are valid, ensuring a consolidation range has been established. We then compare the "currentClosePrice", obtained using the "close" function, against the range boundaries. If the closing price exceeds "rangeHighestHigh.price", we recognize an upward breakout, logging the event and setting "isBreakoutDetected" to true. Similarly, if the closing price falls below "rangeLowestLow.price", we identify a downward breakout and flag it accordingly.
Once a breakout is confirmed, we reset the necessary state variables to prepare for tracking a new consolidation phase. We log the breakout occurrence and store the "breakoutBarNumber" as 1, marking the first bar of the breakout sequence. The "breakoutTimestamp" is recorded using TimeCurrent to note the exact time of the breakout. Additionally, we store "lastImpulseHigh" and "lastImpulseLow" to track post-breakout price behavior. Finally, we reset "isBreakoutDetected" to false and clear the previous consolidation range by setting "rangeHighestHigh.price" and "rangeLowestLow.price" to 0, ensuring the system is ready to detect the next trading opportunity.
If there are confirmed breakouts, we wait and verify them via impulsive movements, and then plot them on the chart.
//--- Check for impulsive movement after breakout and create order blocks if (breakoutBarNumber >= 0 && TimeCurrent() > breakoutTimestamp + barsToWaitAfterBreakout * PeriodSeconds()) { double impulseRange = lastImpulseHigh - lastImpulseLow; double impulseThresholdPrice = impulseRange * impulseMultiplier; isBullishImpulse = false; isBearishImpulse = false; for (int i = 1; i <= barsToWaitAfterBreakout; i++) { 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."); } //--- }
Here, we analyze price action after a breakout to determine whether an impulsive movement has occurred, which is critical for identifying valid order blocks. We first check if "breakoutBarNumber" is valid and if the current time, retrieved via TimeCurrent, has surpassed the "breakoutTimestamp" plus "barsToWaitAfterBreakout" multiplied by PeriodSeconds, ensuring a sufficient waiting period has elapsed. We then calculate "impulseRange" as the difference between "lastImpulseHigh" and "lastImpulseLow", which represents the post-breakout price fluctuation. Using this, we compute "impulseThresholdPrice" by multiplying "impulseRange" by "impulseMultiplier" to define the minimum price extension required for an impulsive move.
Next, we initialize "isBullishImpulse" and "isBearishImpulse" as false, preparing to evaluate the price action over the last "barsToWaitAfterBreakout" bars. We iterate through these bars using a for loop, retrieving the closing price with the "close" function. If "closePrice" is greater than or equal to "lastImpulseHigh + impulseThresholdPrice", we detect an impulsive bullish move, set "isBullishImpulse" to true, and log the event. If "closePrice" is less than or equal to "lastImpulseLow - impulseThresholdPrice", we identify an impulsive bearish move, set "isBearishImpulse" to true, and log it. If neither condition is met, we print a message stating that no impulsive movement was detected. This logic ensures that only strong breakout continuations qualify as valid order blocks for further processing. To visualize them, we use the following logic.
bool isOrderBlockValid = isBearishImpulse || isBullishImpulse; if (isOrderBlockValid) { datetime blockStartTime = iTime(_Symbol, _Period, consolidationBars + barsToWaitAfterBreakout + 1); double blockTopPrice = lastImpulseHigh; int visibleBarsOnChart = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); datetime blockEndTime = blockStartTime + (visibleBarsOnChart / 1) * PeriodSeconds(); double blockBottomPrice = lastImpulseLow; string orderBlockName = OB_Prefix + "(" + TimeToString(blockStartTime) + ")"; color orderBlockColor = isBullishImpulse ? bullishOrderBlockColor : bearishOrderBlockColor; string orderBlockLabel = isBullishImpulse ? "Bullish OB" : "Bearish OB"; if (ObjectFind(0, orderBlockName) < 0) { //--- Create a rectangle for the order block ObjectCreate(0, orderBlockName, OBJ_RECTANGLE, 0, blockStartTime, blockTopPrice, blockEndTime, blockBottomPrice); ObjectSetInteger(0, orderBlockName, OBJPROP_TIME, 0, blockStartTime); ObjectSetDouble(0, orderBlockName, OBJPROP_PRICE, 0, blockTopPrice); ObjectSetInteger(0, orderBlockName, OBJPROP_TIME, 1, blockEndTime); ObjectSetDouble(0, orderBlockName, OBJPROP_PRICE, 1, blockBottomPrice); ObjectSetInteger(0, orderBlockName, OBJPROP_FILL, true); ObjectSetInteger(0, orderBlockName, OBJPROP_COLOR, orderBlockColor); ObjectSetInteger(0, orderBlockName, OBJPROP_BACK, false); //--- Add a text label in the middle of the order block with dynamic font size datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / 2; double labelPrice = (blockTopPrice + blockBottomPrice) / 2; string labelObjectName = orderBlockName + orderBlockLabel; if (ObjectFind(0, labelObjectName) < 0) { ObjectCreate(0, labelObjectName, OBJ_TEXT, 0, labelTime, labelPrice); ObjectSetString(0, labelObjectName, OBJPROP_TEXT, orderBlockLabel); ObjectSetInteger(0, labelObjectName, OBJPROP_COLOR, labelTextColor); ObjectSetInteger(0, labelObjectName, OBJPROP_FONTSIZE, dynamicFontSize); ObjectSetInteger(0, labelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER); } ChartRedraw(0); //--- Store the order block details in arrays ArrayResize(orderBlockNames, ArraySize(orderBlockNames) + 1); orderBlockNames[ArraySize(orderBlockNames) - 1] = orderBlockName; ArrayResize(orderBlockEndTimes, ArraySize(orderBlockEndTimes) + 1); orderBlockEndTimes[ArraySize(orderBlockEndTimes) - 1] = blockEndTime; ArrayResize(orderBlockMitigatedStatus, ArraySize(orderBlockMitigatedStatus) + 1); orderBlockMitigatedStatus[ArraySize(orderBlockMitigatedStatus) - 1] = false; Print("Order Block created: ", orderBlockName); } }
Here, we determine whether an order block should be created based on the detection of an impulsive move. We first evaluate "isOrderBlockValid" by checking if either "isBearishImpulse" or "isBullishImpulse" is true. If valid, we define key parameters for the order block: "blockStartTime" is obtained using the iTime function to reference the bar at "consolidationBars + barsToWaitAfterBreakout + 1", ensuring it aligns with the identified structure. "blockTopPrice" is set to "lastImpulseHigh", and "blockBottomPrice" to "lastImpulseLow", marking the price range of the order block. We use the ChartGetInteger function to determine "visibleBarsOnChart" and calculate "blockEndTime" dynamically based on PeriodSeconds, ensuring the rectangle remains visible within the chart’s current scope.
The order block name is constructed using "OB_Prefix" and the TimeToString function to include the timestamp for uniqueness. The color and label are determined based on whether the impulse is bullish or bearish, selecting "bullishOrderBlockColor" or "bearishOrderBlockColor", and assigning the respective label.
We then check for the existence of the order block using ObjectFind. If it does not exist, we use the ObjectCreate function to draw a rectangle (OBJ_RECTANGLE) representing the order block, setting its time and price boundaries with ObjectSetInteger and ObjectSetDouble. The rectangle is filled (OBJPROP_FILL), color is applied (OBJPROP_COLOR), and it is drawn in the foreground (OBJPROP_BACK = false).
Next, we create a label inside the order block for better visualization. The label's time ("labelTime") is set at the midpoint of "blockStartTime" and "blockEndTime", while "labelPrice" is calculated as the midpoint of "blockTopPrice" and "blockBottomPrice". We generate a unique label name by appending "orderBlockLabel" to "orderBlockName". If the label does not exist, we create a text object (OBJ_TEXT) with "ObjectCreate", setting the text content (OBJPROP_TEXT), color (OBJPROP_COLOR), font size (OBJPROP_FONTSIZE), and centering it with (OBJPROP_ANCHOR = ANCHOR_CENTER). The ChartRedraw function ensures that the newly created elements appear immediately. Since font size would significantly matter based on the chart scale, we calculate it dynamically as below.
//--- Calculate dynamic font size based on chart scale (0 = zoomed out, 5 = zoomed in) int chartScale = (int)ChartGetInteger(0, CHART_SCALE); // Scale ranges from 0 to 5 int dynamicFontSize = 8 + (chartScale * 2); // Font size: 8 (min) to 18 (max)
Finally, we store the order block details in arrays: "orderBlockNames" (stores object names), "orderBlockEndTimes" (stores expiration times), and "orderBlockMitigatedStatus" (tracks whether the order block has been mitigated). We resize each array dynamically using the ArrayResize function to accommodate new entries, ensuring our order block management remains flexible. A confirmation message is printed to indicate the successful creation of an order block. Finally, we just need to reset the breakout tracking variables.
//--- Reset breakout tracking variables breakoutBarNumber = -1; breakoutTimestamp = 0; lastImpulseHigh = 0; lastImpulseLow = 0; isBullishImpulse = false; isBearishImpulse = false;
Upon compilation and running the program, we have the following outcome.
From the image, we can see that we have confirmed and labeled order blocks that result from the impulsive breakout movements. So now we just need to proceed to validate the mitigated order blocks via continued management of the setups within the chart boundaries.
//--- Process existing order blocks for mitigation and trading for (int j = ArraySize(orderBlockNames) - 1; j >= 0; j--) { string currentOrderBlockName = orderBlockNames[j]; bool doesOrderBlockExist = false; //--- Retrieve order block properties double orderBlockHigh = ObjectGetDouble(0, currentOrderBlockName, OBJPROP_PRICE, 0); double orderBlockLow = ObjectGetDouble(0, currentOrderBlockName, OBJPROP_PRICE, 1); datetime orderBlockStartTime = (datetime)ObjectGetInteger(0, currentOrderBlockName, OBJPROP_TIME, 0); datetime orderBlockEndTime = (datetime)ObjectGetInteger(0, currentOrderBlockName, OBJPROP_TIME, 1); color orderBlockCurrentColor = (color)ObjectGetInteger(0, currentOrderBlockName, OBJPROP_COLOR); //--- Check if the order block is still valid (not expired) if (time(1) < orderBlockEndTime) { doesOrderBlockExist = true; } //--- }
We iterate through "orderBlockNames" in reverse, processing each order block for mitigation and trading. "currentOrderBlockName" stores the name of the block being checked. We use ObjectGetDouble and ObjectGetInteger to retrieve "orderBlockHigh", "orderBlockLow", "orderBlockStartTime", "orderBlockEndTime", and "orderBlockCurrentColor", ensuring precise handling of each order block's properties.
To verify if the order block is still valid, we compare "time(1)" (retrieved using the "time" function) with "orderBlockEndTime". If the current time is within the order block’s lifespan, "doesOrderBlockExist" is set to true, confirming that the order block remains active for further processing. If it does, we proceed to process it and trade it.
//--- Get current market prices double currentAskPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); double currentBidPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Check for mitigation and execute trades if trading is enabled if (enableTrading && orderBlockCurrentColor == bullishOrderBlockColor && close(1) < orderBlockLow && !orderBlockMitigatedStatus[j]) { //--- Sell trade when price breaks below a bullish order block double entryPrice = currentBidPrice; double stopLossPrice = entryPrice + stopLossDistance * _Point; double takeProfitPrice = entryPrice - takeProfitDistance * _Point; obj_Trade.Sell(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice); orderBlockMitigatedStatus[j] = true; ObjectSetInteger(0, currentOrderBlockName, OBJPROP_COLOR, mitigatedOrderBlockColor); string blockDescription = "Bullish Order Block"; string textObjectName = currentOrderBlockName + blockDescription; if (ObjectFind(0, textObjectName) >= 0) { ObjectSetString(0, textObjectName, OBJPROP_TEXT, "Mitigated " + blockDescription); } Print("Sell trade entered upon mitigation of bullish OB: ", currentOrderBlockName); } else if (enableTrading && orderBlockCurrentColor == bearishOrderBlockColor && close(1) > orderBlockHigh && !orderBlockMitigatedStatus[j]) { //--- Buy trade when price breaks above a bearish order block double entryPrice = currentAskPrice; double stopLossPrice = entryPrice - stopLossDistance * _Point; double takeProfitPrice = entryPrice + takeProfitDistance * _Point; obj_Trade.Buy(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice); orderBlockMitigatedStatus[j] = true; ObjectSetInteger(0, currentOrderBlockName, OBJPROP_COLOR, mitigatedOrderBlockColor); string blockDescription = "Bearish Order Block"; string textObjectName = currentOrderBlockName + blockDescription; if (ObjectFind(0, textObjectName) >= 0) { ObjectSetString(0, textObjectName, OBJPROP_TEXT, "Mitigated " + blockDescription); } Print("Buy trade entered upon mitigation of bearish OB: ", currentOrderBlockName); }
We begin by retrieving the current market prices using the SymbolInfoDouble function, ensuring both "currentAskPrice" and "currentBidPrice" are normalized to the appropriate number of decimal places using _Digits. This guarantees precision when placing trades. Next, we check if "enableTrading" is active and whether an order block mitigation condition has been met. Mitigation occurs when a price breaks through an order block, indicating a failure in its holding structure.
For bullish order blocks, we verify if the "close" price of the previous bar (obtained using the "close" function) has dropped below "orderBlockLow" and ensure that this order block has not already been mitigated ("orderBlockMitigatedStatus[j] == false"). If these conditions hold, we place a sell trade using the "Sell" function of the "obj_Trade" object. The trade is executed at "currentBidPrice", with a stop-loss ("stopLossPrice") positioned above the entry price by "stopLossDistance * _Point" and a take-profit ("takeProfitPrice") set below the entry price by "takeProfitDistance * _Point".
Once the trade is executed, the order block is marked as mitigated by updating "orderBlockMitigatedStatus[j]" to true, and its color is changed using ObjectSetInteger to indicate its mitigated state. If a text label exists for this order block (checked using ObjectFind), we update it using ObjectSetString to display "Mitigated Bullish Order Block". A Print statement logs the trade execution for tracking and debugging.
For bearish order blocks, the process is similar. We check if the "close" price has risen above "orderBlockHigh", indicating a break of the bearish order block. If the conditions are met, a buy trade is placed via the "Buy" function, using "currentAskPrice" as the entry price. The "stopLossPrice" is positioned below the entry price, and the "takeProfitPrice" is set above it, ensuring proper risk management. After placing the buy trade, we update "orderBlockMitigatedStatus[j]", change the order block’s color using ObjectSetInteger, and modify the text label (if found) to display "Mitigated Bearish Order Block". Finally, a "Print" statement logs the buy trade execution for monitoring purposes. Here is what we achieve.
Finally, once the blocks are out of bounds, we remove them from the storage arrays.
//--- Remove expired order blocks from arrays if (!doesOrderBlockExist) { bool removedName = ArrayRemove(orderBlockNames, j, 1); bool removedTime = ArrayRemove(orderBlockEndTimes, j, 1); bool removedStatus = ArrayRemove(orderBlockMitigatedStatus, j, 1); if (removedName && removedTime && removedStatus) { Print("Success removing OB DATA from arrays at index ", j); } }
If the order block no longer exists, we remove its name, end time, and mitigation status from their respective arrays using the ArrayRemove function. If all removals are successful, we log the action with a Print statement to confirm the cleanup. Here is a sample cleanup confirmation.
From the image, we can see that we successfully do the blocks cleanup. Now we just need to add a trailing stop logic, and for this, we employ a function to encapsulate everything.
//+------------------------------------------------------------------+ //| Trailing stop function | //+------------------------------------------------------------------+ void applyTrailingStop(double trailingPoints, CTrade &trade_object, int magicNo = 0) { //--- Calculate trailing stop levels based on current market prices double buyStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - trailingPoints * _Point, _Digits); double sellStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + trailingPoints * _Point, _Digits); //--- Loop through all open positions for (int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if (ticket > 0) { if (PositionGetString(POSITION_SYMBOL) == _Symbol && (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)) { //--- Adjust stop loss for buy positions if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && buyStopLoss > PositionGetDouble(POSITION_PRICE_OPEN) && (buyStopLoss > PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)) { trade_object.PositionModify(ticket, buyStopLoss, PositionGetDouble(POSITION_TP)); } //--- Adjust stop loss for sell positions else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && sellStopLoss < PositionGetDouble(POSITION_PRICE_OPEN) && (sellStopLoss < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)) { trade_object.PositionModify(ticket, sellStopLoss, PositionGetDouble(POSITION_TP)); } } } } }
Here, we define the "applyTrailingStop" function to dynamically adjust stop-loss levels for active positions. We begin by calculating "buyStopLoss" and "sellStopLoss" using the current bid/ask prices and the specified "trailingPoints". Next, we loop through all open positions, filtering them by symbol and magic number (if provided). If a buy position has a valid stop-loss level above its entry price, and it either exceeds the current stop-loss or is unset, we update it. Similarly, for sell positions, we ensure the new stop-loss is below the entry price before modifying it.
We then call the function inside the OnTick event handler, to process on every tick and not on every bar this time for real-time price checks as follows.
//--- Apply trailing stop to open positions if enabled if (enableTrailingStop) { applyTrailingStop(trailingStopPoints, obj_Trade, uniqueMagicNumber); }
Upon compilation and running of the program, we have the following outcome.
From the visualization, we can see that the program identifies and verifies all the entry conditions and if validated, opens the respective position with the respective entry parameters, hence achieving our objective. The thing that remains is backtesting the program, and that is handled in the next section.
Backtesting
After thorough backtesting, we have the following results.
Backtest graph:
Backtest report:
Conclusion
In conclusion, we have successfully implemented the Mitigation Order Blocks (MOB) Strategy in MQL5, allowing for precise detection, visualization, and automated trading based on smart money concepts. By integrating breakout validation, impulsive move recognition, and mitigation-based trade execution, our system effectively identifies and processes order blocks while adapting to market dynamics. Additionally, we incorporated trailing stops and risk management mechanisms to optimize trade performance and enhance robustness.
Disclaimer: This article is for educational purposes only. Trading involves significant financial risk, and market conditions can be unpredictable. Proper backtesting and risk management are essential before live deployment.
By leveraging these techniques, you can refine your algorithmic trading strategies and improve order block-based trading efficiency. Keep testing, optimizing, and adapting your approach for long-term success. Best of luck!





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Thank you Allan , nicely put together really tike the visuals and the change colour on mitigated and your handling of the arrays . Thanks for sharing
Thanks for the kind feedback. You're welcome.
It's not taking trades in the strategy tester. Default settings on all pairs. No error messages in the journal. Journal messages present; "No extension: Bar outside range" and "No impulsive movement detected".
Did you even read the article? Because we're sure the article pretty much provides all your answers.