preview
Building a Hierarchical Market Structure Framework (Prototype) in MQL5 Using Modular Architecture and Event-Driven Design

Building a Hierarchical Market Structure Framework (Prototype) in MQL5 Using Modular Architecture and Event-Driven Design

MetaTrader 5Indicators |
260 0
Stanley Kimathi Kibaara
Stanley Kimathi Kibaara

Introduction

Market structure analysis underpins many discretionary and algorithmic approaches. A trader or developer needs not only to see swing highs and lows, trends, breaks of structure, and changes of character on a chart, but also to obtain that same data in a programmatically accessible form, for an Expert Advisor, a research tool, or a dashboard.

Most existing market structure indicators for MQL5 are closed, monolithic solutions. They draw lines and labels but expose no convenient interface through which another module can query the current structure state, recent events, or active protected levels. The developer must therefore either extract and embed fragments of the indicator logic into an EA, or rewrite the entire algorithm from scratch. Both paths lead to code duplication, tight coupling, and rising maintenance costs.

This article presents a prototype modular framework for market structure analysis, written entirely in MQL5. It is not a trading strategy or a tutorial on Smart Money Concepts; it is an exercise in applied software architecture. The framework identifies swing points, classifies internal and external structure, emits events when a structural break or change of character occurs, maintains a market state machine, persists every activity to disk, and exposes a unified public API. 

The solution is organized into a set of include files and attaches to a chart as a standard custom indicator. Its output can be consumed by EAs, dashboards, machine learning pipelines, and any other system that requires structured market intelligence.

Important: The code is a prototype. The architecture and fundamental logic are sound, but certain components, notably the trend model and confidence heuristic, are deliberately simplified and need further hardening before production use.

Problem statement

When attempting to integrate market structure analysis into an automated system, developers repeatedly encounter the same obstacles.

First, most available indicators tightly couple computational logic with graphical rendering. Reusing only the analytical core without the visual objects forces manual code extraction and adaptation.

Second, the absence of a shared software layer leads to duplicated calculations. The same swing detection algorithm often exists in several places simultaneously (signal generation, risk management, a monitoring panel), degrading performance and creating inconsistencies.

Third, such solutions rarely offer a stable query interface. An external program cannot directly ask, "What is the current structural state?" or "When was the last bullish break of structure?" It must instead work with raw price series or a limited set of indicator buffers that convey only a fraction of the structural picture.

A further weakness is the lack of an event model. When the structure changes, there is no formal notification; the EA must constantly poll the indicator, adding overhead and lag.

Finally, persistence and multi‑timeframe analysis remain pain points. Historical event data is usually lost on restart, and tracking both internal and external structure simultaneously demands manual coordination of multiple indicator instances.

The proposed framework addresses these issues at the architectural level. It is divided into independent modules, incorporates an internal event‑driven model and a state machine, handles MTF synchronization, stores events, and provides a unified data access API. This design allows market structure to be used as a reusable software service within MQL5 applications, rather than as an isolated visual indicator.

Implementation Overview

The project consists of eleven source files: ten .mqh include modules and one .mq5 indicator entry point. This separation supports isolated testing, code reuse, and straightforward maintenance.

    The architecture follows a strict top‑down layering: foundational types sit at the bottom, the event bus rests on the types, then swing/level/break/classifier modules, then the manager, and finally the state machine, persistence, and framework class. Each file includes only the dependencies it actually needs, avoiding circular references and keeping every component testable in isolation.

    Layered architecture diagram showing the include file hierarchy

    Figure 1: Layered architecture diagram showing the include file hierarchy.

    The following sections walk through each component in detail, presenting a representative code snippet for every file.

    Enumerations and Data Structures (MarketStructureTypes.mqh)

    Before any logic can be written, the framework needs a vocabulary. Three enumerations define the types of events, the possible market states, and the structural levels. They are placed in MarketStructureTypes.mqh along with all structures and a small helper class for generating unique event IDs.

    //+------------------------------------------------------------------+
    //| Enumerations                                                     |
    //+------------------------------------------------------------------+
    enum ENUM_EVENT_TYPE
      {
       EVENT_SWING_HIGH_CREATED,        // new swing high confirmed
       EVENT_SWING_LOW_CREATED,         // new swing low confirmed
       EVENT_INTERNAL_BOS_BULLISH,      // internal break of structure, bullish continuation
       EVENT_INTERNAL_BOS_BEARISH,      // internal break of structure, bearish continuation
       EVENT_EXTERNAL_BOS_BULLISH,      // external break of structure, bullish continuation
       EVENT_EXTERNAL_BOS_BEARISH,      // external break of structure, bearish continuation
       EVENT_INTERNAL_CHOCH_BULLISH,    // internal change of character, potential bullish reversal
       EVENT_INTERNAL_CHOCH_BEARISH,    // internal change of character, potential bearish reversal
       EVENT_EXTERNAL_CHOCH_BULLISH,    // external change of character, potential bullish reversal
       EVENT_EXTERNAL_CHOCH_BEARISH,    // external change of character, potential bearish reversal
       EVENT_STATE_CHANGED              // market state machine transitioned to a new state
      };
    
    enum ENUM_MARKET_STATE
      {
       STATE_UNKNOWN,                   // initial state, insufficient data
       STATE_BULLISH,                   // market structurally bullish
       STATE_BEARISH,                   // market structurally bearish
       STATE_TRANSITION,                // potential reversal detected (CHoCH)
       STATE_RANGE                      // no clear directional trend (consolidation)
      };
    
    enum ENUM_STRUCTURE_LEVEL
      {
       LEVEL_INTERNAL,                  // current chart timeframe structure
       LEVEL_EXTERNAL                   // higher timeframe structure
      };
    
    enum ENUM_FRAMEWORK_STATUS
      {
       FS_OK = 0,                       // framework operating normally
       FS_WAITING_FOR_DATA,             // external data or ATR not yet ready
       FS_PARAMETER_ERROR,              // invalid input parameters
       FS_ATR_ERROR,                    // ATR indicator handle creation failed
       FS_COPY_ERROR                    // incomplete price data copy
      };

    The event types distinguish simple swing notifications, break-of-structure events (BOS), change-of-character events (CHoCH), and state changes. The market state enumeration now includes STATE_RANGE, which the trend model uses when there is no clear directional bias.

    Key structures are SStructureEvent (the primary event payload), SSwingPoint (a single pivot), SStructureSnapshot (a complete structural picture for consumers), EventSignature (used for deduplication), SBreakEvent (result of a level break check), and SProtectedLevel (a price extreme currently being defended). All are defined in this single file, making dependencies explicit and easy to follow.

    Event Type
    Meaning
    EVENT_SWING_HIGH_CREATED
    A new swing high has been confirmed
    EVENT_SWING_LOW_CREATED
    A new swing low has been confirmed
    EVENT_INTERNAL_BOS_BULLISH
    Internal break of structure – bullish continuation
    EVENT_INTERNAL_BOS_BEARISH
    Internal break of structure – bearish continuation
    EVENT_INTERNAL_CHOCH_BULLISH
    Internal change of character – potential reversal to bullish
    EVENT_INTERNAL_CHOCH_BEARISH
    Internal change of character – potential reversal to bearish
    EVENT_EXTERNAL_BOS_BULLISH
    External break of structure – bullish continuation
    EVENT_EXTERNAL_BOS_BEARISH
    External break of structure – bearish continuation
    EVENT_EXTERNAL_CHOCH_BULLISH
    External change of character – potential reversal to bullish
    EVENT_EXTERNAL_CHOCH_BEARISH
    External change of character – potential reversal to bearish
    EVENT_STATE_CHANGED  The market state machine transitioned to a new state

    Table 1.  Event Type Definitions

    The Event Bus and Cache (EventBus.mqh)

    The event bus is the communication backbone of the entire framework. It is implemented as a circular buffer with a fixed capacity, capable of storing and retrieving events by either index or event ID. This module also includes a small ring-buffer cache that prevents the same logical event from being published twice, even if the system recalculates history.

    //+------------------------------------------------------------------+
    //| Circular buffer for events (searchable by eventId)               |
    //+------------------------------------------------------------------+
    class CEventBus
      {
    private:
       SStructureEvent      m_events[];                                             // circular buffer storing the actual events
       int                  m_maxSize;                                              // maximum capacity
       int                  m_head;                                                 // write index (next published event goes here)
       int                  m_count;                                                // current number of stored events
    
    public:
                         CEventBus(int maxSize=1000);                               // constructor, defines buffer size
       void                Publish(const SStructureEvent &event);                   // push a new event into the buffer
       int                 GetCount() const { return m_count; }                     // total events currently stored
       bool                GetEvent(int index, SStructureEvent &out) const;         // retrieve event (0 = newest)
       bool                FindEventById(long eventId, SStructureEvent &out) const; // locate event by its unique ID
       void                Clear();                                                 // empty the buffer
      };

    Publish places the event at the current head position and advances the pointer; the oldest event is overwritten when the buffer fills. GetEvent interprets index 0 as the most recent event, which is the natural iteration order for most consumers. FindEventById allows an EA to retrieve a previously seen event by its unique ID.

    The accompanying CEventCache class stores a small ring buffer of EventSignature objects (type, time, price, level). Before an event is published, the cache is checked. If a match is found, the event is suppressed. This prevents duplicate events during full history rebuilds without requiring the entire event bus to be checked on every emission.

    Swing Detector (SwingDetector.mqh)

    The swing detector identifies local maxima and minima in the price series. It is a bounded, non-repainting implementation that uses a configurable look-back window (m_strength). The module is completely self-contained, depending only on MarketStructureTypes.mqh.

    //+------------------------------------------------------------------+
    //| Swing detector – identifies local maxima and minima              |
    //+------------------------------------------------------------------+
    class CSwingDetector
      {
    private:
       int                  m_strength;                                                                          // number of left/right bars that must be lower/higher
       int                  m_maxHistory;                                                                        // maximum bars to scan for performance bounding
       SSwingPoint          m_swingHighs[];                                                                      // array of confirmed swing highs (oldest first)
       SSwingPoint          m_swingLows[];                                                                       // array of confirmed swing lows (oldest first)
       int                  m_highCount;                                                                         // current number of stored swing highs
       int                  m_lowCount;                                                                          // current number of stored swing lows
    
       bool                GetLastOpposite(const datetime checkTime, bool isHigh, SSwingPoint &out);             // find last opposite swing before a given time
       double              CalcBreakoutScore(const SSwingPoint &swing, const SSwingPoint &opposite, double atr); // ATR-based breakout distance score
    
    public:
                         CSwingDetector();
       void                Init(int strength, int maxHistory=1000);                                              // set detection strength and history bounds
       void                Clear();                                                                              // reset all stored swings
    
       bool                IsSwingHigh(int idx, const double &high[]) const;                                     // test if bar idx is a swing high
       bool                IsSwingLow(int idx, const double &low[]) const;                                       // test if bar idx is a swing low
    
       void                AddSwingHigh(datetime time, double price);                                            // register a new swing high
       void                AddSwingLow(datetime time, double price);                                             // register a new swing low
    
       int                 GetSwingHighCount() const { return m_highCount; }                                     // number of swing highs stored
       int                 GetSwingLowCount() const { return m_lowCount; }                                       // number of swing lows stored
       bool                GetSwingHigh(int index, SSwingPoint &out) const;                                      // retrieve a swing high by index (0 = oldest)
       bool                GetSwingLow(int index, SSwingPoint &out) const;                                       // retrieve a swing low by index (0 = oldest)
       int                 GetStrength() const { return m_strength; }                                            // current swing strength setting
       double              CalcSwingConfidence(const SSwingPoint &swing, double atr);                            // pure quality score for a swing (no directional bias)
      };

    Rather than rebuilding the entire swing array on every bar, the detector now operates incrementally. The CStructureManager calls IsSwingHigh / IsSwingLow on each new bar and adds new swings as they are confirmed. The confidence function, CalcSwingConfidence, returns a pure quality score based on the magnitude of the swing (relative to ATR) and the breakout distance beyond the last opposite extreme. It no longer applies a directional bias, so a swing high is not presumed to be bullish; it is simply a pivot. The score is composed of a magnitude component (up to 50 points), a breakout component (up to 30 points), and a neutral baseline (10 points), summed and capped at 100.

    Raw chart before structure detection

    Figure 1: Raw chart before structure detection


    Component
    Maximum Score
    Calculation Basis 
    Magnitude score
    50.0
      Absolute distance to last opposite swing divided by ATR
    Breakout score
    30.0
      Distance beyond the opposite extreme divided by ATR
    Agreement score
    20.0 (trend aligned) or 5.0 (counter-trend) or 10.0 (no trend)
      Whether swing direction matches current trend bias
    Total
    Capped at 100.0
      Sum of the three components

    Table 2.  Confidence Score Components

    Swing detection output

    Figure 2: Swing detection output

    Level Tracker (LevelTracker.mqh)

    The level tracker stores the last few swing points and derives both the current trend and the protected high and low levels. It replaces simplistic trend logic with a more robust model based on linear regression slopes and hysteresis.

    //+------------------------------------------------------------------+
    //| Level Tracker – maintains protected highs/lows and trend model   |
    //+------------------------------------------------------------------+
    class CLevelTracker
      {
    private:
       SSwingPoint          m_swingHighs[3];                                                                  // rolling history of the last three swing highs
       SSwingPoint          m_swingLows[3];                                                                   // rolling history of the last three swing lows
       int                  m_highCount;                                                                      // number of swing highs currently stored (max 3)
       int                  m_lowCount;                                                                       // number of swing lows currently stored (max 3)
       int                  m_currentTrend;                                                                   // 1 = bullish, -1 = bearish, 0 = neutral/range
       SProtectedLevel      m_protectedHigh;                                                                  // the active protected high (if valid)
       SProtectedLevel      m_protectedLow;                                                                   // the active protected low (if valid)
    
       double               m_atrForTrend;                                                                    // ATR value used to compute the minimum slope threshold
       int                  m_trendConfirmation;                                                              // counter for hysteresis (consecutive identical readings)
       int                  m_previousTrend;                                                                  // raw trend from the previous evaluation
    
       void                UpdateTrend();                                                                     // recalculate trend based on regression slopes
       void                UpdateProtectedLevels();                                                           // select the appropriate protected high/low given the trend
       void                AddToHistory(SSwingPoint &history[], int &count, const SSwingPoint &swing);        // push a new swing into a rolling 3-element history
       double              LinearRegressionSlope(const SSwingPoint &points[], int count, int lookback) const; // compute slope of the last 'lookback' swing points
    
    public:
                         CLevelTracker();
       void                Init();                                                                            // reset all internal state
       void                SetATR(double atr) { m_atrForTrend = atr; }                                        // supply the current ATR for slope threshold
       void                ProcessSwingHigh(const SSwingPoint &swing);                                        // update with a new swing high
       void                ProcessSwingLow(const SSwingPoint &swing);                                         // update with a new swing low
       int                 GetTrend() const { return m_currentTrend; }                                        // current trend direction
       bool                GetProtectedHigh(SProtectedLevel &out) const;                                      // copy out the protected high if valid
       bool                GetProtectedLow(SProtectedLevel &out) const;                                       // copy out the protected low if valid
       void                InvalidateHigh();                                                                  // mark the protected high as broken
       void                InvalidateLow();                                                                   // mark the protected low as broken
      };

    UpdateTrend now looks at the last five swing highs and the last five swing lows. It computes the slope of a linear regression through each set of prices. A trend is considered bullish only if the slope of swing highs is significantly positive and the slope of swing lows is not negative (or vice versa for bearish). A minimum slope threshold, proportional to ATR, prevents small fluctuations from being misread as trends. Hysteresis is applied: the trend only flips when the new reading has persisted for two consecutive evaluations. If the slopes are conflicting or below threshold, the trend is set to 0 (neutral/range).

    UpdateProtectedLevels selects the most recent swing high as the protected high in bullish conditions, but in a bearish trend it demands that the protected high be a lower high. The analogous rule applies to protected lows. When a level is broken, it is invalidated so that subsequent bars do not re-trigger events on the same level.

    Break Detector (BreakDetector.mqh)

    This small module contains a single class responsible for checking whether a bar has violated a protected high or low. It depends only on MarketStructureTypes.mqh.

    //+------------------------------------------------------------------+
    //| Break Detector – checks whether a bar has broken a protected     |
    //| high or a protected low and returns a full break event           |
    //+------------------------------------------------------------------+
    class CBreakDetector
      {
    public:
       bool              CheckBar(datetime barTime,                       // timestamp of the bar being checked
                                  double   barHigh,                       // high of the bar
                                  double   barLow,                        // low of the bar
                                  double   barClose,                      // close of the bar
                                  const SProtectedLevel &protHigh,        // current protected high (may be invalid)
                                  const SProtectedLevel &protLow,         // current protected low (may be invalid)
                                  double   atr,                           // current ATR value for normalization
                                  SBreakEvent &outEvent) const;           // output structure filled when a break occurs
      };

    The method compares the bar’s high against the protected high (if valid) and the bar’s low against the protected low. If a breach is detected, it populates an SBreakEvent structure with the break time, the broken price level, a flag indicating whether the close was beyond the level, and the break distance expressed in ATR multiples. This distance later feeds into a dynamic confidence score for the break event.

    Structure Classifier (StructureClassifier.mqh)

    Once a break has been detected, it must be classified as either a break of structure (BOS) or a change of character (CHoCH). The CStructureClassifier class makes this determination based on the break direction and the current trend.

    //+------------------------------------------------------------------+
    //| Classify a break event as BOS or CHoCH based on trend and level  |
    //+------------------------------------------------------------------+
    ENUM_EVENT_TYPE CStructureClassifier::Classify(const SBreakEvent &breakEv, int trend, ENUM_STRUCTURE_LEVEL level) const
      {
       bool isInternal = (level == LEVEL_INTERNAL);                                                                         // determines whether to emit internal or external event type
    
       //--- High break classification
       if(breakEv.isHighBreak)
         {
          if(trend == 1)                                                                                                    // bullish trend + high break → bullish continuation (BOS)
             return isInternal ? EVENT_INTERNAL_BOS_BULLISH : EVENT_EXTERNAL_BOS_BULLISH;
          else if(trend == -1)                                                                                              // bearish trend + high break → potential bullish reversal (CHoCH)
             return isInternal ? EVENT_INTERNAL_CHOCH_BULLISH : EVENT_EXTERNAL_CHOCH_BULLISH;
          else                                                                                                              // neutral trend → default to BOS (continuation assumption)
             return isInternal ? EVENT_INTERNAL_BOS_BULLISH : EVENT_EXTERNAL_BOS_BULLISH;
         }
       else                                                                                                                 // Low break classification
         {
          if(trend == -1)                                                                                                   // bearish trend + low break → bearish continuation (BOS)
             return isInternal ? EVENT_INTERNAL_BOS_BEARISH : EVENT_EXTERNAL_BOS_BEARISH;
          else if(trend == 1)                                                                                               // bullish trend + low break → potential bearish reversal (CHoCH)
             return isInternal ? EVENT_INTERNAL_CHOCH_BEARISH : EVENT_EXTERNAL_CHOCH_BEARISH;
          else                                                                                                              // neutral trend → default to BOS (continuation assumption)
             return isInternal ? EVENT_INTERNAL_BOS_BEARISH : EVENT_EXTERNAL_BOS_BEARISH;
         }
      }

    The logic is straightforward: a high break during a bullish trend is a continuation (BOS), but the same break during a bearish trend is a potential reversal (CHoCH). When the trend is neutral, the system defaults to BOS classification for both directions, which is a simplification that should be refined in a production version.

    Structure Manager (StructureManager.mqh)

    The structure manager orchestrates the swing detector, level tracker, break detector, and classifier. It processes price bars one at a time (oldest to newest) and emits events for new swing points and structural breaks. This module depends on all the previously described modules.

    //+------------------------------------------------------------------+
    //| Orchestrate swing detection, break detection and event emission  |
    //+------------------------------------------------------------------+
    class CStructureManager
      {
    private:
       ENUM_STRUCTURE_LEVEL m_level;                                                                                     // which structure level this manager handles (internal or external)
       CEventBus           *m_eventBus;                                                                                  // pointer to the central event bus for publishing events
       CEventCache          m_eventCache;                                                                                // local deduplication cache (ring buffer) for events emitted by this manager
       CEventIdCounter     *m_pEventIdCounter;                                                                           // shared counter for generating unique event IDs
       CSwingDetector      *m_detector;                                                                                  // pointer to the swing detector (owned externally, same level as this manager)
    
       CLevelTracker        m_levelTracker;                                                                              // maintains protected levels and trend for this structure
       CBreakDetector       m_breakDetector;                                                                             // stateless checker for level breaks
       CStructureClassifier m_classifier;                                                                                // stateless classifier that converts breaks to BOS/CHoCH event types
    
       int                  m_lastRatesTotal;                                                                            // total number of bars seen in the previous call (used to detect full rebuilds)
       datetime             m_lastProcessedBarTime;                                                                      // timestamp of the most recent bar already processed (prevents reprocessing)
    
       SStructureEvent      m_cachedLastBOS;                                                                             // most recent BOS event (for fast access by consumers)
       SStructureEvent      m_cachedLastCHoCH;                                                                           // most recent CHoCH event (for fast access by consumers)
       bool                 m_hasCachedBOS;                                                                              // indicates that m_cachedLastBOS holds a valid event
       bool                 m_hasCachedCHoCH;                                                                            // indicates that m_cachedLastCHoCH holds a valid event
    
       void                EmitEvent(ENUM_EVENT_TYPE type, datetime time, double price,
                                     int direction, double confidence);                                                  // creates and publishes a deduplicated event
    
    public:
                         CStructureManager();
       void                Init(ENUM_STRUCTURE_LEVEL level, CEventBus *bus,
                                CEventIdCounter *idCounter, CSwingDetector *detector);                                   // initialise with dependencies
       void                ProcessBars(const datetime &time[], const double &high[],
                                       const double &low[], const double &close[],
                                       int rates_total, double atr);                                                     // main update: detect swings, breaks, emit events
       int                 GetTrend() const { return m_levelTracker.GetTrend(); }                                        // current trend direction
       bool                GetProtectedHigh(SProtectedLevel &out) const { return m_levelTracker.GetProtectedHigh(out); } // copy out the active protected high
       bool                GetProtectedLow(SProtectedLevel &out) const { return m_levelTracker.GetProtectedLow(out); }   // copy out the active protected low
       bool                GetLastBOS(SStructureEvent &out);                                                             // retrieve the most recent BOS event
       bool                GetLastCHoCH(SStructureEvent &out);                                                           // retrieve the most recent CHoCH event
      };

    The key method is ProcessBars. It iterates over bars that are newer than the last processed time, checks for swing points, updates the level tracker, then uses the break detector to see if the current protected levels have been penetrated. When a break occurs, the classifier labels it, and an event is emitted with a dynamic confidence score based on the break distance and whether the bar closed beyond the level. The broken level is then invalidated so that subsequent bars do not retrigger on the same structure.

    Internal structure visualization – BOS marked with colored arrow

    Figure 3: Internal structure visualization – BOS marked with colored arrow.

    External structure visualization

    Figure 4: External structure visualization

    Market State Machine (StateMachine.mqh)

    The state machine consumes break events and trend information to maintain a four-state (plus range) model of the market. It is simple, deterministic, and depends only on MarketStructureTypes.mqh and EventBus.mqh.

    //+------------------------------------------------------------------+
    //| Market state machine – consumes break events and trend updates   |
    //+------------------------------------------------------------------+
    class CMarketStateMachine
      {
    private:
       ENUM_MARKET_STATE    m_currentState;                                                      // current market state (BULLISH, BEARISH, TRANSITION, RANGE, UNKNOWN)
       CEventBus           *m_eventBus;                                                          // pointer to the central event bus for publishing state changes
    
       void                SetState(ENUM_MARKET_STATE newState, const SStructureEvent &trigger); // transition to a new state and publish a STATE_CHANGED event
    
    public:
                         CMarketStateMachine();
       void                Init(CEventBus *bus);                                                 // reset the machine and attach to the event bus
       void                ProcessBreakEvent(const SStructureEvent &event, int trend);           // evaluate a new break event and possibly change state
       void                UpdateTrend(int trend);                                               // adjust the state based solely on trend (used when no break events exist)
       ENUM_MARKET_STATE   GetState() const { return m_currentState; }                           // current state
      };

    When a CHoCH event arrives, the machine immediately moves to STATE_TRANSITION. A bullish BOS moves it to STATE_BULLISH, and a bearish BOS to STATE_BEARISH, provided there is no conflicting condition. The UpdateTrend method allows the machine to initialize its state from a purely trend-based reading when no break events have yet occurred; if the trend is flat, it moves to STATE_RANGE. This avoids leaving the state as UNKNOWN during long consolidations.

    [Figure 5: Market state label updating after a structural break]

    Persistence Layer (Persistence.mqh)

    For backtesting and analysis, every structural event is optionally logged to a tab-separated CSV file. This module depends only on MarketStructureTypes.mqh.

    //+------------------------------------------------------------------+
    //| CPersistence – logs structural events to a tab-separated CSV file|
    //+------------------------------------------------------------------+
    class CPersistence
      {
    private:
       bool                 m_enabled;                                                  // true if persistence is active
       string               m_filename;                                                 // full path and name of the output CSV file
       int                  m_fileHandle;                                               // handle to the open file (INVALID_HANDLE when closed)
       string               m_symbol;                                                   // symbol for which events are being logged
       ENUM_TIMEFRAMES      m_tfInternal;                                               // internal timeframe (chart period)
       ENUM_TIMEFRAMES      m_tfExternal;                                               // external higher timeframe
       int                  m_flushInterval;                                            // number of writes between forced flushes to disk
       int                  m_flushCounter;                                             // current count of writes since last flush
       string               m_sessionUUID;                                              // unique identifier for this logging session
    
       void                WriteHeader();                                               // write version, UUID, and column names to the file
    
    public:
                         CPersistence();
       void                Init(bool enable, string symbol,
                                ENUM_TIMEFRAMES internalTF, ENUM_TIMEFRAMES externalTF,
                                int flushInterval=10);                                  // initialise the logger and open the file
       void                WriteEvent(const SStructureEvent &event);                    // log a single event row
       void                Close();                                                     // flush and close the file
      };

    The CSV header includes a version tag, a unique session identifier, and column names for event ID, symbol, timeframe, bar index, event type, price, confidence, direction, level, and state. Events are buffered and flushed only every m_flushInterval writes, which greatly reduces disk I/O compared to flushing after every single event. The file is closed cleanly when the indicator is removed.

    Main Framework Class (MarketStructureFramework.mqh)

    The CMarketStructureFramework class assembles all the components, manages two independent pipelines (internal and external), synchronizes the external timeframe data, and exposes the public API. It depends on every other module.

    //+------------------------------------------------------------------+
    //| Main framework class – aggregates all components, manages MTF    |
    //| synchronization, and exposes the public query API                |
    //+------------------------------------------------------------------+
    class CMarketStructureFramework
      {
    private:
       CSwingDetector        m_internalSwingDetector;                                                            // swing detection on the current timeframe
       CSwingDetector        m_externalSwingDetector;                                                            // swing detection on the higher timeframe
       CStructureManager     m_internalStructure;                                                                // internal structure analysis pipeline
       CStructureManager     m_externalStructure;                                                                // external structure analysis pipeline
       CMarketStateMachine   m_stateMachine;                                                                     // market state machine (consumes break events)
       CEventBus             m_eventBus;                                                                         // central event bus for all events
       CPersistence          m_persistence;                                                                      // CSV logging subsystem
       CEventIdCounter       m_eventIdCounter;                                                                   // monotonic event ID generator
    
       ENUM_TIMEFRAMES       m_externalTF;                                                                       // higher timeframe for external structure
       datetime              m_externalLastBarTime;                                                              // last bar time processed for the external TF
       double                m_externalHighBuffer[];                                                             // array of external TF high prices (series)
       double                m_externalLowBuffer[];                                                              // array of external TF low prices (series)
       double                m_externalCloseBuffer[];                                                            // array of external TF close prices (series)
       datetime              m_externalTimeBuffer[];                                                             // array of external TF bar times (series)
       int                   m_externalBarCount;                                                                 // number of bars currently stored in external buffers
       int                   m_extWindowSize;                                                                    // size of the external history window (bars)
    
       int                   m_atrHandle;                                                                        // handle for internal TF ATR indicator
       int                   m_atrHandleExt;                                                                     // handle for external TF ATR indicator
       double                m_atrValue;                                                                         // current ATR value for the internal TF
       double                m_atrValueExt;                                                                      // current ATR value for the external TF
    
       int                   m_internalStrength;                                                                 // swing detection strength for internal TF
       int                   m_externalStrength;                                                                 // swing detection strength for external TF
       int                   m_maxSwingHistory;                                                                  // maximum bars to scan for swing detection
       bool                  m_persistenceEnabled;                                                               // true if CSV logging is active
       int                   m_flushInterval;                                                                    // number of writes between forced file flushes
       ENUM_STRUCTURE_LEVEL  m_stateMachineSource;                                                               // which structure drives the market state machine
    
       datetime              m_lastPersistedEventTime;                                                           // time of the newest event already written to CSV
       datetime              m_lastStateEventTime;                                                               // time of the newest break event processed by the state machine
    
       ENUM_FRAMEWORK_STATUS m_status;                                                                           // current operational status of the framework
       bool                  m_extDataWaitingLogged;                                                             // flag to log "waiting for external data" only once
       bool                  m_atrDataWaitingLogged;                                                             // flag to log "waiting for ATR" only once
    
       void                UpdateExternalData();                                                                 // fetches new external TF bars if a new bar has appeared
    
    public:
                         CMarketStructureFramework();
       bool                Init(int internalStrength, int externalStrength,
                                ENUM_TIMEFRAMES externalTF, bool persistenceEnabled,
                                string symbol, ENUM_TIMEFRAMES currentTF,
                                int extWindowSize, int maxSwingHistory,
                                ENUM_STRUCTURE_LEVEL stateSource,
                                int flushInterval=10);
       void                OnCalculate(const datetime &time[],
                                       const double &high[],
                                       const double &low[],
                                       const double &close[],
                                       int rates_total);
       void                OnDeinit();
    
       ENUM_MARKET_STATE   GetCurrentState();                                                                    // current market state
       int                 GetCurrentBias();                                                                     // 1=bullish, -1=bearish, 0=neutral/unknown
       bool                GetLastBOS(SStructureEvent &out, ENUM_STRUCTURE_LEVEL level=LEVEL_EXTERNAL);
       bool                GetLastCHoCH(SStructureEvent &out, ENUM_STRUCTURE_LEVEL level=LEVEL_EXTERNAL);
       int                 GetSwingCount(bool isHigh, ENUM_STRUCTURE_LEVEL level);
       bool                GetSwingPoint(bool isHigh, ENUM_STRUCTURE_LEVEL level, int index, SSwingPoint &out);
       int                 GetRecentEventsCount();                                                               // total events in the bus
       bool                GetRecentEvent(int index, SStructureEvent &out);                                      // retrieve event by index (0 = newest)
    
       long                GetLastEventId() const { return m_eventIdCounter.Current(); }
       bool                HasNewEvents(long &lastSeenId, long &fromId, long &toId);
       bool                GetEventById(long eventId, SStructureEvent &out);
       bool                GetCurrentStructureSnapshot(SStructureSnapshot &snap);
       bool                GetProtectedLevels(double &high, double &low);
       ENUM_FRAMEWORK_STATUS GetStatus() const { return m_status; }
      };

    The public API includes methods like GetCurrentState(), GetCurrentBias(), GetLastBOS(), GetLastCHoCH(), GetSwingCount(), GetSwingPoint(), GetRecentEventsCount(), GetEventById(), HasNewEvents(), GetCurrentStructureSnapshot(), and GetProtectedLevels(). An EA can query any of these without ever touching the indicator’s buffers. For example, an EA can call GetProtectedLevels(double &high, double &low) on every tick to monitor intra-bar breakouts, even though the indicator itself is bar-close driven.

    The external data pipeline uses SeriesInfoInteger to detect new external bars and copies the time, high, low, and close arrays only when needed. The ATR for each timeframe is obtained via separate iATR handles, and the values are updated once per bar.

    Indicator Wrapper (MarketStructureIndicator.mq5)

    The final module is the indicator file that attaches to a chart. It declares four buffers (swing highs, swing lows, and two invisible buffers for protected levels), creates an instance of CMarketStructureFramework, and fills the buffers on each new bar. It also manages chart objects (arrows for BOS/CHoCH, a text label for the market state) using a light object tracking system that avoids wholesale deletion of objects.

    //+------------------------------------------------------------------+
    //| Input parameters                                                 |
    //+------------------------------------------------------------------+
    input int               InpInternalSwingStrength = 5;                 // internal swing detection strength (bars)
    input int               InpExternalSwingStrength = 8;                 // external swing detection strength (bars)
    input ENUM_TIMEFRAMES   InpExternalTimeframe = PERIOD_H4;             // higher timeframe for external structure
    input int               InpExternalHistoryBars = 500;                 // number of external bars to keep
    input int               InpMaxSwingHistory = 1000;                    // maximum bars to scan for swings
    input ENUM_STRUCTURE_LEVEL InpStateMachineSource = LEVEL_EXTERNAL;    // which structure drives the market state
    input bool              InpEnablePersistence = false;                 // enable CSV event logging
    input int               InpPersistenceFlushInterval = 10;             // events per file flush
    input bool              InpShowObjects = true;                        // display chart objects (arrows, label)
    input int               InpStateLabelYOffset = 40;                    // vertical pixel offset for state label
    
    CMarketStructureFramework *gFramework = NULL;                         // main framework instance
    double         SwingHighBuffer[];                                     // buffer for swing high arrows
    double         SwingLowBuffer[];                                      // buffer for swing low arrows
    double         ProtectedHighBuffer[];                                 // buffer for protected high (for EA access)
    double         ProtectedLowBuffer[];                                  // buffer for protected low (for EA access)

    Protected level buffers are placed at bar index 0 (the most recent bar) and contain the current protected high and low prices. An EA can read them with iCustom on every tick, enabling real-time breakout monitoring without modifying the indicator’s core bar-close logic.

    Full framework output

    Figure 6: Full framework output – all elements visible simultaneously

    Performance and Resource Usage

    The prototype has been tested on charts with over 20,000 bars. The bounded swing detection and incremental bar processing keep the per-bar cost low.

    Operation
    Typical Time (per new bar)
    Swing detection (bounded)
    < 3 ms
    Structure manager update
    < 1 ms
    External data copy (500 bars)
    < 2 ms
    State machine processing
    < 0.1 ms per event
    Persistence write (buffered)
    < 0.2 ms per event
    Total OnCalculate (new bar)
    ~7 ms

    Table 3. Approximate performance on a 20,000-bar chart (standard desktop CPU). Not a rigorous benchmark.

    Known Limitations

    This prototype is not production-ready. Several limitations are deliberately present and should be addressed before deploying the framework in a live or commercial setting.
    1. Simplified BOS/CHoCH Logic – Classification depends solely on break direction and the current trend. In a real market, confirmation by close, minimum break distance, retests, and the relationship to multiple structural levels would greatly improve accuracy. The current model can produce false events in choppy conditions.
    2. Trend Model is Basic – The trend uses a linear regression slope over only five recent swings. It works reasonably in trending markets but can flip unnecessarily during complex corrections. A more sophisticated model would incorporate amplitude, time decay, and volume.
    3. Bar-Close Driven Only – The indicator processes data only on new bars. Intra-bar break detection is not performed by the indicator itself. The protected level buffers are exposed for EA-side monitoring, but the framework does not emit events until the bar closes.
    4. Multi-Timeframe Synchronization – The external pipeline assumes that sufficient history is available and does not handle edge cases such as partial data gaps, delayed indicator calculations, or the alignment of external events with the internal chart’s time scale.
    5. Event Bus is Internal – The event bus is an in-memory circular buffer, not a true asynchronous inter-process messaging system. An EA must poll the framework (or read indicator buffers) to receive events.
    6. Persistence is Basic – The CSV log is useful, but there is no format versioning beyond a header comment, no built-in protection against duplicates during reinitialization, and the buffered flush may lose a small number of events if the terminal crashes between flushes.
    7. No Unit Tests – The modules are logically separated, but no automated tests are provided. Edge case validation (gap openings, zero-volume bars, historical data recalculation) has not been systematically performed.

    Conclusion

    The motivation behind this work was to fill a distinct gap: MQL5 lacked a reusable and well-architected library dedicated to market structure. The existing tools were monolithic, inseparable from their graphical output, and offered no programmable interface. Developers who needed structural logic in automated strategies were forced to duplicate code or build everything from scratch.

    This prototype addresses that gap. It separates swing detection, level tracking, break detection, classification, state management, and persistence into independent modules. It supports both internal and external structure analysis, emits detailed events, and exposes a clean public API that any EA can consume. The modular design makes it easier to test, extend, and maintain than a single-file implementation.

    The prototype does not promise any trading performance. It does not suggest entry or exit points. It is not a complete trading system. It is an engineering starting point, a foundation upon which a more robust, production-grade framework can be built. The known limitations are clearly stated, and the path for improvement is straightforward: refine the classification logic, strengthen the trend model, add unit tests, and harden the persistence and MTF synchronization layers.

    Future work could include adding a third “context” timeframe, integrating volume-based confirmation, exposing the framework over a socket for external Python or R scripts, or implementing a real asynchronous notification mechanism. Because every layer is independent and communicates only through the event bus, such additions can be made without destabilizing the existing structure.

    Clean, maintainable, modular infrastructure solves the real-world engineering problem of making market structure analysis programmatically accessible in MQL5. This prototype provides that infrastructure in a form that is ready for study, experimentation, and further development.


    Programs used in the article:

    # Name type Description
    1 MarketStructureTypes.mqh
    Include file
    Enumerations, data structures, and the event‑ID counter
    2 EventBus.mqh
    Include file
    Internal event queue and deduplication cache
    3 SwingDetector.mqh
    Include file
    Detection of swing highs and swing lows
    4 LevelTracker.mqh
    Include file
    Trend model and protected‑level management
    5 BreakDetector.mqh
    Include file
    Detection of price breaking a protected level
    6 StructureClassifier.mqh
    Include file
    Classification of a break as BOS or CHoCH
    7 StructureManager.mqh
    Include file
    Orchestrates swing, level, break detection, and emits structural events
    8 StateMachine.mqh
    Include file
    Market state transitions driven by structural events
    9 Persistence.mqh
    Include file
    CSV logging of all structural events
    10 MarketStructureFramework.mqh
    Include file
    Aggregates all components, manages MTF sync, exposes the public API
    11 MarketStructureIndicator.mq5
    Main indicator
    Indicator wrapper; attaches the framework to a chart, handles buffers and visual objects






    Attached files |
    MQL5.zip (27.93 KB)
    Creating a Profit Concentration Analyzer in MQL5 Creating a Profit Concentration Analyzer in MQL5
    Net profit and win rate tell you how much a strategy made, not how the result is distributed. This article builds a native MQL5 script that reads your closed trades and measures profit concentration: the top-N trade share, the Gini coefficient of the winners, an outlier-dependence stress test that removes the best few winners, and the largest day against a prop-firm consistency limit. It combines these into one A+ to F score with recommendations, running inside MetaTrader 5.
    Trust Your Backtest Data First: Building a Reproducible Historical Data Audit in Python for MetaTrader 5 Trust Your Backtest Data First: Building a Reproducible Historical Data Audit in Python for MetaTrader 5
    A reproducible, read-only Python audit for MetaTrader 5 that verifies history quality before any backtest. It exports M5 data from multiple terminals, detects gaps and synthetic bars by timestamp spacing, and reports coverage per year. The same deterministic strategy then runs on three broker feeds over a common window to quantify result drift and decompose it into spread, data/price, and trade effects.
    How To Profile MQL5 Code in MetaEditor How To Profile MQL5 Code in MetaEditor
    This article profiles a rolling z-score indicator with bands using MetaEditor's built-in sampling profiler. We read the Total CPU and Self CPU columns and follow the heat‑mapped source to the true hotspots, replace window rescans with sliding accumulators, remove a redundant array copy, and honor prev_calculated. The result is the same output with measured samples reduced from roughly 7,050 to 59.
    Detecting Structural Breakpoints in Price Series Using CUSUM in MQL5 (Part 1): From Statistical Theory to a Working MQL5 Indicator Detecting Structural Breakpoints in Price Series Using CUSUM in MQL5 (Part 1): From Statistical Theory to a Working MQL5 Indicator
    This article builds a sequential CUSUM breakpoint detector for MetaTrader 5, starting from the statistical construction and ending with a working indicator. It explains standardized log-returns, dual accumulators, the role of k and h, and the ARL₀ baseline from Siegmund. The code walkthrough covers buffer persistence, recalculation handling, idempotent chart objects, and a three-pass engine, so you can compile, attach, and use the detector to flag structural regime shifts earlier than fixed-window smoothers.