Building a Hierarchical Market Structure Framework (Prototype) in MQL5 Using Modular Architecture and Event-Driven Design
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.

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)
//+------------------------------------------------------------------+ //| 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.

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

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.

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

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.

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.- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Creating a Profit Concentration Analyzer in MQL5
Trust Your Backtest Data First: Building a Reproducible Historical Data Audit in Python for MetaTrader 5
How To Profile MQL5 Code in MetaEditor
Detecting Structural Breakpoints in Price Series Using CUSUM in MQL5 (Part 1): From Statistical Theory to a Working MQL5 Indicator
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use