preview
Automating Market Memory Zones Indicator: Where Price is Likely to Return

Automating Market Memory Zones Indicator: Where Price is Likely to Return

MetaTrader 5Examples |
370 0
Hlomohang John Borotho
Hlomohang John Borotho

Table of contents

  1. Introduction
  2. Automation Overview
  3. Getting Started
  4. Backtest Results
  5. Conclusion


Introduction

In the previous discussion, we developed the Market Memory Zones indicator by identifying key areas where price demonstrated meaningful intent—whether through displacement, structural transition, or liquidity sweeps. We translated raw price movement into structured zones that visually represent where imbalances, control shifts, or liquidity events occurred. Rather than marking arbitrary support and resistance, the indicator was designed to highlight locations backed by behavioral evidence: impulsive expansion beyond normal volatility, clear Change of Character (CHoCH) events, and decisive liquidity grabs followed by reversal.

Now, we take the next logical step: transforming this analytical framework into a fully automated Expert Advisor capable of executing trades. Instead of treating zones as passive visual references, the automation process will convert them into structured decision points—detecting their formation in real time, evaluating their validity, and executing trades only when price interaction confirms intent. By embedding confirmation logic, behavioral filters, and risk management rules directly into the system, we move from observation to execution.


Automation Overview

Displacement Zones are automated by detecting unusually strong impulsive candles that signal urgency and imbalance in the market. The EA identifies these zones when a candle’s range significantly exceeds the ATR, has a large body relative to its total range, shows minimal overlap with the previous candle, and closes near its extreme. Once detected, the entire candle range becomes the zone, and its closing direction defines bias. Rather than trading the breakout itself, the EA waits for price to move away and later return to the zone. A buy is triggered in a bullish displacement when price revisits the zone and shows lower-timeframe confirmation, such as rejection or a microstructure shift, with stops placed beyond the zone and targets set at nearby structure highs. The sell scenario mirrors this logic for bearish displacement.

Displacement Zone

Structure Transition Zones are automated by identifying breaks in prior market structure, signaling a change in control between buyers and sellers. The EA tracks swing highs and lows and detects a Change of Character when price breaks against the prevailing trend, such as a swing high being broken in a downtrend. The zone is defined as the last opposing candle before the structural break. In a bullish transition, the EA waits for price to pull back into this zone and confirm strength through higher lows or bullish closes before entering a buy trade. Stops are placed beyond the transition swing, and targets aim for prior range highs or continuation levels. The sell setup follows the same logic in reverse.

Structure Transition Zone

Liquidity Sweep Zones are automated by detecting candles that briefly break above prior highs or below prior lows, then quickly reverse back inside the range. These sweeps often represent stop-hunts followed by strong directional intent. The EA marks the sweep candle’s range as the zone and waits for price to move away before monitoring its return. In a buy scenario, the price sweeps below a low, reverses upward, and later revisits the zone; the EA looks for confirmation, such as failure to create a new low or a bullish engulfing move, before entering. Stops are placed beyond the sweep extreme, and targets aim for opposing liquidity or resistance levels. The sell scenario mirrors this logic after highs are swept.

Liqudity Sweep


Getting Started

//+------------------------------------------------------------------+
//|                                                     Auto MMZ.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/johnhlomohang/"
#property version   "1.00"

#include <Trade/Trade.mqh>
#include <Math/Stat/Math.mqh>
CTrade trade;

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input ENUM_TIMEFRAMES HTF = PERIOD_H1;          // Higher Timeframe
input ENUM_TIMEFRAMES LTF = PERIOD_M5;          // Lower Timeframe
input ENUM_TIMEFRAMES ATF = PERIOD_M15;         // Analysis Timeframe (for zones)
input int             SwingBars = 3;            // Bars for swing detection

// Displacement Zone Parameters
input double          Displacement_ATR_Multiplier = 1.8;    // ATR multiplier for displacement
input double          Displacement_Body_Percent = 60.0;     // Minimum body percentage
input double          Displacement_Overlap_Max = 30.0;      // Maximum overlap with previous candle
input double          Displacement_Close_Extreme = 20.0;    // Close near extreme percentage

// Risk Management
input double          RiskPercent = 1.0;        // Risk per trade (%)
input int             StopLoss_Pips = 150;      // Stop Loss in pips
input double          RiskRewardRatio = 2.0;    // Risk/Reward ratio
input bool            UseDynamicSL = true;      // Use zone-based SL
input bool            UseStructureTP = true;    // Use structure for TP

// Trade Management
input int             MaxTradesPerDay = 10;     // Maximum trades per day
input bool            AllowConsecutiveTrades = true;  // Allow multiple trades in same direction
input double          TrailingStop_ATR = 1.5;   // ATR multiple for trailing stop

// Visualization
input bool            ShowZones = true;         // Show zone rectangles
input bool            ShowArrows = true;        // Show entry arrows
input bool            ShowLabels = true;        // Show trade labels
input bool            DebugMode = true;         // Show debug information

//+------------------------------------------------------------------+
//| Zone Structure                                                   |
//+------------------------------------------------------------------+
enum ZONE_TYPE
{
   ZONE_DISPLACEMENT,
   ZONE_STRUCTURE_TRANSITION,
   ZONE_LIQUIDITY_SWEEP
};

struct TradeZone
{
   datetime          time;
   double            high;
   double            low;
   double            mid;
   ZONE_TYPE         type;
   int               direction;      // 1 for bullish, -1 for bearish
   double            atr;            // ATR at time of zone formation
   datetime          expiry;         // Zone expiry time
   bool              active;
   bool              triggered;
   string            symbol;
   ENUM_TIMEFRAMES   tf;
   
   // Constructor to initialize with default values
   TradeZone()
   {
      Reset();
   }
   
   // Reset method
   void Reset()
   {
      time = 0;
      high = 0;
      low = 0;
      mid = 0;
      type = ZONE_DISPLACEMENT;
      direction = 0;
      atr = 0;
      expiry = 0;
      active = false;
      triggered = false;
      symbol = _Symbol;
      tf = PERIOD_CURRENT;
   }
};

Getting started, we establish the foundation of the Expert Advisor by importing the trading and statistical libraries, initializing the CTrade object for order execution, and defining all configurable inputs that control multi-timeframe analysis, displacement detection rules, risk management, trade management, and visualization behavior. The inputs allow the system to detect zones across higher, lower, and analysis timeframes; evaluate displacement strength using ATR-based conditions; and control risk through percentage-based sizing, stop loss logic, and risk–reward ratios.

We then define an enumerated ZONE_TYPE to classify Displacement, Structure Transition, and Liquidity Sweep zones, followed by a structured TradeZone data model that stores all relevant zone information such as price boundaries, direction, ATR at formation, expiry timing, activation status, and metadata. The constructor and Reset() method ensure every zone starts from a clean, controlled state, providing a structured and scalable framework for managing multiple dynamic market memory zones within the EA.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
SwingPoint htf_lastHigh, htf_prevHigh;
SwingPoint htf_lastLow, htf_prevLow;
SwingPoint atf_lastHigh, atf_prevHigh;
SwingPoint atf_lastLow, atf_prevLow;

TradeZone zones[100];
int zoneCount = 0;

TradeInfo activeTrades[10];
int activeTradeCount = 0;

datetime lastTradeDay = 0;
int tradesToday = 0;

double currentATR = 0;
int atrHandle = -1;

MqlDateTime today;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   // Initialize ATR indicator
   atrHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
   if(atrHandle == INVALID_HANDLE)
   {
      Print("Failed to create ATR indicator");
      return INIT_FAILED;
   }
   
   // Initialize swing points
   htf_lastHigh.Reset();
   htf_prevHigh.Reset();
   htf_lastLow.Reset();
   htf_prevLow.Reset();
   atf_lastHigh.Reset();
   atf_prevHigh.Reset();
   atf_lastLow.Reset();
   atf_prevLow.Reset();
   
   // Initialize zones array
   for(int i = 0; i < ArraySize(zones); i++)
      zones[i].Reset();
   zoneCount = 0;
   
   // Initialize active trades array
   for(int i = 0; i < ArraySize(activeTrades); i++)
      activeTrades[i].Reset();
   activeTradeCount = 0;
   
   // Initialize trade tracking
   lastTradeDay = 0;
   tradesToday = 0;
   
   // Set magic number for trades
   trade.SetExpertMagicNumber(12345);
   
   Print("EA Initialized. Zone Types: Displacement, Structure Transition, Liquidity Sweep");
   
   return INIT_SUCCEEDED;
}

In this section we establish the core global state of the Expert Advisor by declaring swing point trackers for both higher and analysis timeframes, arrays to store up to 100 detected trade zones and 10 active trades, and variables to manage daily trade limits and ATR-based volatility tracking. In the OnInit() function, we initialize the ATR indicator handle to handle volatility calculations, we reset all swing structures to ensure clean starting values, we clear and prepare the zones and active trades arrays, and we reset daily trade counters. Overall, this setup ensures the system starts in a controlled, organized state with properly initialized data structures ready for real-time zone detection and trade execution.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(atrHandle != INVALID_HANDLE)
      IndicatorRelease(atrHandle);
   
   ObjectsDeleteAll(0, "ZONE_");
   ObjectsDeleteAll(0, "TRADE_");
   ChartRedraw();
   
   Print("EA Deinitialized");
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // Update current ATR
   UpdateATR();
   
   // Check for new bars on analysis timeframe
   static datetime lastAtfBar = 0;
   datetime currentAtfBar = iTime(_Symbol, ATF, 0);
   
   if(currentAtfBar != lastAtfBar)
   {
      lastAtfBar = currentAtfBar;
      
      if(DebugMode) Print("New ATF bar: ", TimeToString(currentAtfBar));
      
      // Update market structure
      UpdateMarketStructure();
      
      // Detect new zones
      DetectDisplacementZones();
      DetectStructureTransitionZones();
      DetectLiquiditySweepZones();
      
      // Clean up expired zones
      CleanupZones();
   }
   
   // Check zone entries on every tick
   CheckZoneEntries();
   
   // Manage open positions
   ManageOpenTrades();
   
   // Update visualization
   if(ShowZones) UpdateVisualization();
}

//+------------------------------------------------------------------+
//| Update Market Structure                                          |
//+------------------------------------------------------------------+
void UpdateMarketStructure()
{
   // Update HTF structure
   UpdateSwingPoints(HTF, htf_lastHigh, htf_prevHigh, htf_lastLow, htf_prevLow);
   
   // Update Analysis TF structure
   UpdateSwingPoints(ATF, atf_lastHigh, atf_prevHigh, atf_lastLow, atf_prevLow);
   
   if(DebugMode && atf_lastHigh.price > 0 && atf_lastLow.price > 0)
   {
      Print("Market Structure - ATF High: ", atf_lastHigh.price, 
            " Low: ", atf_lastLow.price,
            " ATR: ", currentATR);
   }
}

The OnDeinit() function ensures a clean shutdown of the Expert Advisor by releasing the ATR indicator handle to free system resources and deleting all chart objects associated with zones and trades using their prefixes. It then refreshes the chart to remove any remaining graphical artifacts and logs a confirmation message. This guarantees that when the EA is removed or restarted, no leftover indicators, drawings, or memory allocations interfere with future executions, maintaining stability and preventing resource leaks.

The OnTick() function serves as the operational engine of the EA, coordinating detection, execution, and management logic in real time. On every tick, it updates the current ATR and checks whether a new bar has formed on the analysis timeframe before recalculating market structure and scanning for new Displacement, Structure Transition, and Liquidity Sweep zones. Expired zones are cleaned up systematically, while entry conditions are evaluated continuously on every tick to ensure timely trade execution. Open trades are actively managed, and visual elements are refreshed when enabled. The UpdateMarketStructure() function supports this workflow by updating swing points on both higher and analysis timeframes, keeping the EA aligned with evolving structural context and volatility conditions.

//+------------------------------------------------------------------+
//| Detect Displacement Zones                                        |
//+------------------------------------------------------------------+
void DetectDisplacementZones()
{
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   int copied = CopyRates(_Symbol, ATF, 0, 5, rates);
   
   if(copied < 2) return;
   
   // Check latest candle for displacement
   MqlRates candle = rates[0];
   MqlRates prevCandle = rates[1];
   
   // Calculate candle metrics
   double range = candle.high - candle.low;
   if(range <= 0) return;
   
   double body = MathAbs(candle.close - candle.open);
   double bodyPercent = (body / range) * 100;
   
   // Calculate overlap with previous candle
   double overlapHigh = MathMin(candle.high, prevCandle.high);
   double overlapLow = MathMax(candle.low, prevCandle.low);
   double overlap = (overlapHigh > overlapLow) ? overlapHigh - overlapLow : 0;
   double overlapPercent = (overlap / range) * 100;
   
   // Check close near extreme (for bullish or bearish)
   double closeToHigh = candle.high - candle.close;
   double closeToLow = candle.close - candle.low;
   double closePercent;
   
   if(candle.close > candle.open) // Bullish candle
      closePercent = (closeToHigh / range) * 100;
   else // Bearish candle
      closePercent = (closeToLow / range) * 100;
   
   // Debug information
   if(DebugMode)
   {
      Print("Checking displacement - Range: ", range, 
            " ATR*Mult: ", currentATR * Displacement_ATR_Multiplier,
            " Body%: ", bodyPercent, " Req: ", Displacement_Body_Percent,
            " Overlap%: ", overlapPercent, " Max: ", Displacement_Overlap_Max,
            " Close%: ", closePercent, " Max: ", Displacement_Close_Extreme);
   }
   
   // Check displacement conditions
   bool isDisplacement = 
      (range >= currentATR * Displacement_ATR_Multiplier) &&
      (bodyPercent >= Displacement_Body_Percent) &&
      (overlapPercent <= Displacement_Overlap_Max) &&
      (closePercent <= Displacement_Close_Extreme);
   
   if(isDisplacement)
   {
      // Check if similar zone already exists (within last 5 bars)
      bool zoneExists = false;
      for(int i = 0; i < zoneCount; i++)
      {
         if(zones[i].active && MathAbs(zones[i].time - candle.time) < PeriodSeconds(ATF) * 5)
         {
            zoneExists = true;
            break;
         }
      }
      
      if(!zoneExists)
      {
         // Create displacement zone
         TradeZone zone;
         zone.time = candle.time;
         zone.high = candle.high;
         zone.low = candle.low;
         zone.mid = (zone.high + zone.low) / 2.0;
         zone.type = ZONE_DISPLACEMENT;
         zone.direction = (candle.close > candle.open) ? 1 : -1;
         zone.atr = currentATR;
         zone.expiry = zone.time + (24 * 60 * 60);  // 24 hour expiry
         zone.active = true;
         zone.triggered = false;
         zone.symbol = _Symbol;
         zone.tf = ATF;
         
         // Add to zones array
         int index = zoneCount % 100;
         zones[index] = zone;
         zoneCount++;
         
         Print("Displacement Zone Detected: ", 
               (zone.direction == 1) ? "BULLISH" : "BEARISH",
               " | High: ", zone.high, " Low: ", zone.low,
               " | Time: ", TimeToString(zone.time));
      }
   }
}

