preview
Interactive Supply and Demand Zone Manager in MQL5 (Part III): Zone Analysis, Stateful Interaction, and Pending Event Management

Interactive Supply and Demand Zone Manager in MQL5 (Part III): Zone Analysis, Stateful Interaction, and Pending Event Management

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

Introduction

In Part I of this series, we introduced a dynamic supply-and-demand zone management framework that transforms static chart annotations into actively managed market structures. Rather than treating support and resistance as passive visual references, the system defines zones as lifecycle-aware entities. Zones support deterministic creation, modification, invalidation, restoration, and state transitions as market conditions evolve.

The primary objective of that initial implementation was to establish the concept of a stateful zone—elevating a simple graphical rectangle into a structured container holding explicit attributes such as classification, historical origin, structural strength, status, and interaction counts. This provided the foundation for representing market structure in a more systematic and machine-readable form.

In Part II, this runtime framework was refactored into a structured event-driven architecture, where system execution was organized around discrete market responses rather than continuous tick-based polling. Alongside this shift, a dedicated logging subsystem was introduced to replace informal debug outputs with a persistent, append-only event record.

Through the MasterLogger framework, each significant lifecycle transition—from zone creation and modification to state changes such as ghosting or reactivation—is captured with contextual metadata describing the system state at the moment of execution. This includes quantitative and structural indicators that allow each event to be reconstructed and analyzed after execution, improving traceability of the zone lifecycle over time.

As the system evolved into a fully operational decision pipeline, an important architectural requirement became clear. The framework now has three tightly connected stages: zone qualification (AnalyzeZoneCandidate()), lifecycle tracking (MonitorZoneLifecycle()), and outcome resolution (ResolvePendingInteractions()). All stages run in an event-driven environment with full logging.

While each component functions correctly in isolation, the overall system depends on strict consistency between these stages. Zone scoring determines which structures are admitted into the system, lifecycle monitoring governs how those structures behave under market interaction, and resolution logic finalizes their outcome states. If any of these stages operate under inconsistent assumptions, the system will still execute correctly, but the resulting behavior may lose structural coherence.


Architectural Focus of This Phase

  1. Formalization of the Zone Decision Pipeline
    The system is structured around three primary stages: candidate evaluation, lifecycle monitoring, and interaction resolution.
  2. Reinforcement of Zone Admission Correctness
    AnalyzeZoneCandidate() acts as the entry validation layer, ensuring only structurally valid zones are admitted into the system based on defined scoring and structural criteria.
  3. Clarification of Lifecycle State Responsibility
    MonitorZoneLifecycle() manages the ongoing state evolution of active zones, including touch detection, pending state transitions, and interaction tracking.
  4. Deterministic Interaction Resolution
    ResolvePendingInteractions() finalizes zone outcomes by resolving pending states into either validated bounces or confirmed breakouts using consistent rule-based logic.
  5. Observational Role of the Logging Layer
    The logging system remains strictly observational, recording execution outcomes without influencing validation rules or decision-making logic within the pipeline.


Framework Extensions

While Parts I and II established the lifecycle management framework and its event-driven execution model, Part III extends the system by introducing a quantitative admission process together with a more sophisticated interaction engine. Rather than admitting every detected pivot into the lifecycle manager, candidate zones are now evaluated using an objective scoring model before becoming active objects within the framework.

To support these additions, the configuration layer has been expanded with a new set of user-adjustable parameters governing three distinct stages of the decision pipeline: quantitative zone qualification, continuous zone evolution, and bounce confirmation. Separating these responsibilities into independent configuration groups allows the framework to remain adaptable across different trading instruments and market conditions while preserving deterministic execution.

1. Quantitative Zone Qualification

The first group of parameters controls the mathematical evaluation performed by AnalyzeZoneCandidate(). Every automatically detected zone is assigned a normalized score based on structural symmetry, volume participation, and price displacement before being considered for admission into the system.

The scoring model is fully configurable through the following parameters:

  • MinZoneScore — Minimum score required for a detected zone to enter the lifecycle engine.
  • EnableVolumeValidation — Enables or disables the volume component during evaluation.
  • VolumeWeight — Maximum contribution allocated to institutional volume participation.
  • VelocityWeight — Maximum contribution allocated to ATR-normalized departure strength.
  • StructureWeight — Maximum contribution allocated to geometric swing symmetry.
  • VelocityLookaheadBars — Defines the observation window used to measure post-pivot price displacement.

These parameters collectively determine the quality threshold required for automatically detected zones to become managed objects.

input group "--- Part 3 Quantitative Scoring Matrix ---"
input int      MinZoneScore        = 50;      // Minimum mathematical score (0-100) required to track zone
input bool     EnableVolumeValidation = true// Enable/disable tick-volume relationship checks
input int      VolumeWeight        = 30;      // Max points allocated to institutional volume (Default: 30)
input int      VelocityWeight      = 40;      // Max points allocated to escape velocity (Default: 40)
input int      StructureWeight     = 30;      // Max points allocated to geometric symmetry (Default: 30)
input int      VelocityLookaheadBars = 3;     // Bar count lookahead window for departure calculation

 2. Live Zone Evolution

Once admitted, zones no longer remain static structures. Instead, their strength evolves as new market interactions occur throughout their lifetime.

The second configuration group governs this dynamic behavior by allowing scores to change in response to age, successful reactions, and structural deterioration.

Key parameters include:

  • EnableTemporalDecay — Enables gradual score reduction as zones become older.
  • DecayRatePerBar — Controls the rate at which age reduces structural confidence.
  • TouchScoreBonus — Rewards zones that produce clean market rejections.
  • PenetrationPenalty — Reduces confidence after significant price penetration into the zone.

Rather than treating zone quality as immutable, this model continuously adjusts confidence to reflect changing market conditions.

input group "--- Part 3 Live Zone Evolution Tracking ---"
input bool     EnableTemporalDecay = true;   //Enable score degradation over time
input double   DecayRatePerBar     = 0.05;   //Score reduction unit per bar age
input double   TouchScoreBonus     = 5.0;    //Score increase awarded for clean rejections
input double   PenetrationPenalty  = 5.0;    //Score reduction unit for deep structural test drillings

3. Bounce Confirmation

Market interaction does not immediately determine whether a zone has successfully held or failed. Instead, candidate reactions first enter a pending state where additional price action is evaluated before a final decision is made.

The final configuration group defines the conditions required to validate a bounce.

  • BounceConfirmCloses — Number of consecutive closing bars required to confirm a successful rejection.
  • BounceATRDistance — Minimum ATR-normalized distance price must move away from the zone before confirmation.
  • BounceTimeoutBars — Maximum duration a pending interaction may remain unresolved before timing out.

Together, these parameters provide a deterministic framework for distinguishing genuine reactions from temporary price fluctuations.

Note: Although BounceTimeoutBars remains available as a configurable parameter, the current implementation resolves pending interactions through the live state-management framework introduced in this article. Consequently, timeout-based resolution is not the primary confirmation mechanism. The parameter is preserved for future enhancements. Time-based interaction validation still requires additional tuning and evaluation before production use.
input group "--- Symmetrical Bounce Settings ---"
input int      BounceConfirmCloses = 7;    // Bars Away Required for Confirmation
input double   BounceATRDistance   = 0.5;  // ATR Multiplier Filter for Movement Away
input int      BounceTimeoutBars   = 5;    // Max Bars Allowed to Remain Pending

Separation of Automatic and Manual Zones

An important architectural refinement introduced in Part III is the separation between automatically detected zones and manually drawn user zones. Automatic zones originate from the market analysis engine and therefore require objective validation before entering the lifecycle framework. Every detected candidate is passed through AnalyzeZoneCandidate(), where its structural quality is quantified and compared against the configured admission threshold.

Manually drawn zones follow a different execution path. Because they represent explicit analyst judgement rather than algorithmic discovery, they bypass the quantitative scoring stage entirely and are registered directly within the shared zone state. This design preserves user intent while ensuring that only machine-generated structures are subjected to mathematical validation. As a result, the scoring framework functions exclusively as an admission filter for automatically detected zones and never overrides discretionary user-defined market structure.

Updated Decision Pipeline

The introduction of quantitative validation modifies the overall execution flow of the framework. Instead of immediately creating every detected pivot, the system now evaluates candidate zones before determining whether they should participate in lifecycle management as the flow chart below shows.

Fig. 1. Architecture flow for Auto Zones

Zone Admission Model: AnalyzeZoneCandidate()

The AnalyzeZoneCandidate() function serves as the primary quantitative validation gate within the zone creation pipeline. Rather than promoting every detected pivot into a tradable support or resistance zone, the function evaluates the structural quality of each candidate before it is admitted into the lifecycle engine.

Each detected pivot is independently assessed using three complementary characteristics:

  • Relative volume participation
  • ATR-normalized price displacement
  • Local structural symmetry

    Each component measures a different aspect of market behavior. Structural symmetry confirms that a valid turning point exists, relative volume evaluates the level of market participation during pivot formation, while velocity measures how decisively price rejects the level after the pivot has formed. By combining these independent measurements, the engine avoids relying on any single market characteristic when determining zone quality.

    The resulting score is normalized to a bounded range of 0–100, providing a consistent measure of structural quality regardless of the trading instrument or market conditions.

    1. Relative Volume Participation

    The first scoring component evaluates whether the pivot formed during an unusually active period of market participation.

    Instead of relying on absolute tick volume, which varies significantly between instruments and trading sessions, the algorithm compares the pivot candle's tick volume against the average tick volume of the preceding fourteen candles:

    Volume Ratio = V_pivot / Average Volume_14

    This produces a normalized participation ratio that measures how exceptional the pivot activity was relative to its immediate historical environment. The ratio is converted into a weighted score using bounded linear scaling:

    • Volume Ratio >= 2.0 -> Full volume allocation
    • Volume Ratio <= 0.5 -> Zero contribution
    • Otherwise -> Linear interpolation

      Volume Score = f(Volume Ratio) * W_vol

      Where:

      • W_vol = VolumeWeight

      Using a relative ratio instead of raw volume allows the same scoring model to adapt naturally across different symbols and changing market conditions. Only significant participation spikes contribute meaningfully toward zone admission.

      2. ATR-Normalized Velocity

      Once a pivot has been identified, the algorithm evaluates how convincingly price departs from that location over a configurable look-ahead window (VelocityLookaheadBars). The displacement is normalized using the current Average True Range (ATR):

      Displacement Ratio = Price Displacement / ATR

      Normalizing by ATR allows the algorithm to evaluate movement relative to prevailing market volatility rather than using fixed pip distances. The resulting ratio is converted into a weighted score:

      • Displacement >= 2.5 ATR -> Full velocity allocation
      • Displacement <= 0.5 ATR -> Zero contribution
      • Otherwise -> Linear interpolation

        Velocity Score = f(Displacement Ratio) * W_vel

        Where:

        • W_vel = VelocityWeight

        This component rewards pivots that generate decisive post-formation movement while filtering out weak turning points that fail to produce meaningful market rejection.

        3. Structural Symmetry

        The final component evaluates the geometric quality of the detected pivot. Using the configured SwingBars parameter, the framework confirms that the candidate remains the dominant local extremum within its surrounding price structure.

        • resistance candidate must have a higher high than all surrounding highs within the swing window.
        • A support candidate must have a lower low than all surrounding lows.

        If the structural validation succeeds:

        Structure Score = W_str

        Otherwise:

        Structure Score = W_str / 2

        Where:

        • W_str = StructureWeight

        This ensures that well-defined structural turning points receive full credit while less distinct formations receive only partial weighting.

        4. Final Score Construction

        The three independently calculated scores are combined to produce the overall structural quality metric:

        Total Score = Volume Score + Velocity Score + Structure Score

        The final value is clamped to the range:

        • 0 ≤ Score ≤ 100

        This normalized score provides a consistent quantitative measure of the candidate zone's initial structural quality.

        5. Tier Classification

        Each candidate is classified according to its final score:

        SCORE RANGE ASSIGNED TIER
        Score >= 95
        ELITE
        Score >= 80 HIGH
        Score >= 50 MODERATE
        Score < 50 LOW

        The assigned tier becomes part of the zone's persistent metadata and is later used during lifecycle management, merging, and prioritization.

        6. Zone Admission Gate

        A candidate is admitted into the tracking engine only if:

        Score >= MinZoneScore

        (Default: 50)

        This admission threshold prevents weak structural candidates from entering the lifecycle engine, ensuring that only sufficiently validated zones proceed to interaction monitoring and subsequent state transitions.

        Output Role Within the Pipeline

        Upon successful evaluation, the function returns four key outputs:

        • score— Overall structural quality metric
        • vol_ratio — Relative participation measurement
        • velocity_pips— ATR-normalized departure strength
        • tier— Structural quality classification

        These values are passed directly to TryCreateAutoZone(), where the zone object is instantiated, initialized with its quantitative metadata, and admitted into the zone lifecycle engine for continuous monitoring and evolution.

        //+------------------------------------------------------------------+
        //| Quantitative Analyzer: Computes Structural Validity Score (0-100)|
        //+------------------------------------------------------------------+
        int AnalyzeZoneCandidate(int pivot_idx, ENUM_ZONE_TYPE type, double p, double atr, double &vol_ratio, double &velocity_pips, string &tier)
          {
           MqlRates rates[];
           ArraySetAsSeries(rates, true);
        
        //--- Dynamically size the lookback allocation based on actual input settings
           int macro_window = SwingBars * 2;
           int sample_period = 14;
        
        //--- Max historical offset needed is the pivot index + whichever requirement is larger
           int max_historical_offset = (macro_window > sample_period) ? macro_window : sample_period;
           int check_bars = pivot_idx + max_historical_offset + VelocityLookaheadBars + 5; // dynamic padding margin
        
           if(CopyRates(_Symbol, _Period, 0, check_bars, rates) < check_bars)
             {
              vol_ratio = 1.0;
              velocity_pips = 0.0;
              tier = "LOW";
              return 0;
             }
        
        //--- 1. HIGH VOLUME RATIO EVALUATION (Strict Zero Floor)
           int vol_score = 0;
           vol_ratio = 1.0;
        
           if(EnableVolumeValidation && (pivot_idx + sample_period) < check_bars)
             {
              double total_vol = 0;
        
              for(int v = 1; v <= sample_period; v++)
                {
                 total_vol += (double)rates[pivot_idx + v].tick_volume;
                }
        
              double avg_vol = total_vol / sample_period;
              double pivot_vol = (double)rates[pivot_idx].tick_volume;
        
              if(avg_vol > 0)
                 vol_ratio = pivot_vol / avg_vol;
        
              //--- Strict floor applied: No pity points below or equal to 0.5 ratio
              if(vol_ratio >= 2.0)
                 vol_score = VolumeWeight;
              else
                 if(vol_ratio <= 0.5)
                    vol_score = 0;
                 else
                   {
                    //--- Linear interpolation mapped to maximum user-defined weight allocation
                    vol_score = (int)(((vol_ratio - 0.5) / 1.5) * VolumeWeight);
                   }
             }
           else
             {
              vol_score = VolumeWeight / 2; // Midpoint baseline assignment if volume processing is disabled
             }
        
        //--- 2. ESCAPE VELOCITY EVALUATION (Dynamic Lookahead Optimization Window)
           int velocity_score = 0;
           velocity_pips = 0.0;
        
           if(pivot_idx >= 1)
             {
              double price_departure = 0.0;
              //--- Secure lookahead pointer: check real-time edge proximity to prevent frozen past readings
              int forward_target_idx = (pivot_idx >= VelocityLookaheadBars) ? (pivot_idx - VelocityLookaheadBars) : 1;
        
              if(type == ZONE_RESISTANCE)
                 price_departure = p - rates[forward_target_idx].close;
              else
                 price_departure = rates[forward_target_idx].close - p;
        
              double pip_multiplier = (_Digits == 3 || _Digits == 5) ? 10.0 : 1.0;
              velocity_pips = (price_departure / _Point) / pip_multiplier;
        
              double displacement_ratio = (atr > 0) ? (price_departure / atr) : 1.0;
        
              if(displacement_ratio >= 2.5)
                 velocity_score = VelocityWeight;
              else
                 if(displacement_ratio <= 0.5)
                    velocity_score = 0;
                 else
                   {
                    //--- Linear interpolation mapped to maximum user-defined weight allocation
                    velocity_score = (int)(((displacement_ratio - 0.5) / 2.0) * VelocityWeight);
                   }
             }
        
        //--- 3. SAFE GEOMETRIC SYMMETRIC EXTENSION CHECK
           int symmetry_score = StructureWeight / 2; // Midpoint default if symmetry fails micro filters
           bool extra_high = true;
           bool extra_low = true;
        
           for(int m = 1; m <= macro_window; m++)
             {
              int lookforward_idx = pivot_idx - m;
              int lookbackward_idx = pivot_idx + m;
        
              if(lookforward_idx >= 0 && lookbackward_idx < check_bars)
                {
                 if(rates[pivot_idx].high <= rates[lookforward_idx].high || rates[pivot_idx].high <= rates[lookbackward_idx].high)
                    extra_high = false;
        
                 if(rates[pivot_idx].low >= rates[lookforward_idx].low || rates[pivot_idx].low >= rates[lookbackward_idx].low)
                    extra_low = false;
                }
              else
                {
                 extra_high = false;
                 extra_low = false;
                 break;
                }
             }
        
           if((type == ZONE_RESISTANCE && extra_high) || (type == ZONE_SUPPORT && extra_low))
             {
              symmetry_score = StructureWeight;
             }
        
        //--- COMBINE ACCUMULATED SCORE METRICS MATRIX (Normalize to Max 100 Cap)
           int total_calculated_score = vol_score + velocity_score + symmetry_score;
           if(total_calculated_score > 100)
              total_calculated_score = 100;
           if(total_calculated_score < 0)
              total_calculated_score = 0;
        
        //--- ASSIGN OBJECTIVE NOMENCLATURE STRINGS WITH ELITE CLASS UPGRADE
           if(total_calculated_score >= 95)
              tier = "ELITE";
           else
              if(total_calculated_score >= 80)
                 tier = "HIGH";
              else
                 if(total_calculated_score >= 50)
                    tier = "MODERATE";
                 else
                    tier = "LOW";
        
           return total_calculated_score;
          }
        


        Zone Registration: TryCreateAutoZone()

        Once a candidate successfully passes quantitative evaluation, it is handed to TryCreateAutoZone(), which serves as the registration layer between zone analysis and lifecycle management. Rather than performing additional structural evaluation, the function is responsible for validating that the candidate can be safely admitted into the runtime framework.

        Before a zone is created, several protective checks are performed to preserve the integrity of the zone database. Invalid structures positioned on the wrong side of the current market are discarded, historical duplicates are rejected, previously blacklisted price levels are ignored, and proximity-based clustering prevents multiple automatic zones from being created around the same market structure.

        After these validation checks succeed, the function constructs a fully initialized zone object using the quantitative metadata produced by AnalyzeZoneCandidate(). This includes the initial structural score, quality tier, volume participation, departure velocity, lifecycle counters, and the initial operational state required for subsequent monitoring.

        Finally, the newly created zone is registered within the central zone manager and recorded through the logging subsystem, making it immediately available for continuous supervision by MonitorZoneLifecycle() . By separating registration from both analysis and lifecycle management, the framework preserves a clear architectural boundary between candidate evaluation, object creation, and behavioral monitoring.

        //+------------------------------------------------------------------+
        //| Filters Structural Creation via Blacklists and Proximity Checks  |
        //+------------------------------------------------------------------+
        void TryCreateAutoZone(double p,
                               ENUM_ZONE_TYPE t,
                               datetime dt,
                               double atr,
                               int score,
                               string tier,
                               double vol_ratio,
                               double velo_pips)
          {
           double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
           double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        
        //--- Immediate validity barrier checks
           if(t == ZONE_SUPPORT && p > bid)
              return;
        
           if(t == ZONE_RESISTANCE && p < ask)
              return;
        
        //--- Prevent historical duplication errors
           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;
             }
        
           double merge_threshold = atr * MergeSensitivity;
        
        //--- Proximity-based blacklist restriction check
           for(int i = 0; i < blacklist.Total(); i++)
             {
              double blacklisted_price = StringToDouble(blacklist.At(i));
              if(MathAbs(blacklisted_price - p) < merge_threshold)
                 return;
             }
        
        //--- Cluster prevention
           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 zone (FULLY INITIALIZED FIRST)
           CZone *nz = new CZone(p, t, dt, atr);
        
        //--- Ensure deterministic ID (no unstable EnumToString(_Period))
           string tf = IntegerToString(_Period);
        
           nz.name = (t == ZONE_SUPPORT ? "S_" : "R_") + tf + "_" +
                     DoubleToString(p, _Digits) + "_" +
                     IntegerToString((long)dt);
        
           nz.is_user_zone = false;
        
        //--- Assign scoring BEFORE logging
           nz.zone_score         = score;
           nz.Current_zone_score = score;
           nz.zone_tier          = tier;
           nz.initial_volume_ratio = vol_ratio;
           nz.departure_velocity   = velo_pips;
        
        //--- Ensure metrics baseline
           nz.touch_count        = 0;
           nz.bounce_count       = 0;
           nz.failure_count      = 0;
           nz.resurrection_count = 0;
           nz.max_reaction_pips  = 0.0;
           nz.breakout_quality   = "NONE";
        
        //--- CRITICAL: register BEFORE logging
           active_zones.Add(nz);
        
        //--- Ensure bounds are valid BEFORE logging
           if(nz.top <= 0.0 || nz.bottom <= 0.0)
             {
              nz.top    = (t == ZONE_RESISTANCE) ? p : p + atr * 0.5;
              nz.bottom = (t == ZONE_SUPPORT) ? p : p - atr * 0.5;
             }
        
        //--- Context string 
           string context_info = StringFormat(
                                    "Score:%d|Tier:%s|VolRatio:%.2f|VeloPips:%.1f|Touches:%d|Bounces:%d|Failures:%d|Resurrections:%d|MaxReaction:%.1f|AgeBars:0|Absorbed:NONE|Status:CREATED",
                                    nz.zone_score,
                                    nz.zone_tier,
                                    nz.initial_volume_ratio,
                                    nz.departure_velocity,
                                    nz.touch_count,
                                    nz.bounce_count,
                                    nz.failure_count,
                                    nz.resurrection_count,
                                    nz.max_reaction_pips
                                 );
        
           string explicit_type = (t == ZONE_SUPPORT) ? "SUPPORT" : "RESISTANCE";
        
           string details = StringFormat(
                               ">>> Quantitative automated %s zone registered successfully.",
                               explicit_type
                            );
        
        //--- FINAL LOG (safe and deterministic)
           logger.LogZone("AUTO",
                          ZE_CREATED,
                          nz.name,
                          "ZONE_" + explicit_type,
                          nz.top,
                          nz.bottom,
                          context_info,
                          details);
          }


        Zone Lifecycle Management: MonitorZoneLifecycle()

        Once a zone has been admitted into the tracking engine, its evaluation does not stop. Financial markets are dynamic, and the significance of a support or resistance level evolves continuously as new price information becomes available.

        The MonitorZoneLifecycle() function is responsible for supervising this evolution. Executed once per completed candle, it monitors every active zone, records market interactions, and updates each zone's internal state without modifying the original structural definition.

        Rather than creating or deleting zones, the function acts as the lifecycle manager responsible for maintaining the health and behavioral history of every active structure.

        The monitoring process consists of four primary responsibilities:

        1. Temporal Score Evolution

        Each completed candle gradually reduces the current confidence score of pending zones using a configurable decay rate. This mechanism reflects the practical observation that structural levels generally lose significance as time passes without meaningful interaction. The original birth score remains permanently preserved, while only the live operational score evolves throughout the zone's lifetime.

        2. Interaction Detection

        The function continuously monitors whether price enters the proximity region surrounding a zone. When price makes contact with the zone for the first time during an interaction cycle, the engine:

        • Records a single touch event
        • Updates the last interaction timestamp
        • Places the zone into a pending state

        This interaction lock guarantees that one market approach generates only one recorded touch, preventing multiple consecutive candles from inflating interaction statistics while price remains inside the zone.

        3. State Transition Preparation

        Once an interaction has been detected, the zone remains locked until the market produces a decisive outcome. At this stage the function does not determine whether the interaction resulted in a successful rejection or a structural failure. Instead, it prepares the zone for subsequent evaluation by the interaction resolution engine.

        This separation of responsibilities isolates interaction detection from interaction interpretation, resulting in a simpler and more modular system architecture.

        4. Lifecycle Telemetry

        Every newly detected interaction is immediately recorded into the zone's historical metadata.

        The engine maintains cumulative statistics including:

        • Total touches
        • Successful bounces
        • Structural failures
        • Resurrection count
        • Current operational score
        • Maximum historical reaction
        • Zone age

        These metrics become part of the persistent lifecycle record and are later used by downstream components for analytics, performance evaluation, and adaptive decision-making.

        Architectural Role

        MonitorZoneLifecycle() serves as the behavioral monitoring layer of the framework.

        Where AnalyzeZoneCandidate() determines whether a pivot deserves to become a tracked zone, MonitorZoneLifecycle() observes how that zone behaves throughout its operational lifetime.

        By separating structural admission from ongoing behavioral monitoring, the framework maintains a clear distinction between zone creation and zone evolution, allowing each subsystem to focus on a single well-defined responsibility.

        //+------------------------------------------------------------------+
        //| Pure Interaction Engine - Counts Touches & Rejections Only       |
        //+------------------------------------------------------------------+
        void MonitorZoneLifecycle(CLogger &log_instance, CArrayObj &zones, double atr, double multiplier)
          {
           static datetime last_bar = 0;
           datetime bar_time = iTime(_Symbol, _Period, 1);
        
           if(bar_time == last_bar)
              return;
        
           last_bar = bar_time;
        
           double tolerance = atr * multiplier;
        
           double high  = iHigh(_Symbol, _Period, 1);
           double low   = iLow(_Symbol, _Period, 1);
           double close = iClose(_Symbol, _Period, 1);
        
           for(int i = zones.Total() - 1; i >= 0; i--)
             {
              CZone *z = (CZone*)zones.At(i);
              if(z == NULL || z.is_broken) // Only track live active structures
                 continue;
        
              //--- Apply Temporal Decay to all live zones once per completed bar
              if(EnableTemporalDecay)
                {
                 z.Current_zone_score = MathMax(0.0, z.Current_zone_score - DecayRatePerBar);
                }
        
              //--- STATE-AWARE ENGINE GATE: Skip evaluation if an interaction is already pending resolution
              if(z.is_pending)
                 continue;
        
              bool touched = (low <= z.top + tolerance && high >= z.bottom - tolerance);
              if(!touched)
                 continue;
        
              //--- Establish Interaction Lock: One interaction = exactly one touch increment
              z.is_pending = true;
              z.touch_count++;
              z.last_touch_time = iTime(_Symbol, _Period, 1);
              z.consecutive_rejection_closes = 0; // Reset rejection validation counter
        
              //--- Calculate current live age of the structure in bars
              int current_age = iBarShift(_Symbol, _Period, z.created);
        
              //--- Construct a standardized, clean context string matching  Part III data matrix
              string context = StringFormat("Score:%d|CurrentScore:%.2f|Birth Tier:%s|VolRatio:%.2f|VeloPips:%.1f|Touches:%d|Bounces:%d|Failures:%d|Resurrections:%d|MaxReaction:%.1f|AgeBars:%d|Status:PENDING_TEST",
                                            z.zone_score, z.Current_zone_score, z.zone_tier, z.initial_volume_ratio, z.departure_velocity,
                                            z.touch_count, z.bounce_count, z.failure_count, z.resurrection_count, z.max_reaction_pips, current_age);
        
              string details = StringFormat(">>> LIVE ZONE PENDING %s | T:%d B:%d F:%d Status:%s",
                                            z.name, z.touch_count, z.bounce_count, z.failure_count, EnumToString(z.status));
        
              log_instance.LogZone("AUTO",
                                   ZE_UPDATED,
                                   z.name,
                                   EnumToString(z.type),
                                   z.top,
                                   z.bottom,
                                   context,
                                   details);
             }
          }
        


        Interaction Resolution Engine: ResolvePendingInteractions()

        While MonitorZoneLifecycle() is responsible for detecting market interactions, the ResolvePendingInteractions() function is responsible for determining their outcome. Executed once per completed candle, the function evaluates every tracked zone currently undergoing an active interaction and decides whether the market has:

        • Successfully rejected the zone
        • Broken through the zone
        • Reclaimed a previously broken zone
        • Expired a broken zone from the tracking system

        Rather than simply recording price contact, the function interprets subsequent market behavior and manages every state transition within the zone lifecycle.

        1. Maximum Reaction Tracking

        Before evaluating any state transitions, the engine continuously measures the greatest distance price has travelled away from each active zone. For support zones, the highest excursion above the zone is recorded.

        For resistance zones, the lowest excursion below the zone is recorded. This value is maintained as MaxReactionPips, representing the strongest historical reaction generated by the zone throughout its lifetime. Unlike the initial velocity score, which measures only the immediate departure after formation, this metric captures the strongest reaction achieved during the zone's operational existence.

        2. Ghost Lifecycle Management

        Once a confirmed structural breakout occurs, the zone is not immediately discarded. Instead, it enters a Ghost state, preserving its structural history while temporarily remaining within the tracking engine.

        During this period the function monitors two possible outcomes:

        • The ghost exceeds its maximum lifetime and is permanently removed.
        • Price reclaims the zone before expiration, allowing the structure to be resurrected.

        This approach acknowledges that important support and resistance levels are frequently reclaimed after false breakouts, allowing the framework to preserve valuable structural information instead of recreating identical zones repeatedly.

        3. Breakout Confirmation

        For zones currently in a pending interaction, the engine evaluates whether price has produced a qualified structural breakout. Breakout validation is intentionally conservative and requires multiple confirmation conditions:

        • Price must exceed the zone boundary by a configurable penetration buffer.
        • Multiple consecutive candle closes beyond the boundary must occur.
        • The interaction must remain structurally consistent throughout the confirmation period.

        Only after all confirmation requirements have been satisfied is the zone classified as broken.

        Upon confirmation the engine:

        • Marks the zone as Broken
        • Activates Ghost mode
        • Applies a configurable score penalty
        • Classifies breakout quality using relative volume and ATR-normalized movement
        • Records the event within the lifecycle telemetry

        This multi-stage confirmation process helps reduce false structural failures caused by temporary price spikes.

        4. Bounce Confirmation

        If price rejects the zone instead of breaking through it, the engine begins evaluating a confirmed bounce. Rather than accepting the first opposing candle as evidence of rejection, the framework requires sustained movement away from the zone over multiple completed candles.

        Once the configured confirmation threshold has been reached:

        • The pending interaction lock is released.
        • The bounce counter is incremented.
        • The live operational score receives a configurable reward.
        • The successful interaction is permanently recorded.

        This confirmation mechanism prevents minor fluctuations around the zone boundary from being incorrectly classified as successful reactions.

        5. State Transition Management

        Throughout its execution, the function manages every major transition within the zone lifecycle.

        Typical transitions include:

        • Virgin → Pending
        • Pending → Bounce Confirmed
        • Pending → Broken
        • Broken → Ghost
        • Ghost → Resurrected
        • Ghost → Expired

        Each transition updates the corresponding lifecycle statistics, operational score, visual state, and historical telemetry, ensuring that every tracked zone accurately reflects its current structural condition.

        Architectural Role

        ResolvePendingInteractions() serves as the behavioral decision engine of the framework.

        Where MonitorZoneLifecycle() detects that price has reached a zone, ResolvePendingInteractions() determines what that interaction ultimately means.

        By separating interaction detection from interaction interpretation, the framework avoids coupling observation with decision-making. This separation results in a cleaner state machine, reduces logical complexity, and allows each subsystem to focus on a single well-defined responsibility within the overall zone lifecycle architecture.

        //+------------------------------------------------------------------+
        //| Evaluates Penetration Bounds, Handles Ghosts, and Resurrections  |
        //+------------------------------------------------------------------+
        void ResolvePendingInteractions(double atr)
          {
           double last_close  = iClose(_Symbol, _Period, 1);
           double last_high   = iHigh(_Symbol, _Period, 1);
           double last_low    = iLow(_Symbol, _Period, 1);
        
           double pip_divider = ((_Digits == 3 || _Digits == 5) ? 10.0 : 1.0) * _Point;
        
           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;
        
              int current_age = iBarShift(_Symbol, _Period, z.created);
        
              //--- LIVE REACTION ENGINE: Tracks maximum dynamic price movement away from zone boundaries
              if(!z.is_broken)
                {
                 double current_reaction = 0.0;
                 if(z.type == ZONE_SUPPORT && last_high > z.top)
                   {
                    current_reaction = (last_high - z.top) / pip_divider;
                    if(current_reaction > z.max_reaction_pips)
                       z.max_reaction_pips = current_reaction;
                   }
                 else
                    if(z.type == ZONE_RESISTANCE && last_low < z.bottom)
                      {
                       current_reaction = (z.bottom - last_low) / pip_divider;
                       if(current_reaction > z.max_reaction_pips)
                          z.max_reaction_pips = current_reaction;
                      }
                }
        
              //--- LAYER 1: GHOST LIFECYCLE MANAGEMENT
              if(z.is_broken)
                {
                 int bars_since_break = iBarShift(_Symbol, _Period, z.broken_time);
        
                 if(bars_since_break > GhostBarLimit)
                   {
                    string dead_name     = z.name;
                    string z_source      = z.is_user_zone ? "USER" : "AUTO";
                    string z_type        = EnumToString(z.type);
                    double z_top         = z.top;
                    double z_bottom      = z.bottom;
        
                    string context_info = StringFormat("Score:%d|CurrentScore:%.2f|Birth Tier:%s|VolRatio:%.2f|VeloPips:%.1f|Touches:%d|Bounces:%d|Failures:%d|Resurrections:%d|MaxReaction:%.1f|AgeBars:%d|Absorbed:NONE|Status:%s",
                                                       z.zone_score, z.Current_zone_score, z.zone_tier, z.initial_volume_ratio, z.departure_velocity,
                                                       z.touch_count, z.bounce_count, z.failure_count, z.resurrection_count, z.max_reaction_pips, current_age,
                                                       "EXTINCT");
        
                    string details = ">>> [ZONE EXTINCTION] Aged ghost zone removed from memory: " + dead_name;
        
                    logger.LogZone(z_source, ZE_DELETED, dead_name, z_type, z_top, z_bottom, context_info, details);
        
                    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;
                    z.resurrection_count++;
                    z.max_reaction_pips = 0.0; // Reset reaction tracking metrics on structural rebirth
        
                    ObjectSetInteger(0, z.name, OBJPROP_COLOR, z.GetColor());
                    ObjectSetInteger(0, z.name, OBJPROP_FILL, true);
        
                    string context_info = StringFormat("Score:%d|CurrentScore:%.2f|Birth Tier:%s|VolRatio:%.2f|VeloPips:%.1f|Touches:%d|Bounces:%d|Failures:%d|Resurrections:%d|MaxReaction:%.1f|AgeBars:%d|Absorbed:NONE|Status:%s",
                                                       z.zone_score, z.Current_zone_score, z.zone_tier, z.initial_volume_ratio, z.departure_velocity,
                                                       z.touch_count, z.bounce_count, z.failure_count, z.resurrection_count, z.max_reaction_pips, current_age,
                                                       "RESURRECTED");
        
                    string details = StringFormat(">>> [ZONE RESURRECTION] Price reclaimed level. Restored: %s at Bar: %d", z.name, bars_since_break);
        
                    logger.LogZone(z.is_user_zone ? "USER" : "AUTO", ZE_RESURRECTED, z.name, EnumToString(z.type), z.top, z.bottom, context_info, details);
                   }
        
                 continue;
                }
        
              // --- LAYER 2: STATE-AWARE INTERACTION RESOLUTION
              if(!z.is_pending)
                 continue;
        
        
              //--- The interaction lock never expires due to time/stagnation.
              //--- It remains locked until a structural Breakout or a confirmed Bounce occurs.
        
              double buffer = MathMax(z.height * BreakBufferPct, atr);
              double bounce_buffer = BounceATRDistance * atr;
        
              bool is_clear_breakout =
                 (z.type == ZONE_SUPPORT && last_close < (z.bottom - buffer)) ||
                 (z.type == ZONE_RESISTANCE && last_close > (z.top + buffer));
        
              bool is_clear_rejection =
                 (z.type == ZONE_SUPPORT && last_close >= (z.top + bounce_buffer)) ||
                 (z.type == ZONE_RESISTANCE && last_close <= (z.bottom - bounce_buffer));
        
              //--- Path A: Evaluate Breakout/Failure Momentum
              if(is_clear_breakout)
                {
                 z.consecutive_rejection_closes = 0;
                 z.consecutive_closes_outside++;
        
                 if(z.consecutive_closes_outside >= BreakConfirmCloses)
                   {
                    z.is_broken = true;
                    z.is_pending = false;
                    z.status = ZONE_BROKEN;
                    z.failure_count++;
                    z.broken_time = TimeCurrent();
        
                    //--- Apply Normalized Structural Penetration Penalty
                    z.Current_zone_score = MathMax(0.0, z.Current_zone_score - PenetrationPenalty);
        
                    MqlRates break_rates[];
                    if(CopyRates(_Symbol, _Period, 1, 1, break_rates) == 1)
                      {
                       double sample_vol = 0;
                       int sample_size = 14;
                       MqlRates vol_samples[];
        
                       if(CopyRates(_Symbol, _Period, 2, sample_size, vol_samples) == sample_size)
                         {
                          for(int v=0; v<sample_size; v++)
                             sample_vol += (double)vol_samples[v].tick_volume;
        
                          double avg_vol = sample_vol / sample_size;
                          double break_vol = (double)break_rates[0].tick_volume;
        
                          double break_vol_ratio = (avg_vol > 0) ? (break_vol / avg_vol) : 1.0;
                          double break_range_pips = (MathAbs(break_rates[0].close - break_rates[0].open) / _Point) /
                                                    ((_Digits == 3 || _Digits == 5) ? 10.0 : 1.0);
        
                          if(break_vol_ratio >= 1.8 && break_range_pips >= (atr / _Point) * 0.8)
                             z.breakout_quality = "STRONG";
                          else
                             if(break_vol_ratio >= 1.0)
                                z.breakout_quality = "MODERATE";
                             else
                                z.breakout_quality = "WEAK";
                         }
                      }
        
                    ObjectSetInteger(0, z.name, OBJPROP_COLOR, clrWhite);
                    ObjectSetInteger(0, z.name, OBJPROP_FILL, false);
        
                    string context_info = StringFormat("Score:%d|CurrentScore:%.2f|Birth Tier:%s|VolRatio:%.2f|VeloPips:%.1f|Touches:%d|Bounces:%d|Failures:%d|Resurrections:%d|MaxReaction:%.1f|AgeBars:%d|Outcome:QUALIFIED_FLIP|BreakForce:%s|Status:%s",
                                                       z.zone_score, z.Current_zone_score, z.zone_tier, z.initial_volume_ratio, z.departure_velocity,
                                                       z.touch_count, z.bounce_count, z.failure_count, z.resurrection_count,
                                                       z.max_reaction_pips, current_age, z.breakout_quality,
                                                       "GHOSTED");
        
                    string details = StringFormat(">>> [ZONE GHOSTED] Level breached via %s force momentum. Ghost mode activated for: %s", z.breakout_quality, z.name);
        
                    logger.LogZone(z.is_user_zone ? "USER" : "AUTO", ZE_GHOSTED, z.name, EnumToString(z.type), z.top, z.bottom, context_info, details);
                   }
                }
        
              //--- Path B: Evaluate Rejection/Bounce Confirmation
              else
                 if(is_clear_rejection)
                   {
                    z.consecutive_closes_outside = 0;
                    z.consecutive_rejection_closes++;
        
                    if(z.consecutive_rejection_closes >= BounceConfirmCloses)
                      {
                       z.is_pending = false;
                       z.bounce_count++;
        
                       //--- Apply Normalized Confirmed Bounce Reward Bonus
                       z.Current_zone_score = MathMin(100.0, z.Current_zone_score + TouchScoreBonus);
        
                       string context_info = StringFormat("Score:%d|CurrentScore:%.2f|Birth Tier:%s|VolRatio:%.2f|VeloPips:%.1f|Touches:%d|Bounces:%d|Failures:%d|Resurrections:%d|MaxReaction:%.1f|AgeBars:%d|Outcome:BOUNCE_CONFIRMED|BreakForce:NONE|Status:%s",
                                                          z.zone_score, z.Current_zone_score, z.zone_tier, z.initial_volume_ratio, z.departure_velocity,
                                                          z.touch_count, z.bounce_count, z.failure_count, z.resurrection_count,
                                                          z.max_reaction_pips, current_age,
                                                          EnumToString(z.status));
        
                       string details = StringFormat(">>> [BOUNCE CONFIRMED] Price rejected level cleanly. Restored: %s | Total Bounces: %d", z.name, z.bounce_count);
        
                       logger.LogZone(z.is_user_zone ? "USER" : "AUTO", ZE_UPDATED, z.name, EnumToString(z.type), z.top, z.bottom, context_info, details);
                      }
                   }
                 else
                   {
                    // Price churning limits inside buffers; maintain interaction window hooks indefinitely
                   }
             }
          }
        


        Operational Walkthrough

        This section demonstrates the operational behavior of the extended zone management framework introduced in Part III. The fundamental runtime workflow of zone creation, lifecycle management, and event-driven execution was established in Parts I and II. Those demonstrations remain the baseline reference for understanding the framework's core behavior and are therefore not repeated here.

        Instead, this walkthrough focuses on the new capabilities introduced in this phase. Specifically, it illustrates how automatically detected zones are quantitatively evaluated before admission, how the enhanced lifecycle engine records richer interaction metadata, and how pending interactions progress through deterministic bounce and breakout resolution.

        The accompanying execution logs highlight the additional telemetry captured by the framework, including structural quality scores, tier classifications, volume participation, departure velocity, evolving lifecycle statistics, and interaction outcomes. Together, these additions demonstrate how the original architecture has been extended into a more comprehensive decision pipeline while preserving the event-driven foundation established in the previous parts.

        Fig. 2. First Segment of detailed info

        Fig. 3. Second segment of detailed log info

        Fig. 4. New log file location


        Conclusion

        Part III extends the zone management framework by introducing quantitative zone analysis and stateful interaction management. Instead of admitting every detected pivot into the system, candidate zones are now evaluated, scored, and classified before becoming active objects within the lifecycle engine.

        Once registered, zones continue to evolve as the market interacts with them. Every interaction is monitored, validated, and resolved through deterministic state transitions, while the logging subsystem records the associated quantitative and lifecycle metadata introduced in this phase.

        Together, these additions demonstrate one practical approach to building a structured and event-driven supply and demand framework. The architecture separates zone analysis, registration, lifecycle monitoring, and interaction resolution into dedicated components. This keeps it modular, extensible, and easier to maintain as new techniques are added.

        The ideas presented throughout this series are not intended to represent the only solution for managing supply and demand zones. Rather, they provide a foundation upon which alternative analytical models and trading methodologies can be developed and evaluated.

        The next phase of this series will shift the focus from framework architecture toward strategy implementation, exploring how the concepts developed across the first three parts can be incorporated into systematic trading decisions.

        The implementation is provided as a modular MQL5 codebase consisting of the following source files and an accompanying archive for direct installation into the MetaTrader terminal directory.

        File Name Description
        MasterLog1.mqh
        A reusable logging module used by the Expert Advisor. It provides the logging functionality required by the project.
        DynamicSR3.mq5 The main Expert Advisor. It demonstrates the Dynamic Support and Resistance interaction engine presented in this article and serves as the entry point for the accompanying source code.
        MQL5.zip
        An archive with the MQL5 folder as its root, holding all files listed above. Unpack it into the terminal installation directory, and each file will be placed in its correct location: DynamicSR3.mq5 in MQL5\Experts\DynamicSR3\ and MasterLog1.mqh in MQL5\Include\DynamicSR3\.
        Attached files |
        MasterLog1.mqh (5.45 KB)
        DynamicSR3.mq5 (57.53 KB)
        MQL5.zip (16.57 KB)
        From Option Chain to 3D Volatility Surface in MetaTrader 5 From Option Chain to 3D Volatility Surface in MetaTrader 5
        This article walks through creating an MT5 indicator that ingests option chains from native symbols or CSV, inverts prices to implied volatility via a hybrid Newton–Raphson/bisection method, and assembles a clean strike–expiry grid. It then renders a shaded, rotatable 3D surface with the platform's DirectX layer, enabling clear, in-terminal analysis of skew and term structure using live or file-based data.
        Persistence Entropy as a Market Regime Indicator in MQL5 Persistence Entropy as a Market Regime Indicator in MQL5
        This article turns the verified TDA pipeline into a live MQL5 indicator. It reduces each price window to two persistence-entropy lines (H0 and H1), computes a normalized loop-strength metric with an adaptive percentile band, and places fade marks only when loop strength is high and price hits a window extreme. You can attach the indicator, read six buffers from an Expert Advisor, and tune key window, ranking, and performance parameters.
        N-BEATS Network-Based Forex EA N-BEATS Network-Based Forex EA
        Implementation of the N-BEATS architecture for Forex trading in MetaTrader 5 with quantile forecasting and adaptive risk management. The architecture is adapted through bilinear normalization and specialized loss functions for financial data. Backtesting on 2025 data shows inability to generate profits, confirming the gap between theoretical achievements and practical trading performance.
        Building an Object-Oriented Order Block Engine in MQL5 Building an Object-Oriented Order Block Engine in MQL5
        The article presents a production-oriented Order Block engine for MQL5 packaged as an include class, it validates zones via displacement and market structure break, maintains mitigation state only on closed bars, and avoids heavy copies by passing data by reference. A diagnostic indicator plots zones, and an EA gates logic to new bars for stable performance and reproducible tests.