English
preview
Реализация шаблона "Декоратор" в MQL5: Добавление логирования, измерения времени выполнения и фильтрации к любому индикатору без изменения исходного кода

Реализация шаблона "Декоратор" в MQL5: Добавление логирования, измерения времени выполнения и фильтрации к любому индикатору без изменения исходного кода

MetaTrader 5Торговые системы |
50 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Введение

Чтобы добавить измерение времени выполнения в существующий класс индикатора RSI, разработчик обычно размещает вызовы GetMicrosecondCount() до и после вычислений. Когда второму разработчику требуется логирование, он открывает тот же файл и добавляет вызовы Print(). Когда третьему разработчику требуется пороговая фильтрация, он добавляет условную логику возврата. После нескольких итераций класс RSI накапливает код, не связанный с вычислением RSI. Он содержит инфраструктурные сквозные аспекты, относящиеся к вызывающему контексту, а не к индикатору.

Такой подход нарушает принцип открытости/закрытости, согласно которому класс должен быть открыт для расширения, но закрыт для модификации. Каждое изменение исходного кода для добавления сквозного аспекта увеличивает риск регрессий в основной вычислительной части индикатора. Изменение необходимо проверить на всех существующих сценариях использования. Если класс индикатора используется несколькими советниками, изменение одновременно влияет на все эти советники. Если класс находится под контролем версий и другой разработчик внес несвязанные изменения в тот же файл, вероятность конфликтов слияния повышается.

Более глубокая проблема состоит в том, что добавляемые аспекты не являются свойствами индикатора. Измерение времени, логирование и фильтрация относятся к способу использования индикатора в конкретном контексте. Другому советнику может потребоваться измерение времени, но не логирование. Окружению для тестирования на исторических данных может не потребоваться ни то, ни другое. Класс индикатора вообще не должен содержать эти аспекты.

Decorator runtime chain and class structure

Рисунок 1. Архитектура декоратора индикатора. Слева: во время выполнения каждый сквозной аспект оборачивает следующий (измерение времени → логирование → фильтрация). Основное вычисление RSI остается неизменным, а каждый слой делегирует вызов GetValue() нижележащему объекту. Справа: соответствующая структура классов, где конкретные декораторы наследуют общий базовый класс, реализующий IIndicator и содержащий ссылку на обернутый объект IIndicator.



Архитектура шаблона "Декоратор"

Шаблон "Декоратор" решает эту проблему введением общей абстракции и базового класса-обертки. Каждый индикатор, будь то конкретный вычислительный компонент или обертка, реализует один и тот же интерфейс IIndicator. Декоратор хранит указатель на другой объект IIndicator, делегирует ему основную операцию и добавляет собственное поведение до или после делегирования. Поскольку оба реализуют один и тот же интерфейс, они взаимозаменяемы с точки зрения вызывающего кода. Вызывающий код хранит указатель IIndicator* и вызывает метод GetValue(). Для вызывающего кода не имеет значения, указывает ли этот указатель на исходный объект RSI или на цепочку из трех декораторов, оборачивающих исходный объект RSI.

Цепочка строится так: один декоратор передается в конструктор другого как оборачиваемый объект Например, CTimingDecorator → CLoggingDecorator → CThresholdFilterDecorator → CRSIIndicator образует цепочку вызовов, в которой каждый слой последовательно добавляет поведение. Самый внешний декоратор – единственный объект, с которым советник взаимодействует напрямую. Внутренние слои скрыты от вызывающего кода.

Интерфейс и компоненты декоратора

КлассТипЗона ответственности
IIndicatorАбстрактный интерфейсОпределяет контракт методов GetValue() и GetName().
CRSIIndicatorКонкретный компонентВычисляет значения RSI с помощью хэндла iRSI().
CMovingAverageIndicatorКонкретный компонентВычисляет значения MA с помощью хэндла iMA().
CBaseDecoratorАбстрактный базовый класс декоратораХранит IIndicator*, делегирует GetValue() и GetName().
CLoggingDecoratorКонкретный декораторЗаписывает имя и значение индикатора в журнал.
CTimingDecoratorКонкретный декораторИзмеряет и записывает в журнал время выполнения в микросекундах.
CThresholdFilterDecoratorКонкретный декораторВозвращает 0,0 для значений, не соответствующих пороговым условиям.

IIndicator – общий контракт

Каждый компонент системы, будь то исходный индикатор или оборачивающий его декоратор, реализует один и тот же интерфейс. Именно поэтому цепочка взаимозаменяема с точки зрения вызывающего кода.

//+------------------------------------------------------------------+
//|                                                   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) – единственная точка входа для вычисления. Передача shift = 0 возвращает значение текущего бара, а передача shift = 1 – значение предыдущего бара. GetName() возвращает понятную человеку строку-идентификатор, которую декораторы дополняют для описания полной цепочки. Поскольку оба метода являются чисто виртуальными, каждый класс системы обязан их реализовать. Это позволяет взаимозаменяемо использовать CTimingDecorator и CRSIIndicator через указатель IIndicator*.

CBaseDecorator – владение и делегирование

CBaseDecorator – структурная основа, от которой наследуются все декораторы. Он хранит указатель на обернутый объект, обеспечивает делегирование по умолчанию и отвечает за время жизни этого указателя.

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

Деструктор – критически важная часть модели владения. CheckPointer(m_wrapped) == POINTER_DYNAMIC проверяет, указывает ли указатель на объект, выделенный в куче, перед вызовом delete. Удаление внешнего декоратора удаляет всю цепочку; внутренние объекты нельзя удалять вручную. Реализации GetValue() и GetName() по умолчанию перед делегированием проверяют, не равен ли указатель на обернутый объект значению NULL, поэтому даже частично построенная цепочка не приводит к сбою при вычислении.

CRSIIndicator и CMovingAverageIndicator – конкретные компоненты

Это конечные узлы цепочки – классы, выполняющие фактические вычисления индикатора. Ни один из этих классов ничего не знает о декораторах.

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

Оба класса следуют одному шаблону: конструктор вызывает встроенную терминальную функцию создания хэндла (iRSI() или iMA()) и сохраняет результат. GetValue() проверяет INVALID_HANDLE перед вызовом CopyBuffer(), который копирует ровно одно значение из буфера индикатора по запрошенному сдвигу бара. Деструктор вызывает IndicatorRelease(), освобождая хэндл в терминале. Ни один из классов не знает о декораторах, логировании, измерении времени или фильтрации: каждый выполняет только одну задачу.

CLoggingDecorator – пассивное наблюдение

Декоратор логирования перехватывает значение, полученное от нижележащего слоя, выводит его в журнал и возвращает без изменений. Он добавляет побочный эффект, не изменяя сигнал.

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

Параметр m_log_shift управляет уровнем детализации логирования. Передача значения 0 ограничивает логирование только текущим баром, предотвращая переполнение журнала при вычислении исторических баров. Передача значения -1 включает логирование при каждом сдвиге. Флаг m_enabled позволяет отключить весь декоратор при создании без удаления из цепочки. Одна и та же конфигурация цепочки может работать без вывода в рабочей среде, а после изменения одного входного параметра – с подробным выводом при диагностике. Возвращаемое значение всегда совпадает со значением, полученным от обернутого объекта.

CTimingDecorator – измерение времени выполнения

Декоратор измерения времени записывает метку времени в микросекундах до делегирования и после него, вычисляет разность и записывает ее в журнал. Как и декоратор логирования, он не изменяет проходящее через него значение.

//+------------------------------------------------------------------+
//|                                              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() вызывается непосредственно до и сразу после делегирования, поэтому измеряемый интервал включает все выполняемые ниже в цепочке операции, включая внутренние декораторы и вызов CopyBuffer() конкретного индикатора. m_last_duration_us сохраняет результат, чтобы советник мог программно получить его через GetLastDurationUs() без повторного вычисления цепочки. Вывод измерений времени ограничен условием shift == 0 по той же причине, что и логирование: вычисление исторических баров не должно создавать поток сообщений о времени выполнения.

CThresholdFilterDecorator – подавление значений

Пороговый фильтр – единственный декоратор в системе, который изменяет передаваемое верхнему слою значение. Значения в заданном диапазоне передаются без изменений; значения за его пределами заменяются на 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
//+------------------------------------------------------------------+

С архитектурной точки зрения это отличается от декораторов логирования и измерения времени. Когда passed равно false, верхнему слою возвращается 0,0, а не исходное значение. Любой декоратор, расположенный выше фильтра в цепочке, получит 0,0 вместо фактического значения индикатора. Это следствие порядка построения цепочки, описанное в таблице "Примеры конфигураций цепочки декораторов": декоратор логирования выше фильтра записывает в журнал отфильтрованное значение, а ниже фильтра – исходное. m_last_passed хранит булев результат последнего вычисления, чтобы советник мог проверить его без повторного вызова цепочки.

CCommentPanel – отображение на графике

Класс панели управляет скользящей историей исходных и отфильтрованных значений и на каждом тике выводит их в комментарии на графике.

//+------------------------------------------------------------------+
//|                                                 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() при каждом вызове сдвигает массивы истории вправо, поэтому индекс 0 всегда содержит последнее значение. Используется тот же паттерн, что и в CopyBuffer(). Переменная m_stored отслеживает число фактически сохраненных значений, чтобы цикл отображения не выводил неинициализированные нули в первые несколько тиков. Update() вызывает Comment() с полностью отформатированной строкой, которую MQL5 отображает как наложение на графике. Clear() вызывает Comment(""), чтобы удалить наложение при деинициализации.

DecoratorPatternEA – построение и удаление цепочки

Именно в функции OnInit() советника эта архитектура разворачивается на практике. Она строит две отдельные цепочки снизу вверх и сохраняет только указатель на самый внешний объект каждой цепочки.

//+------------------------------------------------------------------+
//|                                           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("================================================================");
     }
  }
//+------------------------------------------------------------------+

В OnInit() важны три детали. Сначала указатель rsi сохраняется в g_inner_rsi_obs с помощью оператора присваивания, а затем передается конструктору фильтра. После этого присваивания фильтр становится владельцем указателя, но g_inner_rsi_obs по-прежнему указывает на тот же объект. Это позволяет OnTick() напрямую считывать исходное значение RSI, не проходя через цепочку декораторов. Это намеренное и безопасное решение при том, что g_inner_rsi_obs обнуляется в OnDeinit() сразу после удаления g_chain_a, что и выполняет код. Во-вторых, при ошибке выделения памяти освобождаются только уже успешно созданные объекты. Ветви обработки ошибок удаляют объекты, существующие на момент сбоя, и возвращают INIT_FAILED. В-третьих, в OnDeinit() удаляются только g_chain_a и g_chain_b. Все внутренние указатели очищаются каскадом владения через CBaseDecorator::~CBaseDecorator().

Сравнение композиции и наследования

ПодходРасширение поведенияИзменение исходного кодаГибкость во время выполнения
Изменение исходного кодаИнвазивное; изменяет существующий классТребуется для каждого нового сквозного аспектаФиксируется во время компиляции
НаследованиеСтатическое; для каждой комбинации требуется подклассНе требуется для существующего классаФиксируется во время компиляции
Шаблон проектирования "Декоратор"Динамическое; оборачивание применяется при созданииНе требуется для изменения существующих классовМожно настраивать отдельно для каждого экземпляра советника

Альтернатива шаблону проектирования "Декоратор", основанная на наследовании, требует отдельного подкласса для каждой комбинации сквозных аспектов: CTimedRSI, CLoggedRSI, CTimedLoggedRSI, CTimedLoggedFilteredRSI. При трех декораторах и двух типах индикаторов получается 14 потенциальных подклассов. При четырех декораторах и пяти типах индикаторов получается 75 подклассов. Шаблон проектирования "Декоратор" требует только четыре класса декораторов и пять классов индикаторов независимо от числа используемых комбинаций.


Модель владения объектами

Каждый декоратор владеет своим обернутым объектом. Когда декоратор создается с указателем IIndicator*, он берет на себя ответственность за удаление этого указателя в своем деструкторе. Эта цепочка владения означает, что удаление внешнего декоратора запускает каскадное уничтожение по всей цепочке. Советник строит цепочку и хранит указатель на самый внешний декоратор. При удалении этого указателя вся цепочка очищается без дополнительных действий.

У этой модели владения есть одно ограничение: после передачи указателя конструктору декоратора вызывающий код не должен удалять его самостоятельно. В противном случае у декоратора останется "висячий" указатель. Практическое правило таково: советник строит цепочку снизу вверх, передает каждый внутренний объект конструктору следующего внешнего объекта и сохраняет только указатель на внешний объект. Все внутренние указатели отдаются во владение в момент их передачи.

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

Рисунок 2. Один вызов delete освобождает всю цепочку. Советник хранит указатель только на самый внешний декоратор (CTimingDecorator). Каждый декоратор владеет непосредственно нижележащим объектом, поэтому вызов delete на внешнем объекте последовательно запускает все деструкторы: сначала измерение времени, затем логирование, затем фильтрацию, а в конце – основной CRSIIndicator. Цепочка строится в обратном порядке: советник сначала создает основной индикатор и передает каждый объект конструктору следующего декоратора, передавая владение на каждом шаге и сохраняя только самый внешний указатель.


Анализ последовательности выполнения

При вызове GetValue() на внешнем декораторе вызов проходит по цепочке в порядке объявления и возвращается через каждый слой в обратном порядке. Для цепочки CTimingDecorator → CLoggingDecorator → CThresholdFilterDecorator → CRSIIndicator последовательность выполнения следующая:

  • CTimingDecorator::GetValue() записывает начальную временную метку и делегирует вызов обернутому объекту;
  • CLoggingDecorator::GetValue() делегирует вызов обернутому объекту и готовится записать результат в журнал;
  • CThresholdFilterDecorator::GetValue() делегирует вызов обернутому объекту и готовится применить порог;
  • CRSIIndicator::GetValue() вызывает CopyBuffer() и возвращает исходное значение RSI типа double;
  • CThresholdFilterDecorator получает исходное значение, применяет пороговую проверку и возвращает отфильтрованное значение;
  • CLoggingDecorator получает отфильтрованное значение, записывает его в журнал и возвращает без изменений;
  • CTimingDecorator получает значение, записывает конечную временную метку, выводит длительность и возвращает значение.

Каждый слой получает значение, сформированное нижележащим слоем. Пороговый фильтр изменяет значение перед передачей верхнему слою. Декораторы логирования и измерения времени передают значение без изменений, добавляя только побочные эффекты. Это различие важно с архитектурной точки зрения: декоратор, преобразующий значение, меняет то, что получают декораторы, расположенные выше в цепочке; декоратор, который только наблюдает и создает побочный эффект, наблюдает за значением, не изменяя сигнал.

Примеры конфигураций цепочек декораторов

ЦепочкаПоведение
CRSIIndicatorТолько вычисление исходного значения RSI
CLoggingDecorator → CRSIIndicatorИсходное значение RSI с логированием в журнал.
CTimingDecorator → CLoggingDecorator → CRSIIndicatorИзмерение времени, логирование, исходное значение RSI.
CThresholdFilterDecorator → CTimingDecorator → CLoggingDecorator → CRSIIndicatorПолный пайплайн; фильтр применяется после того, как декораторы измерения времени и логирования получают исходное значение.
CTimingDecorator → CThresholdFilterDecorator → CLoggingDecorator → CRSIIndicatorФильтр применяется до таймера; таймер измеряет только внутренний путь после фильтрации.

Порядок построения цепочки определяет, какие слои получают исходное значение, а какие – отфильтрованное. Это намеренное архитектурное решение: размещение декоратора измерения времени снаружи фильтра означает, что измеряется вся цепочка, включая вычисления фильтра. Размещение декоратора измерения времени внутри фильтра означает, что измеряются только вычисление внутреннего индикатора и этап логирования.


Накладные расходы полиморфной диспетчеризации

Каждый вызов GetValue() в цепочке включает один поиск в таблице виртуальных функций и один косвенный вызов функции. Для цепочки из четырех объектов это означает четыре виртуальных вызова. Каждая диспетчеризация занимает примерно 3-5 наносекунд при прогретом кэше. Общие накладные расходы для четырехуровневой цепочки составляют 15-20 наносекунд.

Для сравнения, один вызов CopyBuffer(), извлекающий значение из загруженного индикатора, обычно занимает 200-800 наносекунд в зависимости от состояния буфера. Накладные расходы цепочки декораторов составляют менее 10% от минимальной стоимости вычисления индикатора. Для индикаторов с более сложными расчетами это соотношение еще ниже.

Более существенные затраты связаны с глубиной стека. Каждый GetValue() декоратора создает отдельный кадр стека. Для четырехуровневой цепочки при каждом вызове в стек помещаются и извлекаются четыре кадра. На стандартном стеке вызовов с достаточной глубиной для советника это не является ограничением. Это становится значимым только при программном создании исключительно длинной цепочки, чего данная архитектура не предполагает.


Соответствие принципу открытости/закрытости

Добавление нового сквозного аспекта в эту архитектуру требует написания одного нового класса, наследующего CBaseDecorator и реализующего GetValue() с новым поведением. Ни один существующий класс не изменяется. Классы CRSIIndicator, CLoggingDecorator, CTimingDecorator и CThresholdFilterDecorator остаются без изменений. Новый декоратор можно вставить в любое место существующей цепочки, не затрагивая классы выше или ниже него.

Анализ принципа открытости/закрытости

ТребованиеИзменение исходного кодаШаблон проектирования "Декоратор"
Добавление нового поведенияСуществующий класс необходимо изменитьТребуется только новый класс декоратора
Изменение существующего кодаТребуетсяНе требуется
Сочетание нескольких аспектовТребуется объединенный подклассЛюбая комбинация, любой порядок
Повторное использование в разных индикаторахПривязано к одному типу индикатораРаботает с любым IIndicator
Риск регрессийВысокий; затрагивается существующая логикаНулевой; существующие классы не изменяются

В систему можно добавить CSmoothingDecorator, применяющий скользящую среднюю к выходным данным любого индикатора, CClampDecorator, ограничивающий значения настраиваемым диапазоном, или CCachingDecorator, возвращающий последнее вычисленное значение при повторном вызове на том же баре. Для этого не нужно изменять ни один существующий файл кодовой базы.

Figure 3: Terminal log output demonstrating runtime interception of cross-cutting concerns and conditional data suppression across multiple evaluation ticks.

Рисунок 3. Вывод журнала терминала, демонстрирующий перехват сквозных аспектов во время выполнения и условное подавление данных при нескольких тиках вычисления.


Заключение

Шаблон проектирования "Декоратор" позволяет подключать сквозные аспекты к любому индикатору при создании без изменения исходного кода индикатора. Каждый декоратор – самостоятельный блок поведения, делегирующий вычисление обернутому объекту и добавляющий ровно один сквозной аспект до или после делегирования. Цепочка собирается в коде инициализации советника — в функции OnInit(), – а внешний указатель представляет собой единственный интерфейс, используемый торговой логикой советника.

Соблюдение принципа открытости/закрытости обеспечивается архитектурой, а не декларациями. Добавление нового сквозного аспекта в систему, построенную на этой архитектуре, сводится к написанию одного нового класса. Ни один существующий класс не нужно изменять, ни один существующий набор тестов не становится недействительным, и ни один советник, использующий неизмененные классы индикаторов, не затрагивается.

Накладные расходы ограничены: один виртуальный вызов и один дополнительный кадр стека на слой, а также одно выделение памяти на декоратор. Для циклов вычисления индикатора, уже включающих вызовы CopyBuffer() и операции с плавающей запятой, эти накладные расходы архитектурно незначительны.

Цепочка владения, в которой каждый декоратор владеет обернутым объектом, а советник – только внешним указателем, обеспечивает детерминированную очистку одним вызовом delete. Она исключает утечки хэндлов и осиротевшие объекты при последовательном соблюдении соглашения о построении цепочки.


Программы, используемые в статье

#ИмяТипОписание
1IIndicator.mqhВключаемый файлАбстрактный интерфейс, объявляющий GetValue(int shift) и GetName() как общий контракт для всех индикаторов и декораторов.
2RSIIndicator.mqhВключаемый файл
Конкретный компонент CRSIIndicator, вычисляющий значения RSI через хэндл индикатора терминала, а также CMovingAverageIndicator для вычисления MA.
3BaseDecorator.mqhВключаемый файл
CBaseDecorator – абстрактный базовый класс декоратора, владеющий указателем IIndicator* и обеспечивающий делегирование по умолчанию для GetValue() и GetName().
4LoggingDecorator.mqhВключаемый файл
CLoggingDecorator выводит имя и значение индикатора в журнал при каждом вызове GetValue().
5TimingDecorator.mqhВключаемый файл
CTimingDecorator измеряет время выполнения обернутой цепочки в микросекундах и записывает результат в журнал.
6ThresholdFilterDecorator.mqhВключаемый файл
CThresholdFilterDecorator возвращает 0,0 для значений вне настраиваемого диапазона, подавляя шум до того, как значение достигнет вызывающего кода.
7CommentPanel.mqhВключаемый файл
CCommentPanel отображает активную цепочку декораторов, исходные и отфильтрованные значения, а также последние пять значений баров в виде комментария на графике.
 8DecoratorPatternEA.mq5 Демонстрационный советник Демонстрационный советник, создающий и использующий несколько цепочек декораторов, независимо логирующий каждый слой, обновляющий панель комментариев на графике и выполняющий детерминированную очистку в OnDeinit(). 
9Decorator_Pattern.zipZIP-архивZIP-архив, содержащий все прикрепленные файлы и пути к ним относительно корневой папки терминала.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22962

Прикрепленные файлы |
IIndicator.mqh (1.16 KB)
RSIIndicator.mqh (6.8 KB)
BaseDecorator.mqh (3.09 KB)
CommentPanel.mqh (4.41 KB)
Моделирование рынка: Position View (XX) Моделирование рынка: Position View (XX)
В этой статье мы рассмотрим, как изменить код индикатора позиции, чтобы создать своего рода тень, которая позволит нам увидеть, где в данный момент находится цена, всё ещё актуальная на торговом сервере. Этот механизм призван облегчить планирование операций, в ходе которых мы перемещаем уровни стоп-лосса или тейк-профита. Добавление этой функции, то есть ценовых теней, может показаться чрезвычайно сложной задачей. В этой статье я покажу, что реализовать это можно очень просто и на практике.
От начального до среднего уровня: Перегрузка операторов (II) От начального до среднего уровня: Перегрузка операторов (II)
Поначалу эта статья может показаться довольно запутанной из-за материала, который я собираюсь в ней изложить. Тем не менее я постарался изложить всё как можно проще и доступнее. Надеюсь, вы поймёте то, что я здесь покажу, и это когда-нибудь вам пригодится.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Возможности Мастера MQL5, которые вам нужно знать (Часть 96): Использование вейвлетного порогового преобразования и сети LSTM в пользовательском классе управления капиталом Возможности Мастера MQL5, которые вам нужно знать (Часть 96): Использование вейвлетного порогового преобразования и сети LSTM в пользовательском классе управления капиталом
В этой статье рассматривается пользовательский класс Мастера MQL5 для управления капиталом. Пользовательский класс CMoneyWaveletLSTM создается объединением алгоритма вейвлетного порогового преобразования и сети LSTM. Как и в других статьях серии, разработанную модель можно тестировать с советниками, собранными в Мастере MQL5 и настраиваемыми с использованием различных классов трейлинг-стопов и входных сигналов. Как и в предыдущих статьях, в качестве сигнала входа используются встроенный класс Envelopes и класс RSI.