Реализация шаблона "Декоратор" в MQL5: Добавление логирования, измерения времени выполнения и фильтрации к любому индикатору без изменения исходного кода
Введение
Чтобы добавить измерение времени выполнения в существующий класс индикатора RSI, разработчик обычно размещает вызовы GetMicrosecondCount() до и после вычислений. Когда второму разработчику требуется логирование, он открывает тот же файл и добавляет вызовы Print(). Когда третьему разработчику требуется пороговая фильтрация, он добавляет условную логику возврата. После нескольких итераций класс RSI накапливает код, не связанный с вычислением RSI. Он содержит инфраструктурные сквозные аспекты, относящиеся к вызывающему контексту, а не к индикатору.
Такой подход нарушает принцип открытости/закрытости, согласно которому класс должен быть открыт для расширения, но закрыт для модификации. Каждое изменение исходного кода для добавления сквозного аспекта увеличивает риск регрессий в основной вычислительной части индикатора. Изменение необходимо проверить на всех существующих сценариях использования. Если класс индикатора используется несколькими советниками, изменение одновременно влияет на все эти советники. Если класс находится под контролем версий и другой разработчик внес несвязанные изменения в тот же файл, вероятность конфликтов слияния повышается.
Более глубокая проблема состоит в том, что добавляемые аспекты не являются свойствами индикатора. Измерение времени, логирование и фильтрация относятся к способу использования индикатора в конкретном контексте. Другому советнику может потребоваться измерение времени, но не логирование. Окружению для тестирования на исторических данных может не потребоваться ни то, ни другое. Класс индикатора вообще не должен содержать эти аспекты.

Рисунок 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

Рисунок 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, возвращающий последнее вычисленное значение при повторном вызове на том же баре. Для этого не нужно изменять ни один существующий файл кодовой базы.

Рисунок 3. Вывод журнала терминала, демонстрирующий перехват сквозных аспектов во время выполнения и условное подавление данных при нескольких тиках вычисления.
Заключение
Шаблон проектирования "Декоратор" позволяет подключать сквозные аспекты к любому индикатору при создании без изменения исходного кода индикатора. Каждый декоратор – самостоятельный блок поведения, делегирующий вычисление обернутому объекту и добавляющий ровно один сквозной аспект до или после делегирования. Цепочка собирается в коде инициализации советника — в функции OnInit(), – а внешний указатель представляет собой единственный интерфейс, используемый торговой логикой советника.
Соблюдение принципа открытости/закрытости обеспечивается архитектурой, а не декларациями. Добавление нового сквозного аспекта в систему, построенную на этой архитектуре, сводится к написанию одного нового класса. Ни один существующий класс не нужно изменять, ни один существующий набор тестов не становится недействительным, и ни один советник, использующий неизмененные классы индикаторов, не затрагивается.
Накладные расходы ограничены: один виртуальный вызов и один дополнительный кадр стека на слой, а также одно выделение памяти на декоратор. Для циклов вычисления индикатора, уже включающих вызовы CopyBuffer() и операции с плавающей запятой, эти накладные расходы архитектурно незначительны.
Цепочка владения, в которой каждый декоратор владеет обернутым объектом, а советник – только внешним указателем, обеспечивает детерминированную очистку одним вызовом delete. Она исключает утечки хэндлов и осиротевшие объекты при последовательном соблюдении соглашения о построении цепочки.
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | IIndicator.mqh | Включаемый файл | Абстрактный интерфейс, объявляющий GetValue(int shift) и GetName() как общий контракт для всех индикаторов и декораторов. |
| 2 | RSIIndicator.mqh | Включаемый файл | Конкретный компонент CRSIIndicator, вычисляющий значения RSI через хэндл индикатора терминала, а также CMovingAverageIndicator для вычисления MA. |
| 3 | BaseDecorator.mqh | Включаемый файл | CBaseDecorator – абстрактный базовый класс декоратора, владеющий указателем IIndicator* и обеспечивающий делегирование по умолчанию для GetValue() и GetName(). |
| 4 | LoggingDecorator.mqh | Включаемый файл | CLoggingDecorator выводит имя и значение индикатора в журнал при каждом вызове GetValue(). |
| 5 | TimingDecorator.mqh | Включаемый файл | CTimingDecorator измеряет время выполнения обернутой цепочки в микросекундах и записывает результат в журнал. |
| 6 | ThresholdFilterDecorator.mqh | Включаемый файл | CThresholdFilterDecorator возвращает 0,0 для значений вне настраиваемого диапазона, подавляя шум до того, как значение достигнет вызывающего кода. |
| 7 | CommentPanel.mqh | Включаемый файл | CCommentPanel отображает активную цепочку декораторов, исходные и отфильтрованные значения, а также последние пять значений баров в виде комментария на графике. |
| 8 | DecoratorPatternEA.mq5 | Демонстрационный советник | Демонстрационный советник, создающий и использующий несколько цепочек декораторов, независимо логирующий каждый слой, обновляющий панель комментариев на графике и выполняющий детерминированную очистку в OnDeinit(). |
| 9 | Decorator_Pattern.zip | ZIP-архив | ZIP-архив, содержащий все прикрепленные файлы и пути к ним относительно корневой папки терминала. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22962
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Моделирование рынка: Position View (XX)
От начального до среднего уровня: Перегрузка операторов (II)
Возможности Мастера MQL5, которые вам нужно знать (Часть 96): Использование вейвлетного порогового преобразования и сети LSTM в пользовательском классе управления капиталом
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования