Implementing the Decorator Pattern in MQL5: Adding Logging, Timing, and Filtering to Any Indicator Non-Invasively
Introduction
To add execution timing to an existing RSI indicator class, a developer typically inserts GetMicrosecondCount() calls around the computation. When a second developer needs logging, they open the same file and add Print() calls. When a third developer needs threshold filtering, they add conditional return logic. After a few iterations, the RSI class accumulates code unrelated to RSI computation. It contains infrastructure concerns that belong to the calling context, not to the indicator.
This pattern violates the Open-Closed Principle, which states that a class should be open for extension but closed for modification. Each source-code modification to add a cross-cutting concern increases the risk of regressions in the indicator's core computation. The modification must be tested against all existing use cases. If the indicator class is shared across multiple EAs, the change affects all of them simultaneously. If the class is under version control and another developer has made unrelated changes to the same file, merge conflicts become more likely.
The deeper problem is that the concerns being added are not properties of the indicator. Timing, logging, and filtering are properties of how the indicator is used in a specific context. A different EA might need timing but not logging. A backtesting harness might need neither. The indicator class should not carry these concerns at all.

Figure 1: Indicator Decorator Architecture. Left: at runtime, each cross-cutting concern wraps the next — timing around logging around filtering — while the core RSI computation stays unmodified, with every layer delegating GetValue() downward. Right: the corresponding class structure, where the concrete decorators extend a shared base that implements IIndicator and holds a reference to any IIndicator it wraps.
Decorator Pattern Architecture
The decorator pattern solves this by introducing a shared abstraction and a base wrapper class. Every indicator, whether a concrete computation or a wrapper, implements the same IIndicator interface. A decorator holds a pointer to another IIndicator object, delegates the core operation to it, and adds its own behavior before or after the delegation. Because both implement the same interface, they are interchangeable to the caller. The caller holds an IIndicator* and calls GetValue(). Whether that pointer points to a raw RSI object or to a chain of three decorators wrapping a raw RSI object makes no difference to the calling code.
The stacking is achieved by passing one decorator as the wrapped object of another. For example, CTimingDecorator → CLoggingDecorator → CThresholdFilterDecorator → CRSIIndicator forms a call chain in which each layer adds behavior in order. The outermost decorator is the only object the EA interacts with directly. The inner layers are invisible to the caller.
Decorator Interface and Components
| Class | Type | Responsibility |
|---|---|---|
| IIndicator | Abstract interface | Defines GetValue() and GetName() contract |
| CRSIIndicator | Concrete component | Computes RSI values via iRSI() handle |
| CMovingAverageIndicator | Concrete component | Computes MA values via iMA() handle |
| CBaseDecorator | Abstract decorator base | Holds IIndicator*, delegates GetValue() and GetName() |
| CLoggingDecorator | Concrete decorator | Logs indicator name and value to the journal |
| CTimingDecorator | Concrete decorator | Measures and logs execution time in microseconds |
| CThresholdFilterDecorator | Concrete decorator | Returns 0.0 for values that do not meet threshold conditions |
IIndicator — The Shared Contract
Every component in this system, whether a raw indicator or a decorator wrapping one, implements the same interface. This is what makes the chain interchangeable from the caller's perspective.
//+------------------------------------------------------------------+ //| IIndicator.mqh | //| Abstract indicator interface. Every concrete indicator and | //| every decorator implements this contract. Callers depend only | //| on IIndicator*, never on concrete types. | //+------------------------------------------------------------------+ #ifndef IINDICATOR_MQH #define IINDICATOR_MQH //+------------------------------------------------------------------+ //| IIndicator | //| Purpose: Interface defining the standard structural contract for | //| mathematical indicator components and data decorators. | //+------------------------------------------------------------------+ class IIndicator { public: //--- Lifecycle Management virtual ~IIndicator(void) {} //--- Interface Contract Methods virtual double GetValue(int shift) = 0; virtual string GetName(void) const = 0; }; #endif // IINDICATOR_MQH //+------------------------------------------------------------------+
GetValue(int shift) is the single computation entry point. Passing shift = 0 retrieves the current bar's value; passing shift = 1 retrieves the previous bar. GetName() returns a human-readable identity string that decorators build upon to describe the full chain. Because both methods are pure virtual, every class in the system is forced to implement them, which is what allows a CTimingDecorator and a CRSIIndicator to be used interchangeably behind an IIndicator*.
CBaseDecorator — Ownership and Delegation
CBaseDecorator is the structural foundation all decorators inherit from. It holds the pointer to the wrapped object, provides default delegation, and owns the wrapped pointer's lifetime.
//+------------------------------------------------------------------+ //| BaseDecorator.mqh | //| CBaseDecorator: abstract base for all decorators. | //| Owns the wrapped IIndicator* and provides default delegation. | //| Concrete decorators inherit this and override GetValue(). | //+------------------------------------------------------------------+ #ifndef BASEDECORATOR_MQH #define BASEDECORATOR_MQH #include "IIndicator.mqh" //+------------------------------------------------------------------+ //| CBaseDecorator | //| Purpose: Abstract structural base class that implements the | //| IIndicator pattern to wrap and extend indicators. | //+------------------------------------------------------------------+ class CBaseDecorator : public IIndicator { protected: IIndicator *m_wrapped; // Pointer to the wrapped base indicator instance public: //--- Lifecycle Management CBaseDecorator(IIndicator *wrapped); virtual ~CBaseDecorator(void); //--- Interface Implementation Contract virtual double GetValue(int shift); virtual string GetName(void) const; }; //+------------------------------------------------------------------+ //| Constructor | //| Purpose: Instantiates structural layers and binds an indicator | //+------------------------------------------------------------------+ CBaseDecorator::CBaseDecorator(IIndicator *wrapped) : m_wrapped(wrapped) { } //+-------------------------------------------------------------------+ //| Destructor | //| Purpose: Releases downstream dynamically allocated indicators. | //+-------------------------------------------------------------------+ CBaseDecorator::~CBaseDecorator(void) { if(CheckPointer(m_wrapped) == POINTER_DYNAMIC) { delete m_wrapped; m_wrapped = NULL; } } //+-------------------------------------------------------------------+ //| GetValue | //| Purpose: Delegates value retrieval to the wrapped indicator. | //+-------------------------------------------------------------------+ double CBaseDecorator::GetValue(int shift) { if(m_wrapped == NULL) { return(0.0); } return(m_wrapped.GetValue(shift)); } //+------------------------------------------------------------------+ //| GetName | //| Purpose: Cascades identity metadata from wrapped references | //+------------------------------------------------------------------+ string CBaseDecorator::GetName(void) const { if(m_wrapped == NULL) { return("NullDecorator"); } return(m_wrapped.GetName()); } #endif // BASEDECORATOR_MQH //+------------------------------------------------------------------+
The destructor is the critical point of the ownership model. CheckPointer(m_wrapped) == POINTER_DYNAMIC verifies the pointer is a heap-allocated object before calling delete. Deleting the outermost decorator deletes the entire chain; inner objects must not be deleted manually. The GetValue() and GetName() default implementations both guard against a NULL wrapped pointer before delegating, so a partially constructed chain does not crash on evaluation.
CRSIIndicator and CMovingAverageIndicator — Concrete Components
These are the leaf nodes of the chain — the classes that perform actual indicator computation. Neither knows anything about decorators.
//+------------------------------------------------------------------+ //| RSIIndicator.mqh | //| CRSIIndicator: concrete IIndicator computing RSI values. | //| CMovingAverageIndicator: concrete IIndicator computing MA. | //| Both create and own their terminal indicator handles. | //+------------------------------------------------------------------+ #ifndef RSIINDICATOR_MQH #define RSIINDICATOR_MQH #include "IIndicator.mqh" //+------------------------------------------------------------------+ //| CRSIIndicator | //| Purpose: Concrete indicator class implementing the IIndicator | //| interface to provide Relative Strength Index values. | //+------------------------------------------------------------------+ class CRSIIndicator : public IIndicator { private: int m_handle; // Native system indicator handle reference string m_symbol; // Financial instrument symbol name int m_period; // Averaging computation period depth public: //--- Lifecycle Management CRSIIndicator(string symbol, ENUM_TIMEFRAMES tf, int period, ENUM_APPLIED_PRICE applied); ~CRSIIndicator(void); //--- Interface Implementation Contract virtual double GetValue(int shift); virtual string GetName(void) const; }; //+--------------------------------------------------------------------+ //| Constructor | //| Purpose: Validates requirements and hooks terminal indicator state | //+--------------------------------------------------------------------+ CRSIIndicator::CRSIIndicator(string symbol, ENUM_TIMEFRAMES tf, int period, ENUM_APPLIED_PRICE applied) : m_symbol(symbol), m_period(period), m_handle(INVALID_HANDLE) { m_handle = iRSI(symbol, tf, period, applied); if(m_handle == INVALID_HANDLE) { Print("[CRSIIndicator] Failed to create RSI handle for " + symbol); } } //+----------------------------------------------------------------------+ //| Destructor | //| Purpose: Cleanly unloads native resources to prevent resource leaks | //+----------------------------------------------------------------------+ CRSIIndicator::~CRSIIndicator(void) { if(m_handle != INVALID_HANDLE) { IndicatorRelease(m_handle); m_handle = INVALID_HANDLE; } } //+------------------------------------------------------------------+ //| GetValue | //| Purpose: Extracts buffer array metric output by dynamic step pass| //+------------------------------------------------------------------+ double CRSIIndicator::GetValue(int shift) { if(m_handle == INVALID_HANDLE) { return(0.0); } double buf[1]; if(CopyBuffer(m_handle, 0, shift, 1, buf) < 1) { return(0.0); } return(buf[0]); } //+------------------------------------------------------------------+ //| GetName | //| Purpose: Constructs complete localized indicator identity label | //+------------------------------------------------------------------+ string CRSIIndicator::GetName(void) const { return("RSI(" + IntegerToString(m_period) + ")[" + m_symbol + "]"); } //+------------------------------------------------------------------+ //| CMovingAverageIndicator | //| Purpose: Concrete indicator class implementing the IIndicator | //| interface to provide Moving Average values. | //+------------------------------------------------------------------+ class CMovingAverageIndicator : public IIndicator { private: int m_handle; // Native system indicator handle reference string m_symbol; // Financial instrument symbol name int m_period; // Averaging computation period depth public: //--- Lifecycle Management CMovingAverageIndicator(string symbol, ENUM_TIMEFRAMES tf, int period, int shift, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE applied); ~CMovingAverageIndicator(void); //--- Interface Implementation Contract virtual double GetValue(int bar_shift); virtual string GetName(void) const; }; //+--------------------------------------------------------------------+ //| Constructor | //| Purpose: Connects terminal internal structure moving average state | //+--------------------------------------------------------------------+ CMovingAverageIndicator::CMovingAverageIndicator(string symbol, ENUM_TIMEFRAMES tf, int period, int shift, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE applied) : m_symbol(symbol), m_period(period), m_handle(INVALID_HANDLE) { m_handle = iMA(symbol, tf, period, shift, method, applied); if(m_handle == INVALID_HANDLE) { Print("[CMovingAverageIndicator] Failed to create MA handle for " + symbol); } } //+------------------------------------------------------------------+ //| Destructor | //| Purpose: Releases core chart resource pointers on class unloading| //+------------------------------------------------------------------+ CMovingAverageIndicator::~CMovingAverageIndicator(void) { if(m_handle != INVALID_HANDLE) { IndicatorRelease(m_handle); m_handle = INVALID_HANDLE; } } //+------------------------------------------------------------------+ //| GetValue | //| Purpose: Polls and maps core buffer outputs safely into strategy | //+------------------------------------------------------------------+ double CMovingAverageIndicator::GetValue(int bar_shift) { if(m_handle == INVALID_HANDLE) { return(0.0); } double buf[1]; if(CopyBuffer(m_handle, 0, bar_shift, 1, buf) < 1) { return(0.0); } return(buf[0]); } //+------------------------------------------------------------------+ //| GetName | //| Purpose: Generates a unified metadata tracking name tag identity | //+------------------------------------------------------------------+ string CMovingAverageIndicator::GetName(void) const { return("MA(" + IntegerToString(m_period) + ")[" + m_symbol + "]"); } #endif // RSIINDICATOR_MQH //+------------------------------------------------------------------+
Both classes follow the same pattern: the constructor calls the terminal's native handle function (iRSI or iMA) and stores the result. GetValue() guards against INVALID_HANDLE before calling CopyBuffer(), which copies exactly one value from the indicator's buffer at the requested bar shift. The destructor calls IndicatorRelease() to return the handle to the terminal. Neither class has any knowledge of decorators, logging, timing, or filtering — they do one thing and nothing else.
CLoggingDecorator — Passive Observation
The logging decorator intercepts the value produced by the layer beneath it, prints it to the journal, and returns it unchanged. It adds a side effect without modifying the signal.
//+------------------------------------------------------------------+ //| LoggingDecorator.mqh | //| CLoggingDecorator: logs indicator name and value to journal | //| on each GetValue() call. Passes value through unchanged. | //+------------------------------------------------------------------+ #ifndef LOGGINGDECORATOR_MQH #define LOGGINGDECORATOR_MQH #include "BaseDecorator.mqh" //+------------------------------------------------------------------+ //| CLoggingDecorator | //| Purpose: Concrete structural decorator providing passive journal | //| telemetry tracking layer across IIndicator targets. | //+------------------------------------------------------------------+ class CLoggingDecorator : public CBaseDecorator { private: bool m_enabled; // Controls whether logging output is active int m_log_shift; // Only log when shift equals this value (-1 = always) public: //--- Lifecycle Management CLoggingDecorator(IIndicator *wrapped, bool enabled, int log_shift); ~CLoggingDecorator(void) {} //--- Interface Implementation Contract virtual double GetValue(int shift); virtual string GetName(void) const; }; //+------------------------------------------------------------------+ //| Constructor | //| Purpose: Maps base decorator instances and custom tracking flags | //+------------------------------------------------------------------+ CLoggingDecorator::CLoggingDecorator(IIndicator *wrapped, bool enabled, int log_shift) : CBaseDecorator(wrapped), m_enabled(enabled), m_log_shift(log_shift) { } //+------------------------------------------------------------------+ //| GetValue | //| Purpose: Intercepts raw technical calculations and pipes data | //| metrics safely into diagnostic logging streams. | //+------------------------------------------------------------------+ double CLoggingDecorator::GetValue(int shift) { if(m_wrapped == NULL) { return(0.0); } double value = m_wrapped.GetValue(shift); if(m_enabled && (m_log_shift < 0 || shift == m_log_shift)) { Print("[LOGGER] " + m_wrapped.GetName() + " | Shift=" + IntegerToString(shift) + " | Value=" + DoubleToString(value, 5)); } return(value); } //+------------------------------------------------------------------+ //| GetName | //| Purpose: Composes string representation labels reflecting logger | //| decoration presence. | //+------------------------------------------------------------------+ string CLoggingDecorator::GetName(void) const { if(m_wrapped == NULL) { return("Logging > Null"); } return("Logging > " + m_wrapped.GetName()); } #endif // LOGGINGDECORATOR_MQH //+------------------------------------------------------------------+
The m_log_shift parameter controls granularity. Passing 0 restricts logging to the current bar only, which avoids flooding the journal when historical bars are evaluated. Passing -1 logs every shift. The m_enabled flag allows the entire decorator to be silenced at construction time without removing it from the chain — the same chain configuration can run silently in production and verbosely in diagnostics by changing a single input parameter. The return value is always the unmodified value received from the wrapped object.
CTimingDecorator — Execution Measurement
The timing decorator records a microsecond timestamp before delegating and another after, computes the difference, and logs it. Like the logging decorator, it does not alter the value passing through it.
//+------------------------------------------------------------------+ //| TimingDecorator.mqh | //| CTimingDecorator: measures execution time of the wrapped chain | //| in microseconds and logs the result. Value passes through | //| unchanged. | //+------------------------------------------------------------------+ #ifndef TIMINGDECORATOR_MQH #define TIMINGDECORATOR_MQH #include "BaseDecorator.mqh" //+------------------------------------------------------------------+ //| CTimingDecorator | //| Purpose: Concrete structural decorator that measures and tracks | //| execution benchmarking latency over downstream links. | //+------------------------------------------------------------------+ class CTimingDecorator : public CBaseDecorator { private: bool m_enabled; // Controls whether timing output is active long m_last_duration_us; // Most recent measured duration in microseconds public: //--- Lifecycle Management CTimingDecorator(IIndicator *wrapped, bool enabled); ~CTimingDecorator(void) {} //--- Interface Implementation Contract virtual double GetValue(int shift); virtual string GetName(void) const; //--- Class Specific Methods long GetLastDurationUs(void) const; }; //+------------------------------------------------------------------+ //| Constructor | //| Purpose: Sets benchmarking runtime properties for active streams.| //+------------------------------------------------------------------+ CTimingDecorator::CTimingDecorator(IIndicator *wrapped, bool enabled) : CBaseDecorator(wrapped), m_enabled(enabled), m_last_duration_us(0) { } //+------------------------------------------------------------------+ //| GetValue | //| Purpose: Profiles performance metrics by measuring execution | //| microseconds across evaluation updates. | //+------------------------------------------------------------------+ double CTimingDecorator::GetValue(int shift) { if(m_wrapped == NULL) { return(0.0); } long t_start = GetMicrosecondCount(); double value = m_wrapped.GetValue(shift); long t_end = GetMicrosecondCount(); m_last_duration_us = t_end - t_start; if(m_enabled && shift == 0) { Print("[TIMER] " + m_wrapped.GetName() + " | Execution Time = " + IntegerToString(m_last_duration_us) + " us"); } return(value); } //+------------------------------------------------------------------+ //| GetName | //| Purpose: Builds descriptive string reflecting benchmarking | //| decorator layers. | //+------------------------------------------------------------------+ string CTimingDecorator::GetName(void) const { if(m_wrapped == NULL) { return("Timing > Null"); } return("Timing > " + m_wrapped.GetName()); } //+------------------------------------------------------------------+ //| GetLastDurationUs | //| Purpose: Safely returns the most recently captured operational | //| execution delay duration value. | //+------------------------------------------------------------------+ long CTimingDecorator::GetLastDurationUs(void) const { return(m_last_duration_us); } #endif // TIMINGDECORATOR_MQH //+------------------------------------------------------------------+
GetMicrosecondCount() is called immediately before and immediately after the delegation call, so the measured interval includes the full execution cost of everything beneath this decorator in the chain — including all inner decorators and the concrete indicator's CopyBuffer() call. m_last_duration_us stores the result so the EA can retrieve it programmatically via GetLastDurationUs() without re-evaluating the chain. The timing output is restricted to shift == 0 for the same reason as the logging decorator: evaluating historical bars should not produce a flood of timing output.
CThresholdFilterDecorator — Value Suppression
The threshold filter is the only decorator in this system that modifies the value it passes upward. Values within the configured range pass through unchanged; values outside it are replaced with 0.0.
//+------------------------------------------------------------------+ //| ThresholdFilterDecorator.mqh | //| CThresholdFilterDecorator: suppresses values outside a | //| configurable range by returning 0.0. Values within range pass | //| through unchanged. Optionally logs filter decisions. | //+------------------------------------------------------------------+ #ifndef THRESHOLDFILTERDECORATOR_MQH #define THRESHOLDFILTERDECORATOR_MQH #include "BaseDecorator.mqh" //+------------------------------------------------------------------+ //| CThresholdFilterDecorator | //| Purpose: Concrete structural decorator that filters and isolates | //| out-of-bounds metrics across downstream data channels. | //+------------------------------------------------------------------+ class CThresholdFilterDecorator : public CBaseDecorator { private: double m_lower_bound; // Values below this are suppressed double m_upper_bound; // Values above this are suppressed bool m_log_decisions; // When true, filter decisions are printed bool m_last_passed; // Result of most recent threshold evaluation public: //--- Lifecycle Management CThresholdFilterDecorator(IIndicator *wrapped, double lower_bound, double upper_bound, bool log_decisions); ~CThresholdFilterDecorator(void) {} //--- Interface Implementation Contract virtual double GetValue(int shift); virtual string GetName(void) const; //--- Class Specific Methods bool GetLastPassed(void) const; }; //+------------------------------------------------------------------+ //| Constructor | //| Purpose: Assigns filtering thresholds and runtime logging flags. | //+------------------------------------------------------------------+ CThresholdFilterDecorator::CThresholdFilterDecorator(IIndicator *wrapped, double lower_bound, double upper_bound, bool log_decisions) : CBaseDecorator(wrapped), m_lower_bound(lower_bound), m_upper_bound(upper_bound), m_log_decisions(log_decisions), m_last_passed(false) { } //+-------------------------------------------------------------------+ //| GetValue | //| Purpose: Evaluates mathematical constraints against current logic | //| and blocks out-of-range market context data sweeps. | //+-------------------------------------------------------------------+ double CThresholdFilterDecorator::GetValue(int shift) { if(m_wrapped == NULL) { return(0.0); } double raw = m_wrapped.GetValue(shift); bool passed = (raw >= m_lower_bound && raw <= m_upper_bound); m_last_passed = passed; if(m_log_decisions && shift == 0) { string pass_str = passed ? "TRUE" : "FALSE"; Print("[FILTER] " + m_wrapped.GetName() + " | Raw=" + DoubleToString(raw, 5) + " | Range=[" + DoubleToString(m_lower_bound, 2) + "," + DoubleToString(m_upper_bound, 2) + "]" + " | Passed=" + pass_str + " | Output=" + DoubleToString(passed ? raw : 0.0, 5)); } return(passed ? raw : 0.0); } //+------------------------------------------------------------------+ //| GetName | //| Purpose: Composes identity mapping markers describing active | //| structural data clipping paths. | //+------------------------------------------------------------------+ string CThresholdFilterDecorator::GetName(void) const { if(m_wrapped == NULL) { return("Filter > Null"); } return("Filter[" + DoubleToString(m_lower_bound, 0) + "-" + DoubleToString(m_upper_bound, 0) + "] > " + m_wrapped.GetName()); } //+------------------------------------------------------------------+ //| GetLastPassed | //| Purpose: Queries state engine flags checking the last evaluated | //| conditional status pass boundary. | //+------------------------------------------------------------------+ bool CThresholdFilterDecorator::GetLastPassed(void) const { return(m_last_passed); } #endif // THRESHOLDFILTERDECORATOR_MQH //+------------------------------------------------------------------+
This is architecturally distinct from the logging and timing decorators. When passed is false, the value returned to the layer above is 0.0, not the raw value. Any decorator placed above the filter in the chain will see 0.0 rather than the actual indicator reading. This is the stacking-order consequence described in the "Example Decorator Chain Configurations" table: a logging decorator placed above the filter logs the filtered output; a logging decorator placed below the filter logs the raw output. m_last_passed stores the boolean result of the most recent evaluation so the EA can inspect it directly without re-calling the chain.
CCommentPanel — Chart Display
The panel class manages a rolling history of raw and filtered values and renders them as a chart comment on every tick.
//+------------------------------------------------------------------+ //| CommentPanel.mqh | //| CCommentPanel: displays active decorator chain, raw and | //| filtered values, and last five bar outputs as a chart comment. | //+------------------------------------------------------------------+ #ifndef COMMENTPANEL_MQH #define COMMENTPANEL_MQH #include "IIndicator.mqh" #define PANEL_HISTORY_SIZE 5 //+------------------------------------------------------------------+ //| CCommentPanel | //| Purpose: Management panel that stores and displays visual | //| diagnostic summaries of indicator chains on the chart. | //+------------------------------------------------------------------+ class CCommentPanel { private: double m_raw_values[]; // Last N raw values from inner chain double m_filtered_values[]; // Last N filtered values from outer chain int m_stored; // Number of values stored so far public: //--- Lifecycle Management CCommentPanel(void); ~CCommentPanel(void) {} //--- Operational Interface Methods void RecordValues(double raw_value, double filtered_value); void Update(string chain_name, IIndicator *inner, IIndicator *outer); void Clear(void); }; //+------------------------------------------------------------------+ //| Constructor | //| Purpose: Initializes dynamic tracking arrays to fixed historic | //| buffer depths. | //+------------------------------------------------------------------+ CCommentPanel::CCommentPanel(void) : m_stored(0) { ArrayResize(m_raw_values, PANEL_HISTORY_SIZE); ArrayResize(m_filtered_values, PANEL_HISTORY_SIZE); ArrayInitialize(m_raw_values, 0.0); ArrayInitialize(m_filtered_values, 0.0); } //+------------------------------------------------------------------+ //| RecordValues | //| Purpose: Shifts historical data array vectors to commit the | //| latest tracking metrics at index position 0. | //+------------------------------------------------------------------+ void CCommentPanel::RecordValues(double raw_value, double filtered_value) { //--- Shift history: index 0 is most recent for(int i = PANEL_HISTORY_SIZE - 1; i > 0; i--) { m_raw_values[i] = m_raw_values[i - 1]; m_filtered_values[i] = m_filtered_values[i - 1]; } m_raw_values[0] = raw_value; m_filtered_values[0] = filtered_value; if(m_stored < PANEL_HISTORY_SIZE) { m_stored++; } } //+--------------------------------------------------------------------+ //| Update | //| Purpose: Formats runtime pipeline structures and historical values | //| into human-readable chart comments text blocks. | //+--------------------------------------------------------------------+ void CCommentPanel::Update(string chain_name, IIndicator *inner, IIndicator *outer) { string text = "=== Decorator Pattern EA ===\n"; text += "Chain: " + chain_name + "\n"; text += "Inner: " + (inner != NULL ? inner.GetName() : "N/A") + "\n"; text += "Outer: " + (outer != NULL ? outer.GetName() : "N/A") + "\n"; text += "\n"; int display_count = (m_stored < PANEL_HISTORY_SIZE) ? m_stored : PANEL_HISTORY_SIZE; for(int i = 0; i < display_count; i++) { text += "Bar " + IntegerToString(i) + "\n"; text += " Raw = " + DoubleToString(m_raw_values[i], 5) + "\n"; text += " Filtered = " + DoubleToString(m_filtered_values[i], 5) + "\n"; } Comment(text); } //+------------------------------------------------------------------+ //| Clear | //| Purpose: Flushes existing active user-interface comment strings. | //+------------------------------------------------------------------+ void CCommentPanel::Clear(void) { Comment(""); } #endif // COMMENTPANEL_MQH //+------------------------------------------------------------------+
RecordValues() shifts the history arrays right on each call so index 0 always holds the most recent value — the same pattern used by CopyBuffer() itself. m_stored tracks how many values have actually been recorded so the display loop does not show uninitialized zeros during the first few ticks. Update() calls Comment() with the fully formatted string, which MQL5 renders as an overlay on the chart. Clear() calls Comment("") to remove the overlay on deinitialization.
DecoratorPatternEA — Chain Construction and Teardown
The EA's OnInit() is where the entire architecture is exercised. It constructs two separate chains bottom-up and stores only the outermost pointer of each.
//+------------------------------------------------------------------+ //| DecoratorPatternEA.mq5 | //| Demonstration EA: constructs and exercises multiple decorator | //| chains, logs each layer independently, shows timing and | //| filtering effects, updates a chart comment panel, and | //| performs deterministic cleanup in OnDeinit(). | //| | //| Chain A: Timing > Logging > Filter > RSI | //| Chain B: Timing > MA (no logging, no filter) | //| | //| Requires: | //| IIndicator.mqh | //| RSIIndicator.mqh | //| BaseDecorator.mqh | //| LoggingDecorator.mqh | //| TimingDecorator.mqh | //| ThresholdFilterDecorator.mqh | //| CommentPanel.mqh | //+------------------------------------------------------------------+ #property strict #include <Decorator_Pattern/RSIIndicator.mqh> #include <Decorator_Pattern/LoggingDecorator.mqh> #include <Decorator_Pattern/TimingDecorator.mqh> #include <Decorator_Pattern/ThresholdFilterDecorator.mqh> #include <Decorator_Pattern/CommentPanel.mqh> //--- Input parameters input group "== Indicator Configuration ==" input int inp_rsi_period = 14; // RSI Period input ENUM_APPLIED_PRICE inp_rsi_applied = PRICE_CLOSE; // RSI Applied Price input int inp_ma_period = 21; // MA Period input ENUM_TIMEFRAMES inp_timeframe = PERIOD_H1; // Indicator Timeframe input group "== Decorator Configuration ==" input bool inp_enable_logging = true; // Enable Logging Decorator Output input bool inp_enable_timing = true; // Enable Timing Decorator Output input bool inp_enable_filter_log = true; // Enable Filter Decision Logging input double inp_filter_lower = 40.0; // Filter Lower Bound (RSI Units) input double inp_filter_upper = 60.0; // Filter Upper Bound (RSI Units) input group "== Diagnostics ==" input int inp_log_interval = 10; // Tick Interval for Periodic Chain Log //--- Global Context Storage Variables IIndicator *g_chain_a = NULL; // Chain A: Timing > Logging > Filter > RSI IIndicator *g_chain_b = NULL; // Chain B: Timing > MA (no logging, no filter) IIndicator *g_inner_rsi_obs = NULL; // Non-owning observation pointer for raw RSI reads CCommentPanel g_panel; // Dashboard visualization panel long g_tick_count = 0; // Operational incoming tick counter //+------------------------------------------------------------------+ //| Expert initialization function | //| Purpose: Allocates and dynamically links decorator chains | //+------------------------------------------------------------------+ int OnInit(void) { //--- Validate core input logic metrics if(inp_rsi_period <= 0 || inp_ma_period <= 0) { Print("[DecoratorPatternEA] Configuration error: periods must be positive."); return(INIT_PARAMETERS_INCORRECT); } if(inp_filter_lower >= inp_filter_upper) { Print("[DecoratorPatternEA] Configuration error: lower bound must be less than upper."); return(INIT_PARAMETERS_INCORRECT); } //--- Construct Chain A bottom-up //--- Each constructor call transfers ownership to the next layer. CRSIIndicator *rsi = new CRSIIndicator(_Symbol, inp_timeframe, inp_rsi_period, inp_rsi_applied); if(CheckPointer(rsi) != POINTER_DYNAMIC) { Print("[DecoratorPatternEA] Failed to allocate CRSIIndicator."); return(INIT_FAILED); } //--- Non-owning observation pointer for raw RSI reads g_inner_rsi_obs = rsi; CThresholdFilterDecorator *filter = new CThresholdFilterDecorator(rsi, inp_filter_lower, inp_filter_upper, inp_enable_filter_log); if(CheckPointer(filter) != POINTER_DYNAMIC) { delete rsi; Print("[DecoratorPatternEA] Failed to allocate CThresholdFilterDecorator."); return(INIT_FAILED); } CLoggingDecorator *logger = new CLoggingDecorator(filter, inp_enable_logging, 0); if(CheckPointer(logger) != POINTER_DYNAMIC) { delete filter; Print("[DecoratorPatternEA] Failed to allocate CLoggingDecorator."); return(INIT_FAILED); } CTimingDecorator *timer_a = new CTimingDecorator(logger, inp_enable_timing); if(CheckPointer(timer_a) != POINTER_DYNAMIC) { delete logger; Print("[DecoratorPatternEA] Failed to allocate CTimingDecorator for chain A."); return(INIT_FAILED); } g_chain_a = timer_a; //--- Construct Chain B bottom-up: Timing > MA CMovingAverageIndicator *ma = new CMovingAverageIndicator(_Symbol, inp_timeframe, inp_ma_period, 0, MODE_EMA, PRICE_CLOSE); if(CheckPointer(ma) != POINTER_DYNAMIC) { delete g_chain_a; g_chain_a = NULL; Print("[DecoratorPatternEA] Failed to allocate CMovingAverageIndicator."); return(INIT_FAILED); } CTimingDecorator *timer_b = new CTimingDecorator(ma, inp_enable_timing); if(CheckPointer(timer_b) != POINTER_DYNAMIC) { delete ma; delete g_chain_a; g_chain_a = NULL; Print("[DecoratorPatternEA] Failed to allocate CTimingDecorator for chain B."); return(INIT_FAILED); } g_chain_b = timer_b; //--- Print chain structure out to terminal journal logs Print("[DecoratorPatternEA] Chain A: " + g_chain_a.GetName()); Print("[DecoratorPatternEA] Chain B: " + g_chain_b.GetName()); PrintFormat("[DecoratorPatternEA] Initialized on %s %s | RSI: %d | MA: %d | Filter: [%.1f, %.1f]", _Symbol, EnumToString(inp_timeframe), inp_rsi_period, inp_ma_period, inp_filter_lower, inp_filter_upper); return(INIT_SUCCEEDED); } //+-------------------------------------------------------------------+ //| Expert deinitialization function | //| Purpose: Cleanly destroys dynamically allocated decorator chains. | //+-------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Deleting the outermost decorator destroys the entire internal chain branch. if(CheckPointer(g_chain_a) == POINTER_DYNAMIC) { delete g_chain_a; g_chain_a = NULL; g_inner_rsi_obs = NULL; } if(CheckPointer(g_chain_b) == POINTER_DYNAMIC) { delete g_chain_b; g_chain_b = NULL; } g_panel.Clear(); PrintFormat("[DecoratorPatternEA] Deinitialized. Reason code: %d.", reason); } //+------------------------------------------------------------------+ //| Expert tick function | //| Purpose: Executes sequential pipeline data evaluations on ticks | //+------------------------------------------------------------------+ void OnTick(void) { //--- Validate state pointers if(CheckPointer(g_chain_a) != POINTER_DYNAMIC || CheckPointer(g_chain_b) != POINTER_DYNAMIC) { return; } g_tick_count++; //--- Evaluate Chain A (bar 0): full Timing > Logging > Filter > RSI double chain_a_value = g_chain_a.GetValue(0); //--- Read raw RSI value for panel comparison bypassing decorators double raw_rsi = (g_inner_rsi_obs != NULL) ? g_inner_rsi_obs.GetValue(0) : 0.0; //--- Evaluate Chain B (bar 0): Timing > MA double chain_b_value = g_chain_b.GetValue(0); //--- Record metrics and refresh user panel g_panel.RecordValues(raw_rsi, chain_a_value); g_panel.Update(g_chain_a.GetName(), g_inner_rsi_obs, g_chain_a); //--- Periodic chain evaluation writeout to the log. if(g_tick_count % inp_log_interval == 0) { Print("=== Decorator Chain Evaluation | Tick " + IntegerToString(g_tick_count) + " ==="); Print("Chain A name : " + g_chain_a.GetName()); Print("Chain A value : " + DoubleToString(chain_a_value, 5)); Print("Raw RSI value : " + DoubleToString(raw_rsi, 5)); Print("Chain B name : " + g_chain_b.GetName()); Print("Chain B value : " + DoubleToString(chain_b_value, 5)); Print("================================================================"); } } //+------------------------------------------------------------------+
In OnInit(), note three details: First, g_inner_rsi_obs is assigned the rsi pointer before it is passed to the filter constructor. After that assignment, the filter owns the pointer — but g_inner_rsi_obs still points to the same object, allowing OnTick() to read the raw RSI value directly without going through the decorator chain. This is intentional and safe as long as g_inner_rsi_obs is nulled in OnDeinit() immediately after g_chain_a is deleted, which the code does. Second, every allocation failure unwinds only what has already been successfully allocated — the error paths delete the objects that exist at the point of failure and return INIT_FAILED. Third, in OnDeinit(), only g_chain_a and g_chain_b are deleted. All inner pointers are cleaned up by the ownership cascade through CBaseDecorator::~CBaseDecorator().
Composition versus Inheritance
| Approach | Behavior Extension | Source Change | Runtime Flexibility |
|---|---|---|---|
| Source modification | Intrusive; alters existing class | Required for every new concern | Fixed at compile time |
| Inheritance | Static; each combination needs a subclass | Not required for existing class | Fixed at compile time |
| Decorator pattern | Dynamic; wrapping applied at construction | Not required for any existing class | Reconfigurable per EA instance |
The inheritance alternative to the decorator pattern requires a separate subclass for every combination of concerns: CTimedRSI, CLoggedRSI, CTimedLoggedRSI, CTimedLoggedFilteredRSI. With three decorators and two indicator types, that is fourteen potential subclasses. With four decorators and five indicator types, it is seventy-five. The decorator pattern requires only the four decorator classes and the five indicator classes, regardless of how many combinations are used.
Object Ownership Model
Each decorator owns its wrapped object. When a decorator is constructed with an IIndicator*, it takes responsibility for deleting that pointer in its destructor. This ownership chain means that deleting the outermost decorator triggers a cascade of destructions through the entire chain. The EA constructs the chain and holds a pointer to the outermost decorator. When it deletes that pointer, the entire chain is cleaned up without any further action required.
This ownership model has one constraint: once a pointer is passed to a decorator's constructor, the caller must not delete it independently. Doing so would leave the decorator holding a dangling pointer. The practical rule is that the EA constructs the chain bottom-up, passing each inner object to the next outer object's constructor, and retains only the pointer to the outermost object. All inner pointers are surrendered at the point of passing.
CRSIIndicator *rsi = new CRSIIndicator(...); // EA creates CThresholdFilterDecorator *filter = new CThresholdFilterDecorator(rsi, ...); // Takes ownership of rsi CLoggingDecorator *logger = new CLoggingDecorator(filter, ...); // Takes ownership of filter CTimingDecorator *timer = new CTimingDecorator(logger, ...); // Takes ownership of logger //--- EA holds only: timer //--- delete timer; — destroys the entire chain

