preview
Interactive Supply and Demand Zone Manager in MQL5: From Manual to Automated Lifecycle

Interactive Supply and Demand Zone Manager in MQL5: From Manual to Automated Lifecycle

MetaTrader 5Examples |
175 0
Francis Nyoike Thumbi
Francis Nyoike Thumbi

The Limitation of Static Objects

A key limitation in MetaTrader 5 is that the native OBJ_RECTANGLE is a "dumb" object. Once a rectangle is placed, it has no inherent awareness of price action. It possesses no internal logic to detect when a candle closes outside its boundaries, how to scale its height when volatility spikes, or how to distinguish between a manual adjustment and an automated update.

Essentially, a standard rectangle has no awareness of:

  • Market State: Is price currently above or below the level?
  • Breakout Conditions: Has the level been breached by a close or just a wick?
  • Mitigation Logic: Has the zone been tested enough to be considered "weak"?
  • Structural Aging: Is the zone still relevant in the current context?

This forces the trader into a loop of continuous manual maintenance: extending rectangles into future candles, recoloring tested zones, and removing invalid structures. Over time, chart management itself becomes the problem.

To solve this, we move beyond simple drawings and toward managed entities. By wrapping the rectangle in a custom CZone class, we give each chart shape persistent state and decision logic. This framework allows zones to categorize themselves automatically, detect breakouts, enter "ghost" states, and eventually deactivate without manual intervention.

The framework introduced in this article will allow zones to:

  •  be categorized automatically with live prices to determine whether they are support or resistance,
  •  transition between structural states,
  •  detect breakouts,
  •  enter ghost states,
  •  and eventually deactivate themselves without manual intervention.

The system is a coordinated lifecycle pipeline. Each stage handles a specific task: synchronization, detection, validation, rendering, or state management.

Rather than operating as isolated functions, the system is designed as a coordinated lifecycle pipeline where each stage is responsible for a specific task such as synchronization, detection, validation, rendering, and state management.

The following flowchart provides an overview of how manual user interaction and automated market analysis move through the framework to produce fully dynamic, self-managed zones.

Before building the synchronization logic itself, we first need an architecture capable of storing and managing structural information efficiently during runtime execution.

For this, the system uses the standard MQL5 dynamic object container CArrayObj.

//+------------------------------------------------------------------+
//|                                                   DynamicSR.mq5  |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd. "
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

//--- Include Standard Libraries for Dynamic Tracking
#include <Arrays\ArrayObj.mqh>
#include <Arrays\ArrayString.mqh>
#include <Trade\Trade.mqh>

The reason we use CArrayObj instead of raw structural arrays is because the engine continuously creates, updates, merges, invalidates, and deletes zone objects during live execution. Traditional arrays become inefficient when handling variable-sized object collections that require frequent memory reallocations. CArrayObj solves this by storing class pointers dynamically while handling indexing and cleanup internally. With the memory infrastructure prepared, we can now define the structural state machine responsible for tracking the lifecycle of every zone.

//+------------------------------------------------------------------+ //| System Enumerations                                              | //+------------------------------------------------------------------+ enum ENUM_ZONE_TYPE   {    ZONE_SUPPORT,    ZONE_RESISTANCE,    ZONE_NEUTRAL   }; enum ENUM_ZONE_STATUS   {    ZONE_VIRGIN,    ZONE_BROKEN,    ZONE_INACTIVE   }; enum ENUM_ZONE_SOURCE   {    ZS_USER,    ZS_EA   };

These enumerations act as the behavioral foundation of the entire framework. Instead of treating zones as simple graphical objects, the engine now treats them as entities capable of transitioning between multiple structural states during their lifetime. For example, a newly created untouched zone begins as ZONE_VIRGIN, a structurally violated zone transitions into ZONE_BROKEN, and old irrelevant structures eventually become ZONE_INACTIVE. This state-driven architecture becomes critical later when we begin the automation of:

  •  recoloring,
  •  breakout logic,
  •  ghost-state transitions,
  •  and cleanup behavior.

With the structural states now defined, the next step is configuring the runtime parameters responsible for controlling how the engine reacts to live market conditions.

//+------------------------------------------------------------------+ //| Input Configuration Parameters                                   | //+------------------------------------------------------------------+ input group "--- Core Engine Settings ---" input int      ApproachPips        = 10;       // Proximity alert aura distance in pips input int      LookbackBars        = 300;      // Historical structural scanning depth input int      SwingBars           = 5;        // Symmetrical bars required for structural pivot input double   MergeSensitivity    = 1.5;      // Proximity threshold multiplier for merging zones input group "--- Breakout & Ghost State Settings ---" input double   BreakBufferPct      = 0.10;     // Percentage of zone height required for breakout penetration input int      BreakConfirmCloses  = 2;        // Number of consecutive candle closes required to confirm breakout input int      GhostBarLimit       = 50;       // Lifetime limit in bars for broken hollow zones input bool     AllowResurrection   = true;     // Re-activate ghost zones if price closes back inside range

At this stage, the framework begins separating itself from traditional static support and resistance objects. We are no longer only concerned with where a zone exists.

We now begin by defining how zones age, how breakouts are confirmed, how long broken structures remain visible, and whether previously invalidated zones are allowed to reactivate themselves after price re-enters the range.

This introduces the concept of structural lifecycle management.

A zone is no longer permanent. It becomes a living object operating under state-based behavioral rules. To manage these runtime entities globally, we now create the central tracking containers responsible for storing active structural objects during execution.

//+------------------------------------------------------------------+ //| Global Tracking Containers                                       | //+------------------------------------------------------------------+ CArrayObj      active_zones;                   // Active structural supply and demand tracking arrays CArrayString   blacklist;                      // Blacklisted names of manually deleted automated zones datetime       last_bar_time       = 0;        // Track bar time shifts to execute periodic functions int            atr_handle          = INVALID_HANDLE; // Core handle for structural ATR calculation

The active zones container acts as the engine's structural registry. Every zone detected manually or algorithmically is stored inside this dynamic collection, allowing the framework to:

  •  iterate through active structures,
  •  synchronize graphical updates,
  •  evaluate breakouts,
  •  merge overlapping zones,
  •  and safely destroy old objects from memory.

The blacklist container serves a different purpose. If a trader manually deletes an automatically generated zone, the engine stores the object's name inside this blacklist to prevent the system from recreating it repeatedly during future scans.

At this point, the infrastructure responsible for storing structural entities is complete.The next challenge is transforming a native MetaTrader rectangle into an intelligent object capable of carrying its own:

  •  pricing metrics,
  •  lifecycle information,
  •  breakout tracking,
  •  visual states,
  •  and behavioral logic.

For this, we introduce the core wrapper architecture of the framework: the CZone class.


The Core Object Wrapper: CZone Class Architecture

To make a native MetaTrader rectangle behave like a stateful object, we wrap it in an object-oriented class. CZone inherits from CObject, so each zone can be stored, indexed, and tracked in the global active_zones container.Instead of unmanaged chart coordinates, the class stores pricing metrics, breakout counters, and visual settings in one entity.

//+------------------------------------------------------------------+
//| Zone Wrapper Class Architecture                                  |
//+------------------------------------------------------------------+
class CZone : public CObject
  {
public:
   string            name;                        // Unique chart object identifier
   double            price_level;                 // Median reference price of the structure
   double            top;                         // Upper boundary ceiling price
   double            bottom;                      // Lower boundary floor price
   double            height;                      // Absolute distance within the zone range

   //--- Ghost Tracking Variables
   bool              is_broken;                  // Flag indicating the zone has been penetrated
   datetime          broken_time;                // Exact timestamp when breakout confirmation occurred
   int               consecutive_closes_outside; // Counter for closing candles beyond boundaries

   ENUM_ZONE_TYPE    type;                       // Structural classification (Support/Resistance)
   ENUM_ZONE_STATUS  status;                     // Operational state tracking (Virgin/Broken/Inactive)
   bool              is_user_zone;               // Identifies if manually drawn or auto-generated
   bool              was_drawn;                  // Confirmed visual presence on active chart
   datetime          created;                    // Original generation timestamp
   datetime          last_touch_time;            // Last recorded price test interaction

   bool              buy_triggered;              // Internal signal flag for support interaction
   bool              sell_triggered;             // Internal signal flag for resistance interaction
   double            strength;                   // Structural weight ranking score

   //--- Default Constructor
                     CZone()
     {
      is_broken = false;
      broken_time = 0;
      consecutive_closes_outside = 0;
      was_drawn = false;
     }

   //--- Primary Algorithmic Constructor (Optimized)
                     CZone(double p, ENUM_ZONE_TYPE t, datetime ct, double current_atr)
     {
      price_level = p;
      type = t;
      status = ZONE_VIRGIN;
      is_user_zone = false;
      was_drawn = false;
      is_broken = false;
      broken_time = 0;

      //--- Dynamic Volatility Sizing via Cached Environment Param
      double zone_height = 0.0;
      if(current_atr > 0.0)
        {
         zone_height = current_atr * 0.5; // Zone size scales directly with volatility
        }
      else
        {
         zone_height = 20.0 * _Point * 10.0; // Fail-safe default size if data is empty
        }

      //--- Geometric Coordinate Calculations
      top = (type == ZONE_RESISTANCE) ? p : p + zone_height;
      bottom = (type == ZONE_RESISTANCE) ? p - zone_height : p;
      height = top - bottom;

      created = ct;
      last_touch_time = ct;
      consecutive_closes_outside = 0;
      buy_triggered = false;
      sell_triggered = false;
      strength = 1.0;

      //--- Unique System Name Generation
      name = (type == ZONE_SUPPORT ? "S_" : "R_") + EnumToString(_Period) + "_" +
             DoubleToString(p, _Digits) + "_" + IntegerToString((long)ct);
     }

   //--- Proximity Border Validation Method
   bool              IsPriceNearZone(double price)
     {
      double pipMultiplier = (_Digits == 3 || _Digits == 5) ? 10.0 : 1.0;
      double proximityBuffer = ApproachPips * _Point * pipMultiplier;

      bool isInside = (price >= (bottom - proximityBuffer) && price <= (top + proximityBuffer));
      return isInside;
     }

   //--- State-Based Color Mapping Method
   color             GetColor(void)
     {
      if(is_broken)
        {
         return clrWhite; // Ghost states render as a hollow white border
        }
      if(status == ZONE_INACTIVE)
        {
         return clrGray;  // Deactivated states render in neutral gray
        }
      if(type == ZONE_SUPPORT)
        {
         return (status == ZONE_VIRGIN ? clrLimeGreen : clrGreen);
        }

      return (status == ZONE_VIRGIN ? clrRed : clrOrangeRed);
     }
  };


Why the CZone Wrapper Matters

1. Properties & Internal State Fields

  • Memory-Cached Coordinates: By storing structural boundaries directly in class variables during creation or synchronization, the engine avoids repetitive ObjectGetDouble() requests during tick execution. This establishes the wrapper as the primary runtime reference layer while reducing terminal API overhead during intensive market loops.
  • Breakout Tracking "consecutive_closes_outside": This internal counter filters temporary close violations and requires candle closes to be confirmed beyond structural boundaries before triggering a breakout state, helping isolate genuine market continuation from short-term volatility noise.
  • Manual Source Tracking "is_user_zone": This flag distinguishes manually drawn trader zones from automatically generated structures, allowing the synchronization layer to prioritize user modifications and prevent unwanted automated regeneration.

2. The Hardened Volatility Constructor

  • Decoupled Volatility Sizing via ATR: Rather than relying on fixed pip distances—which become unreliable as market volatility expands or contracts—the constructor scales zone height dynamically using a 0.5 × ATR calculation. To reduce unnecessary indicator overhead, the constructor receives a cached ATR value directly from the environment layer instead of creating temporary indicator handles internally.
  • Structured-Unique Object Naming: Using the pattern [Prefix]_[Timeframe]_[Price]_[Timestamp] generates a unique signature for every zone instance. This prevents chart object collisions while simplifying synchronization, memory tracking, and manual deletion handling inside the registry system.

3. Spatial Verification & Utility Routines

  • IsPriceNearZone Validation: This function monitors a configurable pip buffer surrounding the zone boundaries, providing an early interaction signal before breakout, rejection, or retest logic is evaluated.
  • State-Driven Styling: Instead of embedding large conditional rendering blocks inside the main loop, the CZone instance evaluates its own internal state flags and returns the appropriate visual color dynamically. This keeps color-state rendering behavior localized and easier to maintain throughout the framework.
  • Diagnostic Logging: Standard Print() telemetry provides real-time feedback on synchronization events, breakout confirmations, and lifecycle state transitions. This creates a transparent debugging layer for validating that memory structures remain synchronized with chart visualizations during execution.


Production Note: Volatility Caching & Memory Management

While the constructor above utilizes current_atr for dynamic sizing, notice that the ATR value is passed as a parameter rather than calculated via iATR handles inside the class. This prevents the "handle leak" common in novice MQL5 designs, where every new object creates a redundant indicator handle.

Implementation Requirement: To ensure efficient memory cleanup during high-frequency backtesting or rapid zone invalidation, the collection managing these objects must be initialized. This ensures that when a zone is removed from the registry, the underlying CZone instance is immediately destroyed, preventing progressive memory accumulation during long-duration trading sessions.


Real-Time User Interaction & Capture (SyncHybridObjects)

The Objective: Traders frequently modify chart objects manually. SyncHybridObjects() is called on every tick to synchronize these manual actions with the framework’s internal memory structures.

The Workflow: Instead of requiring manual menus, this method continuously scans native rectangles on the chart. If it detects a new user-drawn rectangle, it converts it into a CZone instance, tags it as "USER," categorizes it as support or resistance relative to the current live price, and registers it in the active memory array.

If the user resizes an automated zone, the function captures the new coordinates and upgrades the zone from AUTO to a USER-managed state. It also performs a cleanup pass: if an object is deleted, the loop detects the absence, updates the memory arrays, and adds the identifier to an internal blacklist to prevent the system from immediately regenerating it.

//+------------------------------------------------------------------+
//| Intercepts and Synchronizes Manual and Automated Chart Shapes    |
//+------------------------------------------------------------------+
void SyncHybridObjects(void)
  {
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   int total_objects = ObjectsTotal(0, 0, OBJ_RECTANGLE);

   double atr_val[];
   ArraySetAsSeries(atr_val, true);

   double passed_atr = 0.0;

   if(atr_handle != INVALID_HANDLE)
     {
      int copied = CopyBuffer(atr_handle, 0, 0, 1, atr_val);

      if(copied > 0)
        {
         passed_atr = atr_val[0];
        }
     }

   for(int i = 0; i < total_objects; i++)
     {
      string obj_name = ObjectName(0, i, 0, OBJ_RECTANGLE);
      string object_description = ObjectGetString(0, obj_name, OBJPROP_TEXT);

      bool is_already_tagged = (object_description == "AUTO" || object_description == "USER");

      double boundary_p1 = ObjectGetDouble(0, obj_name, OBJPROP_PRICE, 0);
      double boundary_p2 = ObjectGetDouble(0, obj_name, OBJPROP_PRICE, 1);

      CZone* existing_zone = NULL;

      for(int z = 0; z < active_zones.Total(); z++)
        {
         CZone* zone_ptr = (CZone*)active_zones.At(z);

         if(zone_ptr != NULL && zone_ptr.name == obj_name)
           {
            existing_zone = zone_ptr;
            break;
           }
        }

      if(existing_zone == NULL)
        {
         if(is_already_tagged)
            continue;

         ENUM_ZONE_TYPE derived_type = ((boundary_p1 + boundary_p2) / 2.0 > bid) ? ZONE_RESISTANCE : ZONE_SUPPORT;

         //--- Safe initialization passing global cached context value
         CZone* new_zone = new CZone((boundary_p1 + boundary_p2) / 2.0, derived_type, TimeCurrent(), passed_atr);

         new_zone.name = obj_name;
         new_zone.is_user_zone = true;
         new_zone.top = MathMax(boundary_p1, boundary_p2);
         new_zone.bottom = MathMin(boundary_p1, boundary_p2);

         //--- Recalculate structural internals after manual resize injection
         new_zone.height = new_zone.top - new_zone.bottom;
         new_zone.price_level = (new_zone.top + new_zone.bottom) / 2.0;

         new_zone.was_drawn = true;

         ObjectSetString(0, obj_name, OBJPROP_TEXT, "USER");
         active_zones.Add(new_zone);

         PrintFormat(">>>  Registered user zone: %s | Bounds: %.5f - %.5f",
                     new_zone.name, new_zone.bottom, new_zone.top);
        }
      else
        {
         existing_zone.was_drawn = true;

         double previous_top    = existing_zone.top;
         double previous_bottom = existing_zone.bottom;

         existing_zone.top    = MathMax(boundary_p1, boundary_p2);
         existing_zone.bottom = MathMin(boundary_p1, boundary_p2);

         //--- Maintain synchronized internal geometry after manual manipulation
         existing_zone.height = existing_zone.top - existing_zone.bottom;
         existing_zone.price_level = (existing_zone.top + existing_zone.bottom) / 2.0;

         if(MathAbs(previous_top - existing_zone.top) > _Point || MathAbs(previous_bottom - existing_zone.bottom) > _Point)
           {
            if(!existing_zone.is_user_zone)
              {
               existing_zone.is_user_zone = true;
               ObjectSetString(0, existing_zone.name, OBJPROP_TEXT, "USER");
              }

            PrintFormat(">>> ZONE MANIPULATION Resized object: %s | New Bounds: %.5f - %.5f",
                        existing_zone.name, existing_zone.bottom, existing_zone.top);
           }
        }
     }

   for(int i = active_zones.Total() - 1; i >= 0; i--)
     {
      CZone* z = (CZone*)active_zones.At(i);
      if(z == NULL)
         continue;

      if(ObjectFind(0, z.name) < 0 && z.was_drawn)
        {
         PrintFormat(">>>  Deletion Detected user extraction of: %s", z.name);

         if(!z.is_user_zone)
           {
            blacklist.Add(z.name);
           }

         active_zones.Delete(i);
        }
     }
  }


Architectural Performance Considerations: GUI Synchronization & Tick Processing

The SyncHybridObjects() routine intentionally prioritizes transparency and lifecycle visibility over maximum execution efficiency. Because the synchronization layer relies on native MetaTrader graphical API calls such as ObjectName(), ObjectGetString(), and ObjectGetDouble(), the terminal must repeatedly communicate with the chart object subsystem during every synchronization pass.

Under normal execution conditions, this overhead remains negligible. However, on highly volatile instruments—particularly during high-frequency tick bursts on symbols such as gold or US30—the cost of continuously scanning and synchronizing graphical objects inside OnTick() becomes more noticeable. The main cost is repeated interaction between the market-data stream and the chart-rendering subsystem, especially while objects are updated, resized, or queried.

Within this educational prototype, the synchronization routine remains inside OnTick() intentionally. This design keeps the rectangle adoption lifecycle, manual manipulation detection, and wrapper synchronization flow easy to observe, debug, and study in real time.

In production systems, synchronization is usually decoupled from the tick-processing pipeline. A more scalable approach involves migrating user interaction handling toward OnChartEvent() and responding specifically to events such as CHARTEVENT_OBJECT_CREATE, CHARTEVENT_OBJECT_DRAG and CHARTEVENT_OBJECT_DELETE. This event-driven structure minimizes unnecessary graphical polling while keeping the primary tick-processing pipeline lightweight, responsive, and easier to scale under heavy market activity.


Proximity Clustering and Consolidation "MergeZones"

The Objective: Individually detected pivots can often lead to a cluttered chart, where multiple overlapping zones overwhelm the trader and degrade terminal performance. MergeZones() prevents this by using iterative proximity merging to consolidate redundant price levels into a single, unified structure.

The Workflow: The function runs on the opening of each new bar and evaluates the proximity of zones within the same category (e.g., Support vs. Support).

  • Volatility-Scaled Comparison: We measure the center-line distance between neighboring zones. If their center-line distance falls within a user-defined threshold—typically scaled by 1.5 ATR—they are flagged for consolidation.

  • Coordinate Expansion: The surviving zone automatically expands its upper and lower boundaries to encompass the full price range of both original structures.

  • Identity Preservation: To ensure manual work is never lost, the inheritance logic checks the source of the zones. If either of the original areas was marked as a "USER" zone, the surviving zone inherits the protected "USER" status, preserving the zone's user-managed status during future synchronization passes.

    //+------------------------------------------------------------------+
    //| Collapses Adjacent Overlapping Ranges Using Volatility Buffers   |
    //+------------------------------------------------------------------+
    void MergeZones(void)
      {
       double atr_val[];
       ArraySetAsSeries(atr_val, true);
    
       if(CopyBuffer(atr_handle, 0, 0, 1, atr_val) < 1)
          return;
    
       double merge_threshold = atr_val[0] * MergeSensitivity;
    
       for(int i = active_zones.Total() - 1; i >= 1; i--)
         {
          CZone* z1 = (CZone*)active_zones.At(i);
          if(z1 == NULL)
             continue;
    
          for(int j = i - 1; j >= 0; j--)
            {
             CZone* z2 = (CZone*)active_zones.At(j);
             if(z2 == NULL)
                continue;
    
             if(z1.type != z2.type)
                continue;
    
             double space_distance = MathAbs(z1.price_level - z2.price_level);
             if(space_distance > merge_threshold)
                continue;
    
             z1.top         = MathMax(z1.top,    z2.top);
             z1.bottom      = MathMin(z1.bottom, z2.bottom);
             z1.height      = z1.top - z1.bottom;
             z1.price_level = (z1.top + z1.bottom) / 2.0;
    
             if(z2.is_user_zone)
                z1.is_user_zone = true;
    
             PrintFormat(">>> Combined overlapping structures into: %s", z1.name);
    
             ObjectDelete(0, z2.name);
             active_zones.Delete(j);
             break;
            }
         }
      }
    


Dynamic Zone Projection (UpdateZoneVisual)

The objective:Standard chart objects are statically tied to the bars where they were originally drawn. As new data arrives, these objects eventually move off-screen. UpdateZoneVisual ensures that key levels remain relevant by projecting them into the "future" chart space. 

The Workflow: Instead of leaving the right edge of a rectangle fixed to a historical bar, this function updates the time coordinate (OBJPROP_TIME, index 1) during every tick.

By calculating the projection as (TimeCurrent() + PeriodSeconds(_Period) * 20), the right boundary of the zone is projected forward. This ensures that support and resistance levels are always visible in the "Chart Shift" gap, providing immediate context alongside live price action.

Quick Note on Object Interaction: To ensure these projected zones remain fully interactive, we will explicitly enable OBJPROP_SELECTABLE. This allows the SyncHybridObjects loop to "capture" manual user adjustments during subsequent synchronization passes, even while the zone is set to the background (OBJPROP_BACK) to avoid obstructing price action.

//+------------------------------------------------------------------+
//| Generates Graphic Objects and Projects Visual Boundaries Forward |
//+------------------------------------------------------------------+
void UpdateZoneVisuals(void)
  {
   for(int i = 0; i < active_zones.Total(); i++)
     {
      CZone *z = (CZone*)active_zones.At(i);

      if(z == NULL)
         continue;

      if(ObjectFind(0, z.name) < 0)
        {
         if(!ObjectCreate(0, z.name, OBJ_RECTANGLE, 0, z.created, z.top, TimeCurrent(), z.bottom))
            continue;

         ObjectSetInteger(0, z.name, OBJPROP_BACK, true);
         ObjectSetInteger(0, z.name, OBJPROP_FILL, true);
         ObjectSetInteger(0, z.name, OBJPROP_SELECTABLE, true);

         ObjectSetString(0, z.name, OBJPROP_TEXT, z.is_user_zone ? "USER" : "AUTO");
        }

      datetime right_edge_projection = TimeCurrent() + PeriodSeconds(_Period) * 20;

      ObjectSetInteger(0, z.name, OBJPROP_TIME, 1, right_edge_projection);
      ObjectSetDouble(0, z.name, OBJPROP_PRICE, 0, z.top);
      ObjectSetDouble(0, z.name, OBJPROP_PRICE, 1, z.bottom);
      ObjectSetInteger(0, z.name, OBJPROP_COLOR, z.GetColor());
     }

   ChartRedraw();
  }


Volatility-Scaled Structural Pivot Logic (DetectDynamicZones)

The Objective: Automated generation can lead to chart clutter if every minor price fluctuation is marked. DetectDynamicZones filters this noise using a structural pivot filter to identify structurally valid reversal areas.

The Workflow: The function scans historical data to verify that a high or low is validated as a structural pivot. For example, a valid resistance peak must be confirmed by surrounding candles for a specific number of bars (SwingBars) on both sides. To maintain a clean layout, the system calculates a dynamic merge threshold:

Merge Threshold = iATR-based volatility * MergeSensitivity

If a new pivot is detected but its price falls within this threshold of an existing zone, zone creation is skipped for that pivot. This combines the trader’s sensitivity preference with current market volatility to ensure only structurally distinct levels are rendered.

//+------------------------------------------------------------------+
//| Scans Historical Market Data to Identify Valid Symmetrical Pivots|
//+------------------------------------------------------------------+
void DetectDynamicZones(void)
  {
   MqlRates rates[];
   ArraySetAsSeries(rates, true);

   int bars_to_copy = LookbackBars + (SwingBars * 2);

   if(CopyRates(_Symbol, _Period, 0, bars_to_copy, rates) < bars_to_copy)
      return;

   double atr_val[];
   ArraySetAsSeries(atr_val, true);

   if(CopyBuffer(atr_handle, 0, 0, 1, atr_val) < 1)
      return;

   for(int i = SwingBars; i < LookbackBars; i++)
     {
      bool is_high_confirmed = true;
      bool is_low_confirmed  = true;

      for(int j = 1; j <= SwingBars; j++)
        {
         if(rates[i].high <= rates[i-j].high || rates[i].high <= rates[i+j].high)
           {
            is_high_confirmed = false;
           }

         if(rates[i].low >= rates[i-j].low || rates[i].low >= rates[i+j].low)
           {
            is_low_confirmed = false;
           }

         if(!is_high_confirmed && !is_low_confirmed)
            break;
        }

      if(is_high_confirmed)
         TryCreateAutoZone(rates[i].high, ZONE_RESISTANCE, rates[i].time, atr_val[0]);

      if(is_low_confirmed)
         TryCreateAutoZone(rates[i].low, ZONE_SUPPORT, rates[i].time, atr_val[0]);
     }
  }

//+------------------------------------------------------------------+
//| Filters Structural Creation via Blacklists and Proximity Checks  |
//+------------------------------------------------------------------+
void TryCreateAutoZone(double p, ENUM_ZONE_TYPE t, datetime dt, double atr)
  {
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

//--- Layer 1: Market Position Filter
//--- Prevents "laddering" by ignoring overhead support or underlying resistance
   if(t == ZONE_SUPPORT && p > bid)
      return;

   if(t == ZONE_RESISTANCE && p < ask)
      return;

//--- Layer 2: Duplicate Check
   for(int i = 0; i < active_zones.Total(); i++)
     {
      CZone *z = (CZone*)active_zones.At(i);
      if(z == NULL)
         continue;

      if(z.created == dt && z.type == t)
         return;
     }

//--- Maintain identical naming convention with constructor-generated identifiers
   string id = (t == ZONE_SUPPORT ? "S_" : "R_") + EnumToString(_Period) + "_" +
               DoubleToString(p, _Digits) + "_" + IntegerToString((long)dt);

//--- Layer 3: User Blacklist Filter
   for(int i = 0; i < blacklist.Total(); i++)
     {
      if(blacklist.At(i) == id)
         return;
     }

//--- Layer 4: Proximity/Merge Sensitivity Filter
   double merge_threshold = atr * MergeSensitivity;

   for(int i = 0; i < active_zones.Total(); i++)
     {
      CZone *z = (CZone*)active_zones.At(i);
      if(z == NULL)
         continue;

      if(MathAbs(z.price_level - p) < merge_threshold)
         return;
     }

//--- Create the new zone instance and pass the contextual ATR parameter
   CZone *nz = new CZone(p, t, dt, atr);

   nz.name = id;
   nz.is_user_zone = false;

   active_zones.Add(nz);

   PrintFormat(">>> Auto-generated zone registered: %s", nz.name);
  }


The Startup Purge Engine: Core Functionality

NukeIrrelevantZones() runs as a cleanup routine during initialization. Its sole purpose is to sweep the chart and clear out zones that have been invalidated by the current price position. This ensures you start with a clean, uncluttered workspace.

The routine evaluates every active zone against current market prices using simple price-based filtering rules:

  • Invalidated Support: If the current market bid price breaches the support zone boundary, that level has been broken and is flagged as dead.
  • Invalidated Resistance: If the current market ask price breaches the resistance zone boundary, that level has been breached and is flagged as dead.

Why This Routine Matters:

  • Instant Workspace Cleanup: It automatically calls ObjectDelete() to wipe the dead shapes off your screen and removes them from the internal tracking list, saving you from having to manually find and delete old rectangles.
  • Safe Array Processing:The loop processes the list backward. This is the standard way to safely remove items from a list without skipping elements or disrupting the counting order as items disappear.
  • Optimized Visual Refresh: Instead of triggering repeated redraw updates during deletion operations, it performs all the deletions first and then triggers a single ChartRedraw() at the very end, reducing unnecessary intermediate redraws during cleanup.
//+------------------------------------------------------------------+
//| Clears Irrelevant Untradeable Structural Layers on Startup       |
//+------------------------------------------------------------------+
void NukeIrrelevantZones(void)
  {
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   int purged = 0;

//--- Loop through the memory list backwards to safely delete
   for(int i = active_zones.Total() - 1; i >= 0; i--)
     {
      CZone *z = (CZone*)active_zones.At(i);
      if(z == NULL)
         continue;

      bool is_dead = false;

      //--- Remove only zones that are completely invalidated
      //--- relative to the current market position.

      // Support becomes irrelevant only if price is fully below the zone
      if(z.type == ZONE_SUPPORT && bid < z.bottom)
         is_dead = true;

      // Resistance becomes irrelevant only if price is fully above the zone
      if(z.type == ZONE_RESISTANCE && ask > z.top)
         is_dead = true;

      if(is_dead)
        {
         ObjectDelete(0, z.name);  // Remove visual rectangle from chart
         active_zones.Delete(i);   // Remove from memory array ownership
         purged++;
        }
     }

   if(purged > 0)
      PrintFormat(">>> [INITIAL PURGE] DELETED %d irrelevant zones from memory and chart.", purged);

   ChartRedraw();
  }


Zone State Management & Breakout Logic (QualifyBreakouts)

The Objective: Static zones often require manual deletion once breached, which ignores the concept of "market memory." QualifyBreakouts implements a flag-based lifecycle system that automates the zone lifecycle—allowing levels to transition from active barriers to "ghost" levels and, eventually, to extinction or resurrection.

The Workflow: Instead of relying on static zone states, the logic calculates a dynamic breakout buffer to account for market noise and volatility:

Buffer Level = MathMax(Zone Height * BreakBufferPct, ATR value)

We then manage the lifecycle through these three distinct layers.

1. Breakout Confirmation

When a candle close moves beyond the buffered zone boundary, a consecutive close counter increments. If the price remains outside for a specific number of consecutive closes (BreakConfirmCloses), the zone is marked as broken. This prevents premature state changes during minor spikes.

2. The Ghost State

Once a breakout is confirmed, the zone is not deleted. Instead, it converts into a "ghost" state—the fill is disabled, and the border changes to a neutral color (e.g., clrWhite). This visual shift keeps the footprint of the level on the chart, allowing the trader to monitor it for future retests or S/R flips.

3. Resurrection vs. Extinction

The framework tracks the age of these ghost zones in bars.

  • Resurrection: If the price returns and closes back within the original boundaries before the GhostBarLimit is reached, the zone restores its full-fill state and resets breakout tracking counters flag.
  • Extinction: If the zone remains untouched beyond the bar limit, the engine removes the object from the chart and deletes it from the active zone list.

Production Note: This implementation prioritizes simplicity and deterministic behavior over advanced state modeling techniques. More sophisticated breakout systems may use multi-timeframe confirmation, order-flow validation, or probabilistic state transitions. However, this design intentionally keeps the logic lightweight and rule-based to ensure stability, transparency, and predictable execution.

//+------------------------------------------------------------------+
//| Evaluates Penetration Bounds, Handles Ghosts, and Resurrections  |
//+------------------------------------------------------------------+
void QualifyBreakouts(void)
  {
   double last_close = iClose(_Symbol, _Period, 1);

   double atr_val[];
   ArraySetAsSeries(atr_val, true);

   int copied = CopyBuffer(atr_handle, 0, 0, 1, atr_val);

   if(copied < 1)
     {
      Print(">>> [BREAKOUT ENGINE ERROR] ATR buffer copy failed.");
      return;
     }

   double atr = atr_val[0];

   for(int i = active_zones.Total() - 1; i >= 0; i--)
     {
      CZone *z = (CZone*)active_zones.At(i);

      if(z == NULL || z.type == ZONE_NEUTRAL)
         continue;

      // --- LAYER 1: GHOST LIFECYCLE MANAGEMENT
      if(z.is_broken)
        {
         int bars_since_break = iBarShift(_Symbol, _Period, z.broken_time);

         if(bars_since_break > GhostBarLimit)
           {
            PrintFormat(">>> [ZONE EXTINCTION] Aged ghost zone removed from memory: %s", z.name);

            ObjectDelete(0, z.name);
            active_zones.Delete(i);

            continue;
           }

         if(AllowResurrection && last_close > z.bottom && last_close < z.top)
           {
            z.is_broken = false;
            z.status = ZONE_VIRGIN;
            z.consecutive_closes_outside = 0;
            z.buy_triggered = false;
            z.sell_triggered = false;

            ObjectSetInteger(0, z.name, OBJPROP_COLOR, z.GetColor());
            ObjectSetInteger(0, z.name, OBJPROP_FILL, true);

            PrintFormat(">>> [ZONE RESURRECTION] Price reclaimed level. Restored: %s at Bar: %d",
                        z.name,
                        bars_since_break);
           }

         continue;
        }

      // --- LAYER 2: FRESH BREAKOUT QUALIFICATION
      double buffer = MathMax(z.height * BreakBufferPct, atr);

      bool is_clear_breakout =
         (z.type == ZONE_SUPPORT && last_close < (z.bottom - buffer)) ||
         (z.type == ZONE_RESISTANCE && last_close > (z.top + buffer));

      if(is_clear_breakout)
        {
         z.consecutive_closes_outside++;

         if(z.consecutive_closes_outside >= BreakConfirmCloses)
           {
            z.is_broken = true;
            z.status = ZONE_BROKEN;
            z.broken_time = TimeCurrent();

            ObjectSetInteger(0, z.name, OBJPROP_COLOR, clrWhite);
            ObjectSetInteger(0, z.name, OBJPROP_FILL, false);

            PrintFormat(">>> [ZONE GHOSTED] Level breached. Ghost mode activated for: %s", z.name);
           }
        }
      else
        {
         z.consecutive_closes_outside = 0;
        }
     }
  }

Boot Calibration (OnInit()): When the framework is loaded onto a chart and during lifecycle reinitializations such as time frame changes, pre-existing visual elements can lead to duplicates in tracking structures or visual overlap. The OnInit() function acts as a clean state reset. It clears the rectangle-based zone objects from the chart, clears the active zone and blacklist containers, and hooks into the market volatility data stream by instantiating our central ATR tracking handle.

//+------------------------------------------------------------------+
//| Expert Initialization Function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Step 1: Purge visual space to prevent coordinate overlays
   ObjectsDeleteAll(0, -1, OBJ_RECTANGLE);

//--- Secure container pointer ownership rules to prevent leaks
   active_zones.FreeMode(true);
   active_zones.Clear();

//--- Ensure automatic cleanup ownership for blacklist storage
   blacklist.Clear();

//--- Step 2: Establish volatility handle for dynamic auto-sizing
   atr_handle = iATR(_Symbol, _Period, 14);
   if(atr_handle == INVALID_HANDLE)
     {
      Print("[BOOT ERROR] Failed to instantiate core ATR indicator handle.");
      return(INIT_FAILED);
     }

//--- Step 3: Initial background data scan
   DetectDynamicZones();
   NukeIrrelevantZones();

   Print(">>> successfully initialized.");
   return(INIT_SUCCEEDED);
  }

System deinitialization (OnDeinit): Proper memory allocation management requires an application to clean up after itself when it is detached from the terminal chart window. If a program fails to clean up, it can leave behind orphan objects that persist on the chart and clutter the workspace. For this we need to properly dispose of every object and any handles held in memory.  OnDeinit() ensures that when our script is closed, all rectangle-based zone objects are removed from the chart and indicator handles are properly released back to the terminal.

//+------------------------------------------------------------------+
//| Expert Deinitialization Function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, -1, OBJ_RECTANGLE);

   active_zones.Clear();
   blacklist.Clear();

   if(atr_handle != INVALID_HANDLE)
     {
      IndicatorRelease(atr_handle);
      atr_handle = INVALID_HANDLE;
     }

   PrintFormat(">>> Deinitialization Engine detached smoothly. Reason Code: %d", reason);
   ChartRedraw();
  }

Central Processing Router (OnTick): The OnTick() block serves as the core engine of our framework, acting as the primary traffic controller that handles the transition between real-time data and historical chart bars. To maintain peak performance, the router divides operations into two distinct frequencies. High-frequency tasks, such as tracking manually drawn or modified shapes via SyncHybridObjects(), run on every incoming price change to ensure immediate UI responsiveness. Meanwhile, resource-heavy operations like breakout validation, dynamic zone calculation, or merging overlapping ranges are deferred to execute only at the opening of a new bar.

//+------------------------------------------------------------------+
//| Core Tick Processing & Routing Engine                            |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   SyncHybridObjects();

   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double current_price = (bid + ask) / 2.0;

   datetime current_candle_time = iTime(_Symbol, _Period, 0);

   if(current_candle_time != last_bar_time)
     {
      last_bar_time = current_candle_time;

      QualifyBreakouts();
      DetectDynamicZones();
      MergeZones();

      PrintFormat(">>> New Candle Opened: %s | Managing Lifecycles.", TimeToString(current_candle_time));
     }

   UpdateZoneVisuals();
  }


Operational Walkthrough

During initialization, the EA scans historical bars, detects structural pivots, and automatically renders support and resistance zones on the chart. Select the native MetaTrader 5 Rectangle shape and draw it anywhere on the chart (top or bottom of the current price). On the next market tick, the synchronization layer intercepts the object and automatically registers it into the framework. Click and drag the edge of an existing zone to expand or contract it manually.

The synchronization routines immediately detect the boundary modification and update the internal zone data accordingly. Right-click a rectangle and select Delete, or select the object and press the Delete key. The framework detects the removal and clears the associated internal references automatically. Remove the Expert Advisor from the chart to trigger the deinitialization routines, which release all tracked objects and allocated resources cleanly.

fig. 1. Rendered zones on chart

Fig. 1. Renderd zone on chart

fig. 2. inserting rectangles manually

   Fig. 2. inserting rectangles manually

fig. 3. detailed action messages

Fig. 3. detailed action message


Future Scope & Production Scaling

This article presents a reusable scaffold that makes native MetaTrader 5 rectangles stateful via automated lifecycle management. However, moving this structural layout from an educational prototype into a production-grade trading system reveals a vast landscape for optimization.

In future parts of this series, we will explore more advanced architectural extensions, including the following:

  • Event-Driven UI processing: moving entirely away from procedural tick-polling and utilizing native chart events (CHARTEVENT_OBJECT_CREATE, CHARTEVENT_OBJECT_DRAG, and CHARTEVENT_OBJECT_DELETE) for instantaneous, lag-free visual adjustments. Handling user interactions inside OnTick() serves as an excellent educational tracking model, but it is architecturally non-standard; it binds visual responsiveness directly to incoming tick frequency and server ping. Shifting to an event-driven paradigm ensures the UI reacts instantly, even during low-liquidity periods or fast market spikes.

  • Theoretical Trading Frameworks (The "S/R Flip"): Exploring purely conceptual trading models that leverage the wrapper's lifecycle transitions. We will examine how zones transitioning from ZONE_VIRGIN into retained ghost states can theoretically support breakout-retest structures, dynamic stop placement logic, and adaptive take-profit projection without overcomplicating the foundational synchronization engine.

  • Memory Layer Sovereignty: Further evolving the architecture so the internal CArrayObj registry operates as the primary runtime state layer, while graphical chart objects function strictly as passive visualization components. This separation improves structural consistency and allows the framework to recover gracefully from accidental user modifications or graphical deletions.

  • Semantic Metadata Aggregation: Evolving our visual geometric compression into a mathematical matrix that preserves and compounds historical zone attributes when two structures overlap. Instead of basic midpoint classification and string-based blacklisting, a hardened production model will utilize multivariable structural fingerprints—incorporating historical touch frequencies, volume accumulation weights, and rejection scoring formulas into a persistent data key.

  • Multi-Timeframe Confluence Filters: Extending the spatial discovery engine to scan higher-level timeframes (H1 through D1) and dynamically project those structural zones onto lower execution charts, allowing local setups to inherit broader market context and confluence alignment.


Conclusion & Reusable Scaffold

The Czone Architecture integrates three layers of protection against synchronization lag and visual decay. The class-based registry prevents the framework from making redundant terminal calls during high-frequency volatility. The SyncHybridObjects layer bridges manual trader intuition with automated execution, while the volatility-scaled constructor ensures structural zones remain proportional regardless of the asset being traded. 

Key Takeaways:

  • Zone Projections are Dynamic, not Static: By calculating the right-hand time coordinate as TimeCurrent() + (PeriodSeconds() * 20), the framework converts a historical rectangle into a forward-looking ray cast. This moves the support/resistance context into the "Chart Shift" gap where live trading decisions occur.

  • The 1.5 ATR Rule Regularizes Merging: To prevent chart clutter, zones of the same type are consolidated based on a volatility-scaled proximity check. This transforms a collection of individual fractal pivots into a single, high-probability structural area.

  • "Ghost" States Preserve Market Memory: Deletion is a last resort. When a breakout is confirmed via consecutive closes, the zone transitions to an unfilled "ghost" state. This allows the system to monitor for S/R flips or "resurrection" within the GhostBarLimit before the object is cleared from memory.

  • User priority is enforced programmatically: trader interaction is treated as a high-priority interrupt. Any adjustment to an automated zone converts its status to USER, shielding it from being recalculated or moved by the internal math loops.

  • Diagnostic printing is for validation, not logic: the terminal Print() outputs are a transparency layer. If a zone fails to transition to a Ghost state upon a clear breakout, the logs provide the immediate diagnostic path to verify the BreakConfirmCloses counter logic.

Extending the Framework

This architecture is designed to be a "plug-and-play" foundation. Because every zone is a managed object with a unique state, you can easily extend the engine by:

  • Automated Notifications: Adding a SendNotification() call inside the IsPriceNearZone logic for mobile alerts.
  • EA Integration: Passing the active_zones array to an OnTick() entry function to act as a dynamic filter for trade execution.
  • Signal Mapping: Connecting the is_user_zone flag to a Telegram bot to share manual setups alongside automated technical levels.

This system is built in MQL5 to be consumed by high-performance trading tools. It moves the burden of chart maintenance from the trader to the environment, allowing for a more focused and professional approach to live market execution.

Attached files |
DynamicSR.mq5 (23.85 KB)
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Market Microstructure in MQL5 (Part 4): Volatility That Remembers Market Microstructure in MQL5 (Part 4): Volatility That Remembers
This article adds eight volatility functions to MicroStructure_Foundation.mqh, including realized volatility, duration-adjusted volatility, fractional volatility, a FIGARCH-inspired proxy, a volatility clustering index, a GJR-GARCH asymmetry measure (using the Dube library), bipower-variation jump detection, and a wrapper function. The MFDFA implementation is revised to return the conventional Legendre-transform Δα with an R² confidence field, replacing the τ-spread proxy used in the original submission. Thresholds are derived from 514 NY sessions of NQ E-mini Nasdaq 100 futures (May 2024–May 2026); no new include file is created.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
From Basic to Intermediate: Objects (II) From Basic to Intermediate: Objects (II)
In today's article, we will look at how to control some object properties in a simple way using code. We will also see how a custom application can place more than one object on the same chart. In addition, we will begin to understand the importance of assigning a short name to any indicator we plan to implement.