//+------------------------------------------------------------------+
//| Detect Structure Transition Zones (CHoCH)                        |
//+------------------------------------------------------------------+
void DetectStructureTransitionZones()
{
   // Check for bullish CHoCH (downtrend to uptrend)
   if(atf_prevHigh.price > 0 && atf_lastHigh.price > atf_prevHigh.price &&
      atf_prevLow.price > 0 && atf_lastLow.price > atf_prevLow.price)
   {
      // Bullish transition - look for last bearish candle before break
      MqlRates rates[];
      ArraySetAsSeries(rates, true);
      int startBar = MathMax(0, atf_lastHigh.barIndex - 10);
      int copied = CopyRates(_Symbol, ATF, startBar, 20, rates);
      
      if(DebugMode) Print("Checking Bullish CHoCH - PrevHigh: ", atf_prevHigh.price, 
                        " LastHigh: ", atf_lastHigh.price);
      
      for(int i = 0; i < copied; i++)
      {
         if(rates[i].close < rates[i].open)  // Bearish candle
         {
            CreateStructureTransitionZone(rates[i], 1);
            break;
         }
      }
   }
   
   // Check for bearish CHoCH (uptrend to downtrend)
   if(atf_prevLow.price > 0 && atf_lastLow.price < atf_prevLow.price &&
      atf_prevHigh.price > 0 && atf_lastHigh.price < atf_prevHigh.price)
   {
      // Bearish transition - look for last bullish candle before break
      MqlRates rates[];
      ArraySetAsSeries(rates, true);
      int startBar = MathMax(0, atf_lastLow.barIndex - 10);
      int copied = CopyRates(_Symbol, ATF, startBar, 20, rates);
      
      if(DebugMode) Print("Checking Bearish CHoCH - PrevLow: ", atf_prevLow.price, 
                        " LastLow: ", atf_lastLow.price);
      
      for(int i = 0; i < copied; i++)
      {
         if(rates[i].close > rates[i].open)  // Bullish candle
         {
            CreateStructureTransitionZone(rates[i], -1);
            break;
         }
      }
   }
}

The DetectDisplacementZones() function scans the most recent candle on the analysis timeframe and evaluates whether it qualifies as a true displacement move using strict quantitative filters. It calculates the candle’s total range, body size percentage, overlap with the previous candle, and how close the close is to the candle’s extreme. These metrics are compared against ATR-based and percentage thresholds to confirm strong, low-overlap, high-momentum intent. If all displacement conditions are satisfied and no similar recent zone already exists, the function constructs a new TradeZone object, assigns its price boundaries, midpoint, direction, ATR context, expiry time, and activation status, and then stores it in the zones array. This ensures that only statistically significant impulsive moves are recorded as potential return zones.

The DetectStructureTransitionZones() function focuses on identifying Change of Character (CHoCH) events by analyzing swing structure shifts on the analysis timeframe. A bullish transition is detected when both higher highs and higher lows form after a prior downtrend, while a bearish transition is identified when lower lows and lower highs break a prior uptrend. Once a structural shift is confirmed, the function searches backward to locate the last opposing candle before the break—bearish for bullish transitions and bullish for bearish transitions—and uses it to create a Structure Transition Zone via a dedicated creation function.

//+------------------------------------------------------------------+
//| Detect Liquidity Sweep Zones                                     |
//+------------------------------------------------------------------+
void DetectLiquiditySweepZones()
{
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   int copied = CopyRates(_Symbol, ATF, 0, 5, rates);
   
   if(copied < 3) return;
   
   // Check for buy-side liquidity sweep (sweep of lows)
   if(atf_lastLow.price > 0 && rates[1].low < atf_lastLow.price)
   {
      // Check if there's a bullish reversal after the sweep
      bool bullishReversal = rates[1].close > rates[1].open || 
                            (rates[0].close > rates[0].open && rates[0].close > rates[1].high);
      
      if(bullishReversal)
      {
         if(DebugMode) Print("Buy-side Liquidity Sweep detected - Swept Low: ", atf_lastLow.price,
                           " Sweep Candle Low: ", rates[1].low);
         
         CreateLiquiditySweepZone(rates[1], 1);  // Bullish reversal
      }
   }
   
   // Check for sell-side liquidity sweep (sweep of highs)
   if(atf_lastHigh.price > 0 && rates[1].high > atf_lastHigh.price)
   {
      // Check if there's a bearish reversal after the sweep
      bool bearishReversal = rates[1].close < rates[1].open || 
                            (rates[0].close < rates[0].open && rates[0].close < rates[1].low);
      
      if(bearishReversal)
      {
         if(DebugMode) Print("Sell-side Liquidity Sweep detected - Swept High: ", atf_lastHigh.price,
                           " Sweep Candle High: ", rates[1].high);
         
         CreateLiquiditySweepZone(rates[1], -1);  // Bearish reversal
      }
   }
}

//+------------------------------------------------------------------+
//| Create Structure Transition Zone                                 |
//+------------------------------------------------------------------+
void CreateStructureTransitionZone(MqlRates &candle, int direction)
{
   // Check if similar zone already exists (within last 5 bars)
   bool zoneExists = false;
   for(int i = 0; i < zoneCount; i++)
   {
      if(zones[i].active && zones[i].type == ZONE_STRUCTURE_TRANSITION &&
         MathAbs(zones[i].time - candle.time) < PeriodSeconds(ATF) * 5)
      {
         zoneExists = true;
         break;
      }
   }
   
   if(!zoneExists)
   {
      TradeZone zone;
      zone.time = candle.time;
      zone.high = candle.high;
      zone.low = candle.low;
      zone.mid = (zone.high + zone.low) / 2.0;
      zone.type = ZONE_STRUCTURE_TRANSITION;
      zone.direction = direction;
      zone.atr = currentATR;
      zone.expiry = zone.time + (12 * 60 * 60);  // 12 hour expiry
      zone.active = true;
      zone.triggered = false;
      zone.symbol = _Symbol;
      zone.tf = ATF;
      
      // Add to zones array
      int index = zoneCount % 100;
      zones[index] = zone;
      zoneCount++;
      
      Print("Structure Transition Zone Detected: ",
            (zone.direction == 1) ? "BULLISH (CHoCH)" : "BEARISH (CHoCH)",
            " | High: ", zone.high, " Low: ", zone.low,
            " | Time: ", TimeToString(zone.time));
   }
}

The DetectLiquiditySweepZones() function automates the identification of stop-hunt behavior by scanning recent candles for sweeps beyond prior swing highs or lows. It checks whether the price temporarily breaks a known swing level and then confirms that a reversal follows, either within the same candle or the next one. If a prior low is swept and followed by bullish confirmation, a buy-side liquidity sweep zone is created; conversely, if a prior high is swept and followed by bearish confirmation, a sell-side liquidity sweep zone is formed. Debug outputs provide transparency during detection.

The CreateStructureTransitionZone() function formalizes the creation of a CHoCH-based zone once a structural break has been identified. Before creating a new zone, it first checks whether a similar active Structure Transition zone already exists within a recent time window to prevent duplication. If no recent equivalent zone is found, it initializes a new TradeZone object using the candle that preceded the structural break, assigning its price boundaries, midpoint, direction (bullish or bearish), ATR context, and a 12-hour expiry period. The zone is then stored in the zones array using a rolling index mechanism, activated for monitoring, and logged to the terminal, ensuring that each structural transition is cleanly recorded and managed within the EA’s memory framework.

//+------------------------------------------------------------------+
//| Check Zone Entries                                               |
//+------------------------------------------------------------------+
void CheckZoneEntries()
{
   // Reset daily trade count if new day
   TimeToStruct(TimeCurrent(), today);
   if(lastTradeDay != today.day)
   {
      tradesToday = 0;
      lastTradeDay = today.day;
      if(DebugMode) Print("New day started. Trade counter reset.");
   }
   
   // Check if we can take more trades today
   if(MaxTradesPerDay > 0 && tradesToday >= MaxTradesPerDay)
   {
      if(tradesToday == MaxTradesPerDay)
         Print("Maximum daily trades (", MaxTradesPerDay, ") reached for today.");
      return;
   }
   
   double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   
   for(int i = 0; i < zoneCount; i++)
   {
      if(!zones[i].active || zones[i].triggered) continue;
      
      // Check if zone is expired
      if(TimeCurrent() > zones[i].expiry)
      {
         zones[i].active = false;
         continue;
      }
      
      // Check BUY entries (bullish zones)
      if(zones[i].direction == 1)
      {
         // Check if price is returning to zone (using Ask for buys)
         if(currentAsk >= zones[i].low && currentAsk <= zones[i].high)
         {
            // Get LTF confirmation
            if(CheckLTFConfirmation(true))
            {
               Print("BUY Signal: Price in ", ZoneTypeToString(zones[i].type), 
                     " zone | Entry: ", currentAsk, " Zone: [", zones[i].low, "-", zones[i].high, "]");
               ExecuteZoneTrade(true, zones[i], currentAsk);
               zones[i].triggered = true;
               tradesToday++;
               break;  // Take only one trade per tick
            }
         }
      }
      // Check SELL entries (bearish zones)
      else if(zones[i].direction == -1)
      {
         // Check if price is returning to zone (using Bid for sells)
         if(currentBid >= zones[i].low && currentBid <= zones[i].high)
         {
            // Get LTF confirmation
            if(CheckLTFConfirmation(false))
            {
               Print("SELL Signal: Price in ", ZoneTypeToString(zones[i].type), 
                     " zone | Entry: ", currentBid, " Zone: [", zones[i].low, "-", zones[i].high, "]");
               ExecuteZoneTrade(false, zones[i], currentBid);
               zones[i].triggered = true;
               tradesToday++;
               break;  // Take only one trade per tick
            }
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Check LTF Confirmation                                           |
//+------------------------------------------------------------------+
bool CheckLTFConfirmation(bool forBuy)
{
   MqlRates ltfRates[];
   ArraySetAsSeries(ltfRates, true);
   int copied = CopyRates(_Symbol, LTF, 0, 5, ltfRates);
   
   if(copied < 3) return false;
   
   // For BUY confirmation
   if(forBuy)
   {
      // Simple confirmation: bullish candle closing above midpoint
      if(ltfRates[0].close > ltfRates[0].open && 
         ltfRates[0].close > ((ltfRates[0].high + ltfRates[0].low) / 2))
      {
         return true;
      }
      
      // Alternative: bullish engulfing
      if(ltfRates[0].close > ltfRates[0].open && 
         ltfRates[0].close > ltfRates[1].high && 
         ltfRates[0].open < ltfRates[1].low)
      {
         return true;
      }
   }
   // For SELL confirmation
   else
   {
      // Simple confirmation: bearish candle closing below midpoint
      if(ltfRates[0].close < ltfRates[0].open && 
         ltfRates[0].close < ((ltfRates[0].high + ltfRates[0].low) / 2))
      {
         return true;
      }
      
      // Alternative: bearish engulfing
      if(ltfRates[0].close < ltfRates[0].open && 
         ltfRates[0].close < ltfRates[1].low && 
         ltfRates[0].open > ltfRates[1].high)
      {
         return true;
      }
   }
   
   return false;
}

Here, we implement the core execution engine that decides when a detected Market Memory Zone becomes a live trade. The function begins by managing daily trade limits, resetting the counter at the start of a new trading day, and preventing further entries once the maximum number of trades is reached. It then retrieves the current Bid and Ask prices and iterates through all active, non-triggered zones. For each zone, it first checks expiry conditions to ensure outdated zones are deactivated. If the price returns inside a valid zone’s boundaries, the EA evaluates whether it is a bullish or bearish setup and proceeds to request lower timeframe confirmation before executing a trade. Once a trade is placed, the zone is marked as triggered to prevent duplicate entries, and the daily trade count is incremented, ensuring disciplined execution control.

The CheckLTFConfirmation() function provides behavioral validation before any order is sent, acting as a refinement filter to avoid blind level trading. For buy setups, it confirms either a strong bullish candle closing above its midpoint or a bullish engulfing pattern; for sell setups, it looks for the bearish equivalents. By requiring momentum evidence from the lower timeframe, the EA ensures that entries are based on reaction and intent rather than mere price contact within a zone.

//+------------------------------------------------------------------+
//| Execute Zone Trade                                               |
//+------------------------------------------------------------------+
void ExecuteZoneTrade(bool isBuy, TradeZone &zone, double entryPrice)
{
   // Check if position already exists for this symbol
   if(PositionSelect(_Symbol))
   {
      if(!AllowConsecutiveTrades)
      {
         Print("Position already exists and AllowConsecutiveTrades is false, skipping trade");
         return;
      }
   }
   
   ENUM_ORDER_TYPE orderType = isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
   
   // Calculate stop loss
   double stopLoss = CalculateZoneStopLoss(isBuy, zone, entryPrice);
   
   // Calculate take profit
   double takeProfit = CalculateZoneTakeProfit(isBuy, zone, entryPrice, stopLoss);
   
   // Calculate lot size
   double volume = CalculateLotSize(entryPrice, stopLoss);
   
   if(volume <= 0)
   {
      Print("Invalid lot size calculated: ", volume, ". Trade cancelled.");
      return;
   }
   
   // Execute trade
   string comment = StringFormat("%s_%s_%s",
      (isBuy ? "BUY" : "SELL"),
      ZoneTypeToString(zone.type),
      TimeToString(TimeCurrent(), TIME_MINUTES));
   
   bool success = trade.PositionOpen(_Symbol, orderType, volume, 
                                     entryPrice, stopLoss, takeProfit, comment);
   
   if(success)
   {
      ulong ticket = trade.ResultOrder();
      Print("Trade Executed Successfully: ", comment,
            " | Ticket: ", ticket,
            " | Entry: ", entryPrice,
            " | SL: ", stopLoss,
            " | TP: ", takeProfit,
            " | Lots: ", volume);
      
      // Store trade info
      if(activeTradeCount < ArraySize(activeTrades))
      {
         activeTrades[activeTradeCount].entryTime = TimeCurrent();
         activeTrades[activeTradeCount].entryPrice = entryPrice;
         activeTrades[activeTradeCount].stopLoss = stopLoss;
         activeTrades[activeTradeCount].takeProfit = takeProfit;
         activeTrades[activeTradeCount].direction = isBuy ? 1 : -1;
         activeTrades[activeTradeCount].zone = zone;
         activeTrades[activeTradeCount].ticket = ticket;
         activeTradeCount++;
      }
   }
   else
   {
      Print("Trade failed: Error Code=", trade.ResultRetcode(),
            " Description=", trade.ResultRetcodeDescription());
   }
}

//+------------------------------------------------------------------+
//| Calculate Zone Stop Loss                                         |
//+------------------------------------------------------------------+
double CalculateZoneStopLoss(bool isBuy, TradeZone &zone, double entryPrice)
{
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   
   if(UseDynamicSL)
   {
      // Zone-based stop loss with buffer
      double buffer = 50 * point;  // 50 pip buffer
      if(isBuy)
      {
         double sl = zone.low - buffer;
         // Ensure SL is below entry
         if(sl >= entryPrice)
            sl = entryPrice - (100 * point);
         return NormalizeDouble(sl, _Digits);
      }
      else
      {
         double sl = zone.high + buffer;
         // Ensure SL is above entry
         if(sl <= entryPrice)
            sl = entryPrice + (100 * point);
         return NormalizeDouble(sl, _Digits);
      }
   }
   else
   {
      // Fixed pip stop loss
      double slDistance = StopLoss_Pips * 10 * point;
      if(isBuy)
         return NormalizeDouble(entryPrice - slDistance, _Digits);
      else
         return NormalizeDouble(entryPrice + slDistance, _Digits);
   }
}

//+------------------------------------------------------------------+
//| Calculate Zone Take Profit                                       |
//+------------------------------------------------------------------+
double CalculateZoneTakeProfit(bool isBuy, TradeZone &zone, double entry, double sl)
{
   double risk = MathAbs(entry - sl);
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   
   if(UseStructureTP)
   {
      if(isBuy)
      {
         // Take profit at next structure high
         double target = entry + (risk * RiskRewardRatio);
         
         if(atf_lastHigh.price > entry)
            target = atf_lastHigh.price;
         else if(htf_lastHigh.price > entry)
            target = htf_lastHigh.price;
         
         // Ensure minimum distance
         double minDistance = 50 * point;
         if(target - entry < minDistance)
            target = entry + (risk * RiskRewardRatio);
         
         return NormalizeDouble(target, _Digits);
      }
      else
      {
         // Take profit at next structure low
         double target = entry - (risk * RiskRewardRatio);
         
         if(atf_lastLow.price < entry)
            target = atf_lastLow.price;
         else if(htf_lastLow.price < entry)
            target = htf_lastLow.price;
         
         // Ensure minimum distance
         double minDistance = 50 * point;
         if(entry - target < minDistance)
            target = entry - (risk * RiskRewardRatio);
         
         return NormalizeDouble(target, _Digits);
      }
   }
   else
   {
      // Fixed risk/reward ratio
      if(isBuy)
         return NormalizeDouble(entry + (risk * RiskRewardRatio), _Digits);
      else
         return NormalizeDouble(entry - (risk * RiskRewardRatio), _Digits);
   }
}

The ExecuteZoneTrade() function manages the complete process of placing a trade once a valid zone entry has been confirmed. It first checks whether a position already exists for the symbol and respects the AllowConsecutiveTrades setting to prevent unwanted stacking of trades. The function then determines the order type (buy or sell), calculates a stop loss and take profit using dedicated zone-based logic, and computes the appropriate lot size based on risk parameters. If the calculated volume is valid, it sends the trade using PositionOpen() and attaches a structured comment that includes direction, zone type, and timestamp. Upon successful execution, the trade details are stored in the activeTrades array for future management and tracking; if the order fails, the system logs the broker return code and description for debugging purposes.

The stop-loss and take-profit calculations are designed to align with the Market Memory concept. When UseDynamicSL is enabled, the stop loss is positioned beyond the zone boundary with a safety buffer, ensuring logical invalidation if price fully breaches the zone; otherwise, a fixed pip-based stop is applied. The take profit can dynamically target recent structural highs or lows from the analysis or higher timeframe when UseStructureTP is enabled, while still respecting a minimum distance requirement to avoid overly tight targets. If structure-based targeting is disabled, a fixed risk–reward multiple is applied instead.

//+------------------------------------------------------------------+
//| Update Visualization                                             |
//+------------------------------------------------------------------+
void UpdateVisualization()
{
   ObjectsDeleteAll(0, "ZONE_");
   ObjectsDeleteAll(0, "TRADE_");
   
   // Draw zones
   int activeZoneCount = 0;
   for(int i = 0; i < zoneCount; i++)
   {
      if(zones[i].active)
      {
         DrawZone(zones[i], i);
         activeZoneCount++;
      }
   }
   
   // Draw structure levels
   if(ShowLabels)
   {
      if(atf_lastHigh.price > 0)
         CreateHorizontalLine("ATF_High", atf_lastHigh.price, clrRed, STYLE_DASH);
      
      if(atf_lastLow.price > 0)
         CreateHorizontalLine("ATF_Low", atf_lastLow.price, clrBlue, STYLE_DASH);
      
      if(htf_lastHigh.price > 0)
         CreateHorizontalLine("HTF_High", htf_lastHigh.price, clrDarkRed, STYLE_DOT);
      
      if(htf_lastLow.price > 0)
         CreateHorizontalLine("HTF_Low", htf_lastLow.price, clrDarkBlue, STYLE_DOT);
   }
   
   // Show zone count in corner
   if(ShowLabels)
   {
      string labelText = "Active Zones: " + IntegerToString(activeZoneCount);
      CreateLabel("ZoneCount", labelText, 10, 180, clrWhite);
   }
}

//+------------------------------------------------------------------+
//| Draw Zone                                                        |
//+------------------------------------------------------------------+
void DrawZone(TradeZone &zone, int index)
{
   string name = "ZONE_" + IntegerToString(index);
   
   // Calculate zone width (4 bars forward)
   datetime endTime = zone.time + (PeriodSeconds(zone.tf) * 4);
   
   // Draw rectangle
   if(ObjectCreate(0, name, OBJ_RECTANGLE, 0, zone.time, zone.high, endTime, zone.low))
   {
      // Set color based on zone type and direction
      color zoneColor;
      int zoneStyle = STYLE_SOLID;
      
      switch(zone.type)
      {
         case ZONE_DISPLACEMENT:
            zoneColor = (zone.direction == 1) ? clrLimeGreen : clrIndianRed;
            zoneStyle = STYLE_SOLID;
            break;
         case ZONE_STRUCTURE_TRANSITION:
            zoneColor = (zone.direction == 1) ? clrDodgerBlue : clrOrangeRed;
            zoneStyle = STYLE_DASH;
            break;
         case ZONE_LIQUIDITY_SWEEP:
            zoneColor = (zone.direction == 1) ? clrGold : clrMagenta;
            zoneStyle = STYLE_DOT;
            break;
         default:
            zoneColor = clrGray;
      }
      
      ObjectSetInteger(0, name, OBJPROP_COLOR, zoneColor);
      ObjectSetInteger(0, name, OBJPROP_STYLE, zoneStyle);
      ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);
      ObjectSetInteger(0, name, OBJPROP_BACK, true);
      ObjectSetInteger(0, name, OBJPROP_FILL, true);
      ObjectSetInteger(0, name, OBJPROP_ZORDER, 0);
      
      // Add label
      if(ShowLabels)
      {
         string labelName = name + "_LABEL";
         if(ObjectCreate(0, labelName, OBJ_TEXT, 0, zone.time, zone.high))
         {
            string typeText;
            switch(zone.type)
            {
               case ZONE_DISPLACEMENT: typeText = "DISP"; break;
               case ZONE_STRUCTURE_TRANSITION: typeText = "CHoCH"; break;
               case ZONE_LIQUIDITY_SWEEP: typeText = "LIQ"; break;
               default: typeText = "UNK";
            }
            
            string directionText = (zone.direction == 1) ? "BULL" : "BEAR";
            ObjectSetString(0, labelName, OBJPROP_TEXT, typeText + " " + directionText);
            ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrWhite);
            ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8);
            ObjectSetInteger(0, labelName, OBJPROP_BACK, true);
         }
      }
      
      if(DebugMode && ShowZones)
         Print("Drawing zone: ", name, " Type: ", ZoneTypeToString(zone.type), 
               " Dir: ", zone.direction, " High: ", zone.high, " Low: ", zone.low);
   }
}

The UpdateVisualization() function manages all graphical elements on the chart to ensure the display accurately reflects the EA’s current internal state. It begins by clearing previously drawn zones and trade objects to prevent duplication or outdated visuals. It then loops through all stored zones and redraws only those that remain active, keeping the chart synchronized with live market memory logic. If label display is enabled, the function also plots horizontal lines representing the most recent swing highs and lows from both the analysis and higher timeframes, giving clear structural context.

The DrawZone() function handles the detailed rendering of each individual zone. It creates a forward-projecting rectangle from the zone’s origin time and price boundaries, extending it several bars into the future to visualize its area of influence. Each zone type—Displacement, Structure Transition (CHoCH), or Liquidity Sweep—is assigned a distinct color and line style, while bullish and bearish directions are visually differentiated. Optional text labels identify the zone type and direction (e.g., “DISP BULL” or “CHoCH BEAR”), making it easy to interpret at a glance.


Back Test Results

The back-testing was evaluated on the H1 timeframe across a 2-month testing window (01 June 2025 to 14 August 2025), with the default settings:

Equity Curve

Back Test Results


Conclusion

In summary, we transformed the Market Memory Zones concept from a visual analytical tool into a fully automated trading engine by encoding its structural logic directly into the Expert Advisor. We implemented systematic detection of displacement candles, structure transitions (CHoCH), and liquidity sweeps, then stored these zones in a controlled circular buffer to ensure long-term stability during backtesting and live execution. Each zone now carries contextual data such as direction, ATR conditions, expiry timing, and structure references, allowing the EA to monitor price behavior in real time. Automated entry logic, lower-timeframe confirmation, dynamic stop-loss placement, structure-based take-profit targeting, and risk-based position sizing complete the transformation from discretionary interpretation to rule-driven execution.

In conclusion, automating Market Memory Zones removes emotional bias while preserving the structural intelligence behind the strategy. The system continuously tracks where price is likely to return, validates reactions with multi-timeframe confirmation, and executes trades with predefined risk management—making the entire process consistent, scalable, and testable. By converting price memory into structured, programmable logic, traders gain clarity, repeatability, and the ability to optimize performance across instruments and timeframes.

Attached files |
Auto_MMZ.mq5 (41.48 KB)
Risk Management (Part 5): Integrating the Risk Management System into an Expert Advisor Risk Management (Part 5): Integrating the Risk Management System into an Expert Advisor
In this article, we will implement the risk management system developed in previous publications and add the Order Blocks indicator described in other articles. In addition, we will run a backtest so we can compare results with the risk management system enabled and evaluate the impact of dynamic risk.
Introduction to MQL5 (Part 40): Beginner Guide to File Handling in MQL5 (II) Introduction to MQL5 (Part 40): Beginner Guide to File Handling in MQL5 (II)
Create a CSV trading journal in MQL5 by reading account history over a defined period and writing structured records to file. The article explains deal counting, ticket retrieval, symbol and order type decoding, and capturing entry (lot, time, price, SL/TP) and exit (time, price, profit, result) data with dynamic arrays. The result is an organized, persistent log suitable for analysis and reporting.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
MQL5 Trading Tools (Part 18): Rounded Speech Bubbles/Balloons with Orientation Control MQL5 Trading Tools (Part 18): Rounded Speech Bubbles/Balloons with Orientation Control
This article shows how to build rounded speech bubbles in MQL5 by combining a rounded rectangle with a pointer triangle and controlling orientation (up, down, left, right). It details geometry precomputation, supersampled filling, rounded apex arcs, and segmented borders with an extension ratio for seamless joins. Readers get configurable code for size, radii, colors, opacity, and thickness, ready for alerts or tooltips in trading interfaces.