Figure 2: One delete frees the entire chain. The EA keeps a pointer to only the outermost decorator (CTimingDecorator). Each decorator owns the object directly below it, so calling delete on the outermost runs every destructor in turn — timing, then logging, then filtering, then the core CRSIIndicator last. The chain is built in the opposite order: the EA creates the core indicator first and passes each object into the next decorator's constructor, surrendering ownership at each step and keeping only the outermost pointer.
Execution Flow Analysis
When GetValue() is called on the outermost decorator, the call traverses the chain in declaration order and returns through each layer in reverse order. For the chain CTimingDecorator → CLoggingDecorator → CThresholdFilterDecorator → CRSIIndicator, the execution sequence is:
- CTimingDecorator::GetValue() records start timestamp, delegates to wrapped object
- CLoggingDecorator::GetValue() delegates to wrapped object, prepares to log the result
- CThresholdFilterDecorator::GetValue() delegates to wrapped object, prepares to apply threshold
- CRSIIndicator::GetValue() calls CopyBuffer(), returns the raw RSI double
- CThresholdFilterDecorator receives raw value, applies threshold test, returns filtered value
- CLoggingDecorator receives filtered value, prints it to the journal, returns it unchanged
- CTimingDecorator receives the value, records end timestamp, prints duration, returns value
Each layer receives the value produced by the layer below it. The threshold filter modifies the value before passing it upward. The logger and timer pass the value through unchanged, contributing only side effects. This distinction matters architecturally: a value-transforming decorator changes what subsequent decorators see; a side-effect-only decorator observes without altering the signal.
Example Decorator Chain Configurations
| Chain | Behavior |
|---|---|
| CRSIIndicator | Raw RSI computation only |
| CLoggingDecorator → CRSIIndicator | Raw RSI with journal logging |
| CTimingDecorator → CLoggingDecorator → CRSIIndicator | Timing, logging, raw RSI |
| CThresholdFilterDecorator → CTimingDecorator → CLoggingDecorator → CRSIIndicator | Full pipeline; filter applied after timing and logging observe the raw value |
| CTimingDecorator → CThresholdFilterDecorator → CLoggingDecorator → CRSIIndicator | Filter applied before timing; timing measures only the filtered path |
The order of stacking determines which layers observe the raw value and which observe the filtered value. This is a deliberate design dimension: placing the timing decorator outside the filter means timing measures the full chain including the filter's computation. Placing timing inside the filter means timing measures only the inner indicator's computation and the logging step.
Polymorphic Dispatch Overhead
Each GetValue() call in the chain involves one vtable lookup and one indirect function call. For a chain of four objects, that is four virtual dispatches. Each dispatch costs approximately three to five nanoseconds on a warm cache. The total overhead for a four-level chain is fifteen to twenty nanoseconds.
For context, a single CopyBuffer() call retrieving one value from a loaded indicator typically takes between two hundred and eight hundred nanoseconds depending on buffer state. The decorator chain overhead is below ten percent of the indicator's own computation cost at minimum. For indicators with more complex calculations, the ratio is lower still.
The more significant cost is the stack depth. Each decorator's GetValue() is a separate stack frame. A four-level chain requires four frames to be pushed and popped per call. On a standard call stack with adequate depth for an EA, this is not a constraint. It becomes relevant only if an exceptionally long chain is constructed programmatically, which the architecture does not encourage.
Open-Closed Principle Compliance
Adding a new concern to this architecture requires writing one new class that inherits from CBaseDecorator and implements GetValue() with the new behavior. No existing class is modified. The CRSIIndicator, CLoggingDecorator, CTimingDecorator, and CThresholdFilterDecorator classes remain unchanged. The new decorator can be inserted anywhere in an existing chain without affecting the classes above or below it.
Open-Closed Principle Analysis
| Requirement | Source Modification | Decorator Pattern |
|---|---|---|
| Add new behavior | Existing class must be changed | New decorator class only |
| Existing code changes | Required | Not required |
| Chain multiple features | Requires combined subclass | Any combination, any order |
| Reusability across indicators | Tied to one indicator type | Works with any IIndicator |
| Regression risk | High; existing logic touched | Zero; existing classes unchanged |
A CSmoothingDecorator that applies a moving average to the output of any indicator, or a CClampDecorator that constrains values to a configurable range, or a CCachingDecorator that returns the last computed value when called on the same bar twice, can all be added to the system without touching any file already in the codebase.

Figure 3: Terminal log output demonstrating runtime interception of cross-cutting concerns and conditional data suppression across multiple evaluation ticks.
Conclusion
The decorator pattern enables cross-cutting concerns to be attached to any indicator at construction time without modifying the indicator's source code. Each decorator is a self-contained unit of behavior that delegates computation to its wrapped object and adds exactly one concern before or after the delegation. The chain is assembled in the EA's initialization code, and the outermost pointer is the only interface the EA's trading logic uses.
The Open-Closed Principle compliance is structural rather than aspirational. Adding a new cross-cutting concern to a system built on this architecture is a matter of writing one new class. No existing class is opened, no existing test suite is invalidated, and no existing EA that uses the unchanged indicator classes is affected.
The overhead is bounded: one virtual dispatch and one extra stack frame per layer, plus one allocation per decorator. For indicator computation cycles that already involve CopyBuffer() calls and floating-point arithmetic, this overhead is architecturally insignificant.
The ownership chain, where each decorator owns its wrapped object and the EA owns only the outermost pointer, provides deterministic cleanup with a single delete call and no possibility of leaked handles or orphaned objects, provided the construction convention is followed consistently.
Programs used in the article:
| # | Name | Type | Description |
|---|---|---|---|
| 1 | IIndicator.mqh | Include File | Abstract interface declaring GetValue(int shift) and GetName() as the shared contract for all indicators and decorators |
| 2 | RSIIndicator.mqh | Include File | CRSIIndicator concrete component computing RSI values via a terminal indicator handle; also CMovingAverageIndicator for MA computation |
| 3 | BaseDecorator.mqh | Include File | CBaseDecorator abstract decorator base class owning an IIndicator* and providing default delegation for GetValue() and GetName() |
| 4 | LoggingDecorator.mqh | Include File | CLoggingDecorator printing indicator name and value to the journal on each GetValue() call |
| 5 | TimingDecorator.mqh | Include File | CTimingDecorator measuring execution time of the wrapped chain in microseconds and logging the result |
| 6 | ThresholdFilterDecorator.mqh | Include File | CThresholdFilterDecorator returning 0.0 for values outside a configurable range, suppressing noise before the value reaches the caller |
| 7 | CommentPanel.mqh | Include File | CCommentPanel displaying the active decorator chain, raw and filtered values, and the last five bar outputs as a chart comment |
| 8 | DecoratorPatternEA.mq5 | Demo EA | Demonstration EA constructing and exercising multiple decorator chains, logging each layer independently, updating the chart comment panel, and performing deterministic cleanup in OnDeinit() |
| 9 | Decorator_Pattern.zip | Zip Archive | Zip archive containing all the attached files and their paths relative to the terminal's root folder. |
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.
Building a Divergence System: Creating the MPO4 Custom Indicator
Neural Networks in Trading: LSTM Optimization for Multivariate Time Series Forecasting (Final Part)
Features of Experts Advisors
Market Simulation: Getting started with SQL in MQL5 (IV)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use