preview
Implementing the Decorator Pattern in MQL5: Adding Logging, Timing, and Filtering to Any Indicator Non-Invasively

Implementing the Decorator Pattern in MQL5: Adding Logging, Timing, and Filtering to Any Indicator Non-Invasively

MetaTrader 5Trading systems |
78 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

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.

Decorator runtime chain and class structure

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

ClassTypeResponsibility
IIndicatorAbstract interfaceDefines GetValue() and GetName() contract
CRSIIndicatorConcrete componentComputes RSI values via iRSI() handle
CMovingAverageIndicatorConcrete componentComputes MA values via iMA() handle
CBaseDecoratorAbstract decorator baseHolds IIndicator*, delegates GetValue() and GetName()
CLoggingDecoratorConcrete decoratorLogs indicator name and value to the journal
CTimingDecoratorConcrete decoratorMeasures and logs execution time in microseconds
CThresholdFilterDecoratorConcrete decoratorReturns 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

ApproachBehavior ExtensionSource ChangeRuntime Flexibility
Source modificationIntrusive; alters existing classRequired for every new concernFixed at compile time
InheritanceStatic; each combination needs a subclassNot required for existing classFixed at compile time
Decorator patternDynamic; wrapping applied at constructionNot required for any existing classReconfigurable 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

Decorator delete cascade

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

ChainBehavior
CRSIIndicatorRaw RSI computation only
CLoggingDecorator → CRSIIndicatorRaw RSI with journal logging
CTimingDecorator → CLoggingDecorator → CRSIIndicatorTiming, logging, raw RSI
CThresholdFilterDecorator → CTimingDecorator → CLoggingDecorator → CRSIIndicatorFull pipeline; filter applied after timing and logging observe the raw value
CTimingDecorator → CThresholdFilterDecorator → CLoggingDecorator → CRSIIndicatorFilter 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

RequirementSource ModificationDecorator Pattern
Add new behaviorExisting class must be changedNew decorator class only
Existing code changesRequiredNot required
Chain multiple featuresRequires combined subclassAny combination, any order
Reusability across indicatorsTied to one indicator typeWorks with any IIndicator
Regression riskHigh; existing logic touchedZero; 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.

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:

#NameTypeDescription
1IIndicator.mqhInclude FileAbstract interface declaring GetValue(int shift) and GetName() as the shared contract for all indicators and decorators
2RSIIndicator.mqhInclude File
CRSIIndicator concrete component computing RSI values via a terminal indicator handle; also CMovingAverageIndicator for MA computation
3BaseDecorator.mqhInclude File
CBaseDecorator abstract decorator base class owning an IIndicator* and providing default delegation for GetValue() and GetName()
4LoggingDecorator.mqhInclude File
CLoggingDecorator printing indicator name and value to the journal on each GetValue() call
5TimingDecorator.mqhInclude File
CTimingDecorator measuring execution time of the wrapped chain in microseconds and logging the result
6ThresholdFilterDecorator.mqhInclude File
CThresholdFilterDecorator returning 0.0 for values outside a configurable range, suppressing noise before the value reaches the caller
7CommentPanel.mqhInclude File
CCommentPanel displaying the active decorator chain, raw and filtered values, and the last five bar outputs as a chart comment
 8DecoratorPatternEA.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() 
9Decorator_Pattern.zipZip ArchiveZip archive containing all the attached files and their paths relative to the terminal's root folder.
Building a Divergence System: Creating the MPO4 Custom Indicator Building a Divergence System: Creating the MPO4 Custom Indicator
We introduce MPO4, a pressure-based oscillator that emphasizes the body and direction of candles in the context of current volatility. The article details its mathematics, normalization into a bounded range, and the EMA smoothing, then builds a pivot-driven divergence module designed not to repaint. You get complete MQL5 implementation and practical guidance for interpreting signals, including a comparison with RSI as an alternative source.
Neural Networks in Trading: LSTM Optimization for Multivariate Time Series Forecasting (Final Part) Neural Networks in Trading: LSTM Optimization for Multivariate Time Series Forecasting (Final Part)
We continue to implement the DA-CG-LSTM framework, which offers innovative methods for time series analysis and forecasting. The use of CG-LSTM and dual attention allows for more accurate detection of both long-term and short-term dependencies in data, which is particularly useful for working with financial markets.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Market Simulation: Getting started with SQL in MQL5 (IV) Market Simulation: Getting started with SQL in MQL5 (IV)
Many people tend to underestimate SQL, or even not use it at all, because they do not fully understand how it actually works. When running queries against an SQL database, we are not always looking for a universal answer; in some cases, we need a very specific and practical answer. If a database is created with a proper structure and data model, almost any type of information can be integrated into it.