English
preview
Мультитаймфреймовый движок без утечек дескрипторов с чтением закрытых баров в MQL5

Мультитаймфреймовый движок без утечек дескрипторов с чтением закрытых баров в MQL5

MetaTrader 5 — Индикаторы |
63 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Когда вы впервые приступаете к написанию советника, работающего с несколькими таймфреймами, всё кажется достаточно простым. Вы подключаете несколько индикаторов со старших таймфреймов, считываете их буферы — и стратегия работает.

Но спустя некоторое время начинают происходить странные вещи. Советник, который в тестере работал безупречно, после нескольких дней работы начинает потреблять всё больше и больше памяти. Сигналы внезапно «сдвигаются» после закрытия бара. Таймфреймы рассинхронизируются. А иногда терминал начинает работать нестабильно без видимой причины.

Суть в том, что большинство мультитаймфреймовых реализаций в MQL5 содержат две скрытые проблемы:

  • экземпляры индикаторов создаются, но никогда не освобождаются полностью;
  • расчёты опираются на данные текущего незавершённого бара.

При проведении коротких тестов эти проблемы практически незаметны. Однако в системах, работающих в течение длительного времени, мелкие ошибки постепенно превращаются в реальные проблемы со стабильностью. Именно для этого и был создан Unified MTF Engine — легковесная библиотека, которая берёт на себя все повторяющиеся и подверженные ошибкам части логики работы с несколькими таймфреймами.

Движок автоматически:

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

В результате торговый код становится значительно проще, а система в целом — гораздо более стабильной и предсказуемой

    // Quick Start Example
    #include <MTFEngine.mqh>
    int slot_fast_ma;
    
    int OnInit()
      {
       slot_fast_ma = AddMA(_Symbol, PERIOD_H4, 20);
       return(INIT_SUCCEEDED);
      }
    
    void OnTick()
      {
       if(!IsReady()) return; // Wait for HTF data
       double ma_val = ReadBuffer(slot_fast_ma, 0); 
      }


    Введение

    Писать мультитаймфреймовую (MTF) логику на MQL5 в теории довольно просто: создать дескрипторы индикаторов в OnInit(), считывать значения из последнего закрытого бара (shift = 1) и освобождать дескрипторы в OnDeinit(). На практике эти правила существуют лишь «в голове разработчика». Достаточно одного вызова iMA() внутри OnTick() или набранного по привычке CopyBuffer(…,0,…), чтобы появились две незаметные, но опасные ошибки: неосвобожденные экземпляры индикаторов, которые постепенно исчерпывают ресурсы терминала, и «фантомные» сигналы, срабатывающие на значениях еще формирующегося бара и исчезающие после закрытия бара. Симптомы малозаметны: растущее потребление памяти процессом mt5.exe, строки в журнале вроде «IndicatorCreate failed» или стратегия, которая в реальной торговле генерирует больше сигналов, чем в бэктесте на закрытых барах, — поэтому многие авторы упускают причину.

    В этой статье представлено структурное решение: MTFEngine.mqh. Вместо того чтобы полагаться на дисциплину разработчика, движок делает правильное поведение поведением по умолчанию. Все дескрипторы индикаторов создаются только во время инициализации и управляются внутренне; функция ReadBuffer() по умолчанию использует bar_shift = 1 и обеспечивает безопасное чтение массивов для проверки пересечений; функция IsReady() проверяет как содержимое буфера, так и временные метки баров старшего таймфрейма (HTF), чтобы избежать чтения устаревших данных старшего таймфрейма; а единый вызов ReleaseAll() освобождает все дескрипторы. В результате получается небольшой и понятный API, который предотвращает два распространенных сценария сбоя еще до их появления.


    Раздел 1. Как работают дескрипторы индикаторов MetaTrader 5

    Что на самом деле представляет собой дескриптор

    Когда вы вызываете iMA(), iRSI() или любую другую встроенную функцию индикатора в MQL5, такой вызов не приводит к немедленному вычислению каких-либо значений. Вместо этого он отправляет запрос на сервер данных MetaTrader 5 с просьбой создать и поддерживать работающий экземпляр этого индикатора с указанными параметрами. Сервер возвращает целое число — «дескриптор», который является ссылкой на этот работающий экземпляр.

    Система дескрипторов спроектирована так, чтобы быть эффективной. Если дважды вызвать iMA() с абсолютно одинаковыми параметрами для одного и того же символа и таймфрейма, сервер распознает, что оба запроса указывают на одну и ту же конфигурацию индикатора, и оба раза возвращает один и тот же дескриптор. Данные рассчитываются один раз и используются совместно. Именно такое поведение кэширования и делает дескрипторы быстрыми.

    В MQL5 терминал устроен таким образом, что возвращает существующий дескриптор, если параметры, переданные в функцию индикатора, совпадают. Тем не менее, опасность вызова таких функций, как iMA() или iCustom(), внутри функции OnTick() по-прежнему остается значительной. Разработчики часто неосознанно вносят небольшие изменения, например динамическое значение сдвига, меняющуюся строку символа или разные входные параметры пользовательского индикатора; каждое из них мгновенно создает уникальный новый дескриптор. Что ещё важнее, вызов этих функций внутри высокочастотного обработчика OnTick() делает управление жизненным циклом этих ресурсов невозможным. Вместо контролируемой инициализации советник переходит в состояние «ресурсного хаоса», при котором он слепо полагается на внутренний кэш терминала. Если эти дескрипторы не отслеживаются явно и не освобождаются с помощью функции IndicatorRelease(), это может привести к исчерпанию памяти и к тому, что советник фактически перестанет реагировать.

    Утечка ресурсов / утечка дескрипторов (неосвобожденные экземпляры индикаторов) на практике

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

    НЕПРАВИЛЬНО — создает новый дескриптор при каждом тике
    ПРАВИЛЬНО — создает дескриптор один раз в функции OnInit()
    void OnTick()
      {
    //--- BAD: iMA called on every tick.
    //--- Each call with fresh parameters creates a NEW handle
    //--- if the terminal cannot match the cache.
    //--- After thousands of ticks, the terminal runs out of handle slots.
       int handle = iMA(_Symbol,PERIOD_D1,200,0,MODE_EMA,PRICE_CLOSE);
    
       double buffer[1];
       if(CopyBuffer(handle,0,1,1,buffer) > 0)
         {
          //--- Use the data
          double ma_value = buffer[0];
         }
    
    //--- Handle is never released using IndicatorRelease()!
      }
    
    int g_handle_200_d1 = INVALID_HANDLE;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //--- GOOD: create handle once during initialization
       g_handle_200_d1 = iMA(_Symbol,PERIOD_D1,200,0,MODE_EMA,PRICE_CLOSE);
    
       return(INIT_SUCCEEDED);
      }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
       double buffer[1];
    
    //--- Reuse the same handle every tick
       if(CopyBuffer(g_handle_200_d1,0,1,1,buffer) < 0)
         {
          return;
         }
      }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
      {
    //--- Properly release the handle to free memory resources
       if(g_handle_200_d1 != INVALID_HANDLE)
         {
          IndicatorRelease(g_handle_200_d1);
         }
      }

    Таблица 1: неправильный и правильный шаблоны управления дескрипторами индикаторов. Основное отличие заключается в том, что функция iMA() должна вызываться в OnInit(), а не в OnTick(). Дескриптор, созданный в функции OnInit(), хранится в глобальной переменной, повторно используется на каждом тике и освобождается в функции OnDeinit().

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

    Как проверить, нет ли утечки дескрипторов

    В MetaTrader 5 откройте окно «Терминал» (Ctrl+T), перейдите на вкладку «Журнал» и после продолжительной работы следите за строками, содержащими сообщения «IndicatorCreate failed» или «handle allocation failed». В качестве альтернативы можно открыть «Диспетчер задач» и отслеживать объем памяти, занимаемый процессом mt5.exe, в течение нескольких часов. Постоянный рост потребления памяти на фоне снижения количества выдаваемых сигналов — надежный признак того, что дескрипторы не освобождаются.

    IndicatorRelease() и почему о ней забывают

    Функция IndicatorRelease() принимает целое число, представляющее дескриптор, и сообщает терминалу, что вызывающая программа больше не нуждается в данном экземпляре индикатора. Если ни одна другая программа не удерживает ссылку на тот же дескриптор, терминал может освободить ресурсы. На практике большинство разработчиков не забывают создавать дескрипторы в функции OnInit(), но забывают освобождать их в функции OnDeinit().

    Это упущение легко объяснить. Функция OnDeinit() пишется в последнюю очередь, зачастую наспех, и переменные-дескрипторы, объявленные в начале файла, легко упустить из виду. Подключаемый файл MTFEngine, созданный в рамках этой статьи, полностью устраняет проблему за счет внутреннего управления дескрипторами и предоставления единственной функции ReleaseAll(), которую советник вызывает один раз в OnDeinit(). Здесь не о чем забывать.


    Раздел 2: Проблема индекса бара

    Индекс 0 никогда не безопасен для сигнальной логики

    В MetaTrader 5 данные цены и индикаторов доступны как временной ряд, где индекс 0 обозначает текущий формирующийся бар, а большие индексы относятся к более старым историческим данным. При использовании функции CopyBuffer() вы извлекаете данные из этого списка с «обратной индексацией». Индекс 0 — это текущий формирующийся бар. Индекс 1 — это последний бар, который полностью закрылся. Индекс 2 — это бар перед ним, и так далее.

    Для любого сигнала, основанного на закрытом баре, пересечении линий, пробое уровня или шаблоне, правильной ссылкой является индекс 1. Последний закрытый бар имеет итоговые значения OHLC и итоговое значение индикатора, которые не изменятся, сколько бы тиков ни поступило в дальнейшем. Индекс 0 изменяется при каждом тике по мере формирования текущего бара, а это означает, что пересечение, обнаруженное при индексе 0, к моменту поступления следующего тика может уже не существовать.

    Индекс Бар Состояние Использовать для
    0 Текущий формирующийся бар
    Незавершён — цена всё ещё движется внутри него
    НЕ использовать для принятия решений по сигналу
    1 Последний закрытый бар
    Завершён — данные OHLC окончательны и не изменятся
    Правильный индекс для всей сигнальной логики
    2 Бар перед последним закрытым
    Завершён — используется для сравнения при пересечении
    Сравнение с предыдущим баром (например, для пересечения)
    0 (H1 из советника на D1)
    Текущий бар D1
    Незавершён на графике H1 — вводящее в заблуждение значение внутри бара
    Всегда используйте индекс 1 при обращении к старшему таймфрейму

    Таблица 2. Руководство по индексам баров. Индекс 1 — правильный выбор для любого чтения сигналов в советниках, которые работают по завершённым барам. Индекс 0 ни в коем случае не следует использовать для принятия решений по сигналам на торговом таймфрейме, а также для ссылок на старший таймфрейм.

    Проблема индекса бара особенно остро проявляется в мультитаймфреймовых конфигурациях. Предположим, что советник на H1 считывает дневную EMA для определения направления тренда. На дневном таймфрейме при индексе бара 0 это значение представляет сегодняшнюю формирующуюся EMA — в 09:00 она может быть выше цены, а к 16:00 — ниже, в зависимости от хода сессии. Советник, который считывает EMA на D1 по индексу 0 и проверяет, находится ли цена выше или ниже нее, будет давать противоречивые результаты в течение дня. Чтение по индексу 1 дает значение EMA на момент предыдущего дневного закрытия, которое является стабильным и окончательным.

    Проблема на практике

    Ошибка индекса бара не приводит ни к ошибке компиляции, ни к предупреждению во время выполнения. Она приводит к появлению советника, который, на первый взгляд, работает — дескрипторы загружаются, сигналы срабатывают, позиции открываются, — но сигналы, по которым он действует, не всегда подтверждены завершенными ценовыми данными. Приведенный ниже эксперимент позволяет это измерить.

    Оба приведенных ниже советника используют одинаковые параметры индикаторов и одинаковую логику пересечения. Единственное отличие заключается в одной цифре в вызове CopyBuffer(): Index0MTF.mq5 передает 0 в качестве начального индекса бара, считывая данные из текущего формирующегося бара. Index1MTF.mq5 передает 1, считывая данные из последнего полностью закрытого бара. Если запустить оба советника на одном и том же символе, периоде и диапазоне дат, количество сигналов будет различаться.

    Index0MTF.mq5 — считывает данные из индекса бара 0 (формирующийся бар):

    //+------------------------------------------------------------------+
    //|                                                   Index0MTF.mq5  |
    //| Reads indicator values from bar index 0 (current forming bar).   |
    //| Demonstrates unstable signal timing at bar boundaries.           |
    //| Run in Strategy Tester only — for comparison purposes.           |
    //+------------------------------------------------------------------+
    #property strict
    #property description "Index 0 EA — reads from forming bar (incorrect)"
    
    //--- Global variables
    int      g_h_fast     = INVALID_HANDLE;
    int      g_h_slow     = INVALID_HANDLE;
    int      g_bar_count  = 0;
    int      g_sig_count  = 0;
    datetime g_last_bar   = 0;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       g_h_fast = iMA(_Symbol,PERIOD_CURRENT,20,0,MODE_EMA,PRICE_CLOSE);
       g_h_slow = iMA(_Symbol,PERIOD_CURRENT,50,0,MODE_EMA,PRICE_CLOSE);
    
       if(g_h_fast == INVALID_HANDLE || g_h_slow == INVALID_HANDLE)
         {
          Print("Index0MTF: Handle creation failed.");
          return(INIT_FAILED);
         }
    
       return(INIT_SUCCEEDED);
      }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
      {
       IndicatorRelease(g_h_fast);
       IndicatorRelease(g_h_slow);
    
       PrintFormat("Index0MTF: Complete. Bars: %d | Crossover signals: %d",
                   g_bar_count,g_sig_count);
      }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
    //--- New bar gate
       datetime current_bar = iTime(_Symbol,PERIOD_CURRENT,0);
       if(current_bar == g_last_bar)
         {
          return;
         }
       g_last_bar = current_bar;
       g_bar_count++;
    
       double fast_buf[1];
       double slow_buf[1];
    
    //--- INDEX 0: reads from the currently forming bar.
    //--- Value changes on every tick until the bar closes.
       if(CopyBuffer(g_h_fast,0,0,1,fast_buf) < 1)
         {
          return;
         }
       if(CopyBuffer(g_h_slow,0,0,1,slow_buf) < 1)
         {
          return;
         }
    
       static double prev_fast = 0.0;
       static double prev_slow = 0.0;
    
       if(prev_fast != 0.0)
         {
          bool cross_up   = (prev_fast <= prev_slow && fast_buf[0] > slow_buf[0]);
          bool cross_down = (prev_fast >= prev_slow && fast_buf[0] < slow_buf[0]);
    
          if(cross_up || cross_down)
            {
             g_sig_count++;
             PrintFormat("Index0MTF: Signal #%d on bar %d | Fast=%.5f Slow=%.5f",
                         g_sig_count,g_bar_count,fast_buf[0],slow_buf[0]);
            }
         }
    
       prev_fast = fast_buf[0];
       prev_slow = slow_buf[0];
      }
    //+------------------------------------------------------------------+

    Index1MTF.mq5 — считывает данные из индекса бара 1 (последний закрытый бар):

    //+------------------------------------------------------------------+
    //|                                                   Index1MTF.mq5  |
    //| Reads indicator values from bar index 1 (last closed bar).       |
    //| Demonstrates stable, confirmed signal timing.                    |
    //| Run on the same symbol, period and dates as Index0MTF.mq5.       |
    //+------------------------------------------------------------------+
    #property strict
    #property description "Index 1 EA — reads from closed bar (correct)"
    
    //--- Global variables
    int      g_h_fast     = INVALID_HANDLE;
    int      g_h_slow     = INVALID_HANDLE;
    int      g_bar_count  = 0;
    int      g_sig_count  = 0;
    datetime g_last_bar   = 0;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       g_h_fast = iMA(_Symbol,PERIOD_CURRENT,20,0,MODE_EMA,PRICE_CLOSE);
       g_h_slow = iMA(_Symbol,PERIOD_CURRENT,50,0,MODE_EMA,PRICE_CLOSE);
    
       if(g_h_fast == INVALID_HANDLE || g_h_slow == INVALID_HANDLE)
         {
          Print("Index1MTF: Handle creation failed.");
          return(INIT_FAILED);
         }
    
       return(INIT_SUCCEEDED);
      }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
      {
       IndicatorRelease(g_h_fast);
       IndicatorRelease(g_h_slow);
    
       PrintFormat("Index1MTF: Complete. Bars: %d | Crossover signals: %d",
                   g_bar_count,g_sig_count);
      }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
    //--- New bar gate
       datetime current_bar = iTime(_Symbol,PERIOD_CURRENT,0);
       if(current_bar == g_last_bar)
         {
          return;
         }
       g_last_bar = current_bar;
       g_bar_count++;
    
       double fast_buf[1];
       double slow_buf[1];
    
    //--- INDEX 1: reads from the last fully closed bar.
    //--- Value is final and stable for the entire duration of the current bar.
       if(CopyBuffer(g_h_fast,0,1,1,fast_buf) < 1)
         {
          return;
         }
       if(CopyBuffer(g_h_slow,0,1,1,slow_buf) < 1)
         {
          return;
         }
    
       static double prev_fast = 0.0;
       static double prev_slow = 0.0;
    
       if(prev_fast != 0.0)
         {
          bool cross_up   = (prev_fast <= prev_slow && fast_buf[0] > slow_buf[0]);
          bool cross_down = (prev_fast >= prev_slow && fast_buf[0] < slow_buf[0]);
    
          if(cross_up || cross_down)
            {
             g_sig_count++;
             PrintFormat("Index1MTF: Signal #%d on bar %d | Fast=%.5f Slow=%.5f",
                         g_sig_count,g_bar_count,fast_buf[0],slow_buf[0]);
            }
         }
    
       prev_fast = fast_buf[0];
       prev_slow = slow_buf[0];
      }
    //+------------------------------------------------------------------+

    Как провести эксперимент

    Скомпилируйте оба советника и запустите два отдельных бэктеста в Тестере стратегий с одинаковыми настройками, меняя между запусками только советник. Используйте режим «Каждый тик на основе реальных тиков» — это критически важный параметр. В режиме OHLC на M1 обрабатывается один тик на каждый бар, из-за чего значения индекса 0 и индекса 1 на этом единственном тике совпадают. Режим «Каждый тик» воспроизводит полную последовательность тиков внутри каждого бара, и только так становится заметной нестабильность индекса 0 в середине бара.

    Параметр Значение
    Символ EURUSD
    Период H1
    Начало 01.01.2022
    Окончание 01.06.2023
    Режим Каждый тик на основе реальных тиков

    Таблица 3. Настройки Тестера стратегий для проверки проблемы индекса бара. Используйте одинаковые настройки для обоих советников

    Вывод журнала

    Чтобы получить эмпирические данные о влиянии индексации по барам, был проведён сравнительный бэктест за 18-месячный период на таймфрейме H1 пары EURUSD. Логика сигналов реализована в двух вариантах: один выполняет чтение формирующегося бара (индекс 0), другой — чтение последнего закрытого бара (индекс 1). Это позволяет выделить волатильность внутри бара и сделать расхождение измеримым. Приведённые ниже выводы журнала показывают, сколько «фантомных сигналов» возникает, когда стратегия опирается на неподтверждённые, колеблющиеся данные, в отличие от стабильных и повторяемых результатов, генерируемых движком.

    Вывод журнала для обоих советников
    Index0MTF: Завершено. Бары: 8799 | Сигналы пересечения: 223
    Index1MTF: Завершено. Бары: 8799 | Сигналы пересечения: 171

    Таблица 4: вывод журнала для обоих советников

    Вывод журнала Index0MTF

    Рис. 1. Вывод журнала Index0MTF по итогам 18-месячного бэктеста EURUSD H1. Оба советника обработали одни и те же 8 799 баров, но версия с индексом 0 обнаружила 223 сигнала пересечения — на 52 больше подтверждённого количества. Эти избыточные сигналы возникают в тех случаях, когда значения EMA формирующегося бара кратковременно пересекались в процессе построения бара, а затем до его закрытия снова расходились, что приводило к обнаружению пересечений, которые так и не были подтверждены закрытым баром.

    Вывод журнала Index1MTF

    Рис. 2. Вывод журнала Index1MTF, полученный в ходе того же прогона бэктеста. Эти 171 сигнал отражают только подтверждённые пересечения — случаи, когда соотношение EMA на полностью закрытом баре отличалось от соотношения на предыдущем баре. В реальном советнике, где каждый сигнал открывает позицию, 52 «фантомных» сигнала из версии с индексом 0 представляют собой сделки, открытые на основе данных, которые на момент исполнения ещё не были окончательными, причём в журнале нет ни предупреждения, ни сообщения об ошибке, указывающих на наличие проблемы.

    Почему это важно для движка

    В функции ReadBuffer() из MTFEngine.mqh по умолчанию используется bar_shift = 1. Одного этого значения по умолчанию достаточно для исправления. Разработчик, использующий прямые вызовы CopyBuffer(), должен помнить, что нужно указывать 1 в качестве третьего аргумента при каждом вызове, для каждого индикатора и в каждом написанном им советнике: один ноль, введённый по привычке, незаметно приводит к показанному выше расхождению в 52 сигнала, без ошибок компиляции. Сделав индекс 1 значением по умолчанию и требуя явного аргумента для его переопределения, движок делает правильное поведение самым простым вариантом.

    Шаблон обнаружения нового бара

    Стандартное решение — отслеживать временную метку последнего бара, обработанного советником, и запускать сигнальную логику только при открытии нового бара. Это означает, что советник проверяет наличие сигналов ровно один раз на бар — в момент открытия нового бара, — а затем ждёт следующего бара. Во время этой единственной проверки все обращения к индикатору выполняются с индексом 1, который в данный момент соответствует только что закрывшемуся бару.

    //--- Declare outside OnTick() so it persists between ticks
    datetime g_last_bar_time = 0;
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
    //--- iTime() returns the open time of bar at the given index.
    //--- Bar 0 = current bar. A new value here means a new bar opened.
       datetime current_bar_time = iTime(_Symbol,PERIOD_CURRENT,0);
    
    //--- If bar time hasn't changed, we are still on the same bar.
    //--- Skip all signal logic until the next bar opens.
       if(current_bar_time == g_last_bar_time)
         {
          return;
         }
       g_last_bar_time = current_bar_time;
    
    //--- A new bar has opened. The bar that just CLOSED is now at index 1.
    //--- This is where all signal reading should happen.
       double fast_ema[1], slow_ema[1];
    
    //--- Copy values from index 1 (the last closed bar)
       if(CopyBuffer(g_fast_handle,0,1,1,fast_ema) < 1 ||
          CopyBuffer(g_slow_handle,0,1,1,slow_ema) < 1)
         {
          return;
         }
    
    //--- Now use these values for signal decisions.
       if(fast_ema[0] > slow_ema[0])
         {
          Print("Trend is up on the last closed bar.");
         }
      }

    Чтобы выявить сигнал, такой как пересечение, нужна последовательность данных. Функция CopyBuffer() позволяет выбрать, откуда начинать чтение — обычно с последнего закрытого бара — и насколько далеко назад брать данные. Запрос двух баров заполняет небольшой список (массив), который позволяет коду сравнить самую последнюю тенденцию с предыдущей, чтобы найти триггер входа.

    Пограничный случай старшего таймфрейма (HTF)

    При чтении индикаторов на таймфрейме старше таймфрейма графика есть один пограничный случай, который нужно обрабатывать явно, а не считать несущественным. В тот самый момент, когда открывается новый бар H1, терминал вызывает OnTick() для графика, однако индикатор D1 или H4, работающий на собственном старшем таймфрейме, может ещё не успеть пересчитаться для нового периода. Бар с индексом 1 на старшем таймфрейме — это правильный закрытый бар для чтения, но если этот бар только что закрылся, а индикатор ещё не успел его обработать, CopyBuffer() может вернуть значение из предыдущего цикла расчёта, а не данные только что закрытого бара.

    Стандартный подход, заключающийся в простой проверке того, что функция CopyBuffer() возвращает хотя бы один элемент, в данном случае недостаточен. Буфер может содержать устаревшие данные и при этом возвращать количество, равное одному. Правильная проверка — это вызов iTime() для индекса бара 1 на собственном таймфрейме индикатора. Если только что закрывшийся бар H4 доступен, iTime(_Symbol, PERIOD_H4, 1) возвращает дату и время его открытия, что является ненулевым значением. Если терминал ещё не обработал этот бар, функция возвращает ноль. MTFEngine.mqh: функция IsReady() выполняет эту проверку для каждого зарегистрированного дескриптора по отдельности, используя символ и таймфрейм, сохранённые в структуре MTFHandle. Движок не сообщит о готовности, пока для каждого слота не будут выполнены оба условия: CopyBuffer() возвращает данные, а iTime() для индекса 1 на таймфрейме индикатора возвращает действительную временную метку.

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


    Раздел 3: Подключаемый файл MTFEngine

    Цели проектирования

    MTFEngine.mqh разработан с учетом трёх требований. Во-первых, все дескрипторы индикаторов должны создаваться в одной функции инициализации и храниться внутри; советник никогда не вызывает iMA() или любую другую функцию индикатора напрямую. Во-вторых, все запросы на чтение значений должны возвращать данные только из закрытых баров, при этом индекс бара всегда должен явно устанавливаться равным 1. В-третьих, очистка должна происходить автоматически: советник вызывает одну функцию освобождения в OnDeinit(), и каждый дескриптор, созданный движком, освобождается.

    Движок не является полноценным уровнем абстракции. Он не пытается скрыть от разработчика понятия индикаторов или таймфреймов. Вызывающий советник по-прежнему указывает, какой индикатор ему нужен, на каком таймфрейме и с какими параметрами. Движок устраняет шаблонный код: хранение дескрипторов, дисциплину работы с индексом бара и управление освобождением ресурсов. Разработчик получает чистый интерфейс, а движок берёт на себя всю служебную обвязку.

    Поддерживаемые индикаторы

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

    Индикатор Функция MQL5 Полезные буферы Параметры регистрации в движке (AddMA...) Читаемые буферы Типичные ошибки и примечания по буферам
    EMA / SMA / WMA
    iMA()
    Буфер 0
    символ, таймфрейм, период, сдвиг, метод, цена 1 (основной) Буфер 0: единственный буфер. Убедитесь, что сдвиг учтён; положительный сдвиг смещает линию вправо.
    RSI
    iRSI()
    Буфер 0
    символ, таймфрейм, период, цена 1 (основной) Один буфер: распространённая ошибка — пытаться читать сигнальную линию (индекс 1), которой у этих индикаторов нет.
    ATR
    iATR()
    Буфер 0
    символ, таймфрейм, период 1 (основной) Индекс 0: только один буфер. Примечание: у индикатора ATR нет параметра цены; он всегда использует диапазон High/Low.
    Стохастик
    iStochastic()
    Буфер 0 (K), буфер 1 (D)
    символ, таймфрейм, K_period, D_period, замедление, метод, поле 2 (основная, сигнальная) Несколько буферов: индекс 0 — линия %K, индекс 1 — линия %D (сигнальная). Пользователи часто меняют их местами.
    MACD
    iMACD()
    Буфер 0 (основной), буфер 1 (сигнальный)
    symbol, tf, fast_ema, slow_ema, signal_period, price 2 (основная, сигнальная) Особенности MQL5: индекс 0 — линия MACD, индекс 1 — сигнальная линия. Примечание: гистограмма часто вычисляется как Main - Signal.
    Полосы Боллинджера
    iBands()
    0 = средняя, 1 = верхняя, 2 = нижняя
    symbol, tf, period, shift, deviation, price 3 (базовая/средняя, верхняя, нижняя) Тройной буфер: индекс 0 = средняя (базовая) полоса, индекс 1 = верхняя полоса, индекс 2 = нижняя полоса. Использование неверного индекса приводит к некорректной логике пересечения.
    CCI
    iCCI()
    Буфер 0
    символ, таймфрейм, период, цена 1 (основной) Индекс 0: один буфер. Распространенная ошибка: забывать о том, что значения CCI не ограничены (в отличие от RSI) и, как правило, колеблются в диапазоне от -100 до +100.
    Пользовательский индикатор
    iCustom()
    Любой буфер
    symbol, tf, path, label Переменная Динамически: необходимо проверить «Окно данных» в MetaTrader 5, чтобы определить, какому индексу соответствует какая линия для конкретного файла .ex5.

    Таблица 5. Индикаторы, поддерживаемые MTFEngine.mqh. Каждый из них работает по одному и тому же шаблону создания дескриптора и чтения буфера. Пользовательские индикаторы, добавленные через iCustom(), следуют той же схеме: создаются в InitHandles(), считываются через ReadBuffer() и освобождаются в ReleaseAll().

    Форма ReadBuffer для массива

    Предусмотрено два варианта функции ReadBuffer(). Скалярная форма возвращает одно значение типа double и является правильным выбором для индикаторов, которым требуется только значение последнего закрытого бара; в данной стратегии к этой категории относятся как EMA на D1, так и RSI на H4. Форма для массива заполняет массив типа double последовательными значениями за один вызов CopyBuffer() и является правильным выбором для обнаружения пересечений, когда одновременно требуются два последовательных значения закрытых баров.

    //--- Scalar form: returns one value at bar_shift (default = 1)
    double ReadBuffer(int slot,int buffer_num=0,int bar_shift=1);
    
    //--- Array form: fills result[] with 'count' values starting at bar_shift
    //--- result[0] = bar_shift bar, result[1] = bar_shift+1 bar, etc.
    //--- Returns the number of values successfully copied (0 on failure)
    int ReadBuffer(int slot,double &result[],int buffer_num=0,int bar_shift=1,int count=2);

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

    Полный файл MTFEngine.mqh

    //+------------------------------------------------------------------+
    //|                                                    MTFEngine.mqh |
    //|          Multi-timeframe indicator engine for MQL5 EAs.          |
    //+------------------------------------------------------------------+
    #property strict
    
    //--- Maximum indicator slots. Increase if more than 20 are needed.
    #define MTF_MAX_HANDLES 20
    
    //+------------------------------------------------------------------+
    //| MTFHandle struct                                                 |
    //| Stores metadata the engine needs for one handle.                 |
    //+------------------------------------------------------------------+
    struct MTFHandle
      {
       int               handle;       // Indicator handle returned by iXxx()
       ENUM_TIMEFRAMES   timeframe;    // Timeframe the indicator runs on
       string            label;        // Human-readable name for log messages
       string            symbol;       // Symbol the indicator is attached to
       datetime          last_htf_bar; // Open time of last HTF bar read at index 1
      };
    
    //--- Internal state
    MTFHandle g_mtf_handles[MTF_MAX_HANDLES];
    int       g_mtf_count = 0;
    datetime  g_last_bar  = 0;
    
    //+------------------------------------------------------------------+
    //| RegisterHandle                                                   |
    //+------------------------------------------------------------------+
    int RegisterHandle(int handle,ENUM_TIMEFRAMES tf,string label,string symbol)
      {
       if(g_mtf_count >= MTF_MAX_HANDLES)
         {
          PrintFormat("MTFEngine: Slot limit reached (%d).",MTF_MAX_HANDLES);
          return(-1);
         }
       if(handle == INVALID_HANDLE)
         {
          PrintFormat("MTFEngine: Invalid handle for '%s'. Error: %d",label,GetLastError());
          return(-1);
         }
    
       g_mtf_handles[g_mtf_count].handle       = handle;
       g_mtf_handles[g_mtf_count].timeframe    = tf;
       g_mtf_handles[g_mtf_count].label        = label;
       g_mtf_handles[g_mtf_count].symbol       = symbol;
       g_mtf_handles[g_mtf_count].last_htf_bar = 0;
    
       PrintFormat("MTFEngine: Handle allocated for %s",label);
       return(g_mtf_count++);
      }
    
    //+------------------------------------------------------------------+
    //| IsReady                                                          |
    //+------------------------------------------------------------------+
    bool IsReady()
      {
       for(int i = 0; i < g_mtf_count; i++)
         {
          if(g_mtf_handles[i].handle == INVALID_HANDLE)
            {
             return(false);
            }
    
          //--- Check 1: buffer contains data
          double buf[1];
          if(CopyBuffer(g_mtf_handles[i].handle,0,1,1,buf) < 1)
            {
             return(false);
            }
    
          //--- Check 2: HTF bar at index 1 is synchronised
          datetime htf_bar = iTime(g_mtf_handles[i].symbol,g_mtf_handles[i].timeframe,1);
          if(htf_bar == 0)
            {
             return(false);
            }
         }
       return(true);
      }
    
    //+------------------------------------------------------------------+
    //| IsNewBar                                                         |
    //+------------------------------------------------------------------+
    bool IsNewBar()
      {
       datetime current_bar = iTime(_Symbol,PERIOD_CURRENT,0);
       if(current_bar == g_last_bar)
         {
          return(false);
         }
       g_last_bar = current_bar;
       return(true);
      }
    
    //+------------------------------------------------------------------+
    //| ReadBuffer (scalar form)                                         |
    //+------------------------------------------------------------------+
    double ReadBuffer(int slot,int buffer_num=0,int bar_shift=1)
      {
       if(slot < 0 || slot >= g_mtf_count)
         {
          return(EMPTY_VALUE);
         }
    
       double buf[1];
       if(CopyBuffer(g_mtf_handles[slot].handle,buffer_num,bar_shift,1,buf) < 1)
         {
          PrintFormat("MTFEngine: CopyBuffer failed for '%s'. Error: %d",g_mtf_handles[slot].label,GetLastError());
          return(EMPTY_VALUE);
         }
    
       if(bar_shift == 1)
         {
          g_mtf_handles[slot].last_htf_bar = iTime(g_mtf_handles[slot].symbol,g_mtf_handles[slot].timeframe,1);
         }
    
       return(buf[0]);
      }
    
    //+------------------------------------------------------------------+
    //| ReadBuffer (array form)                                          |
    //+------------------------------------------------------------------+
    int ReadBuffer(int slot,double &result[],int buffer_num=0,int bar_shift=1,int count=2)
      {
       if(slot < 0 || slot >= g_mtf_count)
         {
          return(0);
         }
    
       ArrayResize(result,count);
       int copied = CopyBuffer(g_mtf_handles[slot].handle,buffer_num,bar_shift,count,result);
       if(copied < count)
         {
          PrintFormat("MTFEngine: ReadBuffer[%d] failed for '%s'. Error: %d",slot,g_mtf_handles[slot].label,GetLastError());
         }
       return(copied);
      }
    
    //+------------------------------------------------------------------+
    //| ReadPrevBuffer                                                   |
    //+------------------------------------------------------------------+
    double ReadPrevBuffer(int slot,int buffer_num=0)
      {
       return(ReadBuffer(slot,buffer_num,2));
      }
    
    //+------------------------------------------------------------------+
    //| GetHandleCount                                                   |
    //+------------------------------------------------------------------+
    int GetHandleCount()
      {
       return(g_mtf_count);
      }
    
    //+------------------------------------------------------------------+
    //| ReleaseAll                                                       |
    //+------------------------------------------------------------------+
    void ReleaseAll()
      {
       for(int i = 0; i < g_mtf_count; i++)
         {
          if(g_mtf_handles[i].handle != INVALID_HANDLE)
            {
             PrintFormat("MTFEngine: Releasing handle for %s",g_mtf_handles[i].label);
             IndicatorRelease(g_mtf_handles[i].handle);
             g_mtf_handles[i].handle = INVALID_HANDLE;
            }
         }
       g_mtf_count = 0;
       g_last_bar  = 0;
       Print("MTFEngine: All handles released.");
      }
    
    //+------------------------------------------------------------------+
    //| AddMA helper                                                     |
    //+------------------------------------------------------------------+
    int AddMA(string symbol,ENUM_TIMEFRAMES tf,int period,ENUM_MA_METHOD method=MODE_EMA,ENUM_APPLIED_PRICE price=PRICE_CLOSE,int shift=0)
      {
       string label = StringFormat("MA(%d,%s,%s)",period,EnumToString(method),EnumToString(tf));
       int h = iMA(symbol,tf,period,shift,method,price);
       return(RegisterHandle(h,tf,label,symbol));
      }
    
    //+------------------------------------------------------------------+
    //| AddRSI helper                                                    |
    //+------------------------------------------------------------------+
    int AddRSI(string symbol,ENUM_TIMEFRAMES tf,int period,ENUM_APPLIED_PRICE price=PRICE_CLOSE)
      {
       string label = StringFormat("RSI(%d,%s)",period,EnumToString(tf));
       int h = iRSI(symbol,tf,period,price);
       return(RegisterHandle(h,tf,label,symbol));
      }
    
    //+------------------------------------------------------------------+
    //| AddATR helper                                                    |
    //+------------------------------------------------------------------+
    int AddATR(string symbol,ENUM_TIMEFRAMES tf,int period)
      {
       string label = StringFormat("ATR(%d,%s)",period,EnumToString(tf));
       int h = iATR(symbol,tf,period);
       return(RegisterHandle(h,tf,label,symbol));
      }
    
    //+------------------------------------------------------------------+
    //| AddStochastic helper                                             |
    //+------------------------------------------------------------------+
    int AddStochastic(string symbol,ENUM_TIMEFRAMES tf,int k_period=5,int d_period=3,int slowing=3)
      {
       string label = StringFormat("Stoch(%d,%d,%d,%s)",k_period,d_period,slowing,EnumToString(tf));
       int h = iStochastic(symbol,tf,k_period,d_period,slowing,MODE_SMA,STO_LOWHIGH);
       return(RegisterHandle(h,tf,label,symbol));
      }
    
    //+------------------------------------------------------------------+
    //| AddMACD helper                                                   |
    //+------------------------------------------------------------------+
    int AddMACD(string symbol,ENUM_TIMEFRAMES tf,int fast_ema=12,int slow_ema=26,int signal_period=9)
      {
       string label = StringFormat("MACD(%d,%d,%d,%s)",fast_ema,slow_ema,signal_period,EnumToString(tf));
       int h = iMACD(symbol,tf,fast_ema,slow_ema,signal_period,PRICE_CLOSE);
       return(RegisterHandle(h,tf,label,symbol));
      }
    
    //+------------------------------------------------------------------+
    //| AddBands helper                                                  |
    //+------------------------------------------------------------------+
    int AddBands(string symbol,ENUM_TIMEFRAMES tf,int period=20,int band_shift=0,double deviation=2.0)
      {
       string label = StringFormat("BB(%d,%.1f,%s)",period,deviation,EnumToString(tf));
       int h = iBands(symbol,tf,period,band_shift,deviation,PRICE_CLOSE);
       return(RegisterHandle(h,tf,label,symbol));
      }
    
    //+------------------------------------------------------------------+
    //| AddCustom (no inputs)                                            |
    //+------------------------------------------------------------------+
    int AddCustom(string symbol,ENUM_TIMEFRAMES tf,string path,string label)
      {
       int h = iCustom(symbol,tf,path);
       return(RegisterHandle(h,tf,label,symbol));
      }
    
    //+------------------------------------------------------------------+
    //| AddCustom (with input parameters)                                |
    //+------------------------------------------------------------------+
    int AddCustom(string symbol,ENUM_TIMEFRAMES tf,string path,string label,string p1,string p2="",string p3="",string p4="")
      {
       int h;
       if(p4 != "")
          h = iCustom(symbol,tf,path,p1,p2,p3,p4);
       else
          if(p3 != "")
             h = iCustom(symbol,tf,path,p1,p2,p3);
          else
             if(p2 != "")
                h = iCustom(symbol,tf,path,p1,p2);
             else
                h = iCustom(symbol,tf,path,p1);
    
       return(RegisterHandle(h,tf,label,symbol));
      }
    //+------------------------------------------------------------------+


    Раздел 4: Использование движка в реальном советнике

    Стратегия

    Демонстрационный советник торгует EURUSD на таймфрейме H1, используя трёхуровневую структуру подтверждения, широко применяемую при разработке стратегий для розничных трейдеров. EMA(200) на D1 задаёт общее направление тренда: сигналы на покупку принимаются только выше этой линии, а сигналы на продажу — только ниже неё. RSI(14) на таймфрейме H4 служит фильтром импульса: значение RSI выше 55 подтверждает бычий импульс, а ниже 45 — медвежий. Пересечение EMA(20) и EMA(50) на H1 генерирует непосредственный сигнал на вход в рынок. Для открытия сделки должны быть выполнены все три условия.

    Без использования движка для правильной реализации этого советника потребовались бы шесть глобально объявленных переменных для дескрипторов, шесть вызовов создания индикаторов в OnInit(), тщательное управление индексом бара при трёх отдельных чтениях через CopyBuffer() и шесть вызовов IndicatorRelease() в OnDeinit(). При использовании движка это сводится к шести вызовам AddXxx() в одной функции InitHandles(), шести вызовам ReadBuffer() с использованием возвращённых индексов слотов и одному вызову ReleaseAll() в OnDeinit(). Логика остаётся прежней; шаблонный код исчезает.

    Полный демонстрационный советник

    //+------------------------------------------------------------------+
    //|                                                  MTFDemo.mq5     |
    //|          Three-layer MTF EA using MTFEngine.mqh library.         |
    //|    Layers: D1 EMA(200) trend, H4 RSI(14) momentum, H1 cross.     |
    //+------------------------------------------------------------------+
    #property strict
    #property description "MTF Demo: D1 trend + H4 RSI + H1 cross entry"
    
    #include <Trade\Trade.mqh>
    #include "MTFEngine.mqh"
    
    //--- Strategy inputs
    input int      InpD1EmaPeriod    = 200;  // D1 EMA period (trend filter)
    input int      InpH4RsiPeriod    = 14;   // H4 RSI period (momentum filter)
    input double   InpRsiBullLevel   = 55.0; // RSI minimum for bullish confirmation
    input double   InpRsiBearLevel   = 45.0; // RSI maximum for bearish confirmation
    input int      InpH1FastPeriod   = 20;   // H1 fast EMA (entry signal)
    input int      InpH1SlowPeriod   = 50;   // H1 slow EMA (entry signal)
    input double   InpLotSize        = 0.1;  // Position size
    input int      InpStopLossPips   = 30;   // Stop loss in pips
    input int      InpTakeProfitPips = 60;   // Take profit in pips
    
    //--- Engine slot indices returned by AddXxx(), used by ReadBuffer()
    int g_slot_d1_ema   = -1; // D1 EMA(200) — trend layer
    int g_slot_h4_rsi   = -1; // H4 RSI(14)  — momentum layer
    int g_slot_h1_fast  = -1; // H1 EMA(20)  — fast line for crossover
    int g_slot_h1_slow  = -1; // H1 EMA(50)  — slow line for crossover
    
    CTrade g_trade;
    
    //--- Diagnostic flags
    bool g_diagnostic_printed = false;
    int  g_bar_count          = 0; // Bar counter used for leak verification
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //--- Register all indicators with the engine.
       g_slot_d1_ema  = AddMA(_Symbol,PERIOD_D1,InpD1EmaPeriod);
       g_slot_h4_rsi  = AddRSI(_Symbol,PERIOD_H4,InpH4RsiPeriod);
       g_slot_h1_fast = AddMA(_Symbol,PERIOD_H1,InpH1FastPeriod);
       g_slot_h1_slow = AddMA(_Symbol,PERIOD_H1,InpH1SlowPeriod);
    
    //--- Abort if any handle failed to register
       if(g_slot_d1_ema < 0 || g_slot_h4_rsi < 0 || g_slot_h1_fast < 0 || g_slot_h1_slow < 0)
         {
          Print("MTFDemo: Initialization failed. One or more handles are invalid.");
          return(INIT_FAILED);
         }
    
       g_diagnostic_printed = false;
       g_bar_count          = 0;
    
    //--- Start a one-second timer
       EventSetTimer(1);
    
       PrintFormat("MTFDemo: Initialized on %s %s. Handles registered: %d.",
                   _Symbol,EnumToString(_Period),GetHandleCount());
       Print("MTFDemo: Waiting for first bar and indicator warmup.");
    
       return(INIT_SUCCEEDED);
      }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
      {
       EventKillTimer();
    //--- One call releases every handle the engine created
       ReleaseAll();
      }
    
    //+------------------------------------------------------------------+
    //| PrintDiagnostic                                                  |
    //+------------------------------------------------------------------+
    void PrintDiagnostic()
      {
       if(g_diagnostic_printed)
         {
          return;
         }
    
       double d1_ema  = ReadBuffer(g_slot_d1_ema);
       double h4_rsi  = ReadBuffer(g_slot_h4_rsi);
       double h1_fast = ReadBuffer(g_slot_h1_fast);
       double h1_slow = ReadBuffer(g_slot_h1_slow);
    
       if(d1_ema == EMPTY_VALUE || h4_rsi == EMPTY_VALUE || h1_fast == EMPTY_VALUE || h1_slow == EMPTY_VALUE)
         {
          Print("MTFDemo: Diagnostic skipped — one or more buffers not yet ready.");
          return;
         }
    
       Print("══════════════════════════════════════════════════════");
       Print("MTFDemo DIAGNOSTIC — Bar Index 1 (last closed bar)");
       PrintFormat("D1 EMA(%d)  = %.5f  <- verify on a separate D1 chart",InpD1EmaPeriod,d1_ema);
       PrintFormat("H4 RSI(%d)   = %.2f   <- verify on a separate H4 chart",InpH4RsiPeriod,h4_rsi);
       PrintFormat("H1 EMA(%d)  = %.5f  <- verify on a separate H1 chart",InpH1FastPeriod,h1_fast);
       PrintFormat("H1 EMA(%d)  = %.5f  <- verify on a separate H1 chart",InpH1SlowPeriod,h1_slow);
       PrintFormat("Engine handle count: %d (must stay fixed at this value)",GetHandleCount());
       Print("══════════════════════════════════════════════════════");
    
       g_diagnostic_printed = true;
       EventKillTimer();
      }
    
    //+------------------------------------------------------------------+
    //| Timer function                                                   |
    //+------------------------------------------------------------------+
    void OnTimer()
      {
       if(g_diagnostic_printed)
         {
          return;
         }
       if(!IsReady())
         {
          return;
         }
       PrintDiagnostic();
      }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
    //--- Gate 1: Only process once per new H1 bar.
       if(!IsNewBar())
         {
          return;
         }
    
    //--- Gate 2: Wait until all indicators have enough bars loaded.
       if(!IsReady())
         {
          return;
         }
    
    //--- Diagnostic: print once after first valid bar
       if(!g_diagnostic_printed)
         {
          PrintDiagnostic();
         }
    
    //--- Leak verification
       g_bar_count++;
       if(g_bar_count % 100 == 0)
         {
          PrintFormat("MTFDemo: Bar %d | Engine handles: %d | Expected: 4",g_bar_count,GetHandleCount());
         }
    
    //--- Only manage one position for this demo
       if(PositionsTotal() > 0)
         {
          return;
         }
    
    //--- Read values from closed bars
       double d1_ema = ReadBuffer(g_slot_d1_ema);
       double h4_rsi = ReadBuffer(g_slot_h4_rsi);
    
       double fast_vals[];
       double slow_vals[];
    
       int fast_copied = ReadBuffer(g_slot_h1_fast,fast_vals,0,1,2);
       int slow_copied = ReadBuffer(g_slot_h1_slow,slow_vals,0,1,2);
    
    //--- Validate all reads
       if(d1_ema == EMPTY_VALUE || h4_rsi == EMPTY_VALUE || fast_copied < 2 || slow_copied < 2)
         {
          return;
         }
    
       double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
       double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
    
    //--- Layer 1: D1 TREND FILTER
       bool bullish_trend = (bid > d1_ema);
       bool bearish_trend = (bid < d1_ema);
    
    //--- Layer 2: H4 RSI MOMENTUM FILTER
       bool bullish_mom = (h4_rsi >= InpRsiBullLevel);
       bool bearish_mom = (h4_rsi <= InpRsiBearLevel);
    
    //--- Layer 3: H1 EMA CROSSOVER ENTRY SIGNAL
       bool bullish_cross = (fast_vals[1] <= slow_vals[1] && fast_vals[0] > slow_vals[0]);
       bool bearish_cross = (fast_vals[1] >= slow_vals[1] && fast_vals[0] < slow_vals[0]);
    
    //--- ENTRY LOGIC
       double point    = SymbolInfoDouble(_Symbol,SYMBOL_POINT);
       int    digits   = (int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
       double pip_size = (digits == 5 || digits == 3) ? point * 10.0 : point;
    
       if(bullish_trend && bullish_mom && bullish_cross)
         {
          double sl = ask - InpStopLossPips * pip_size;
          double tp = ask + InpTakeProfitPips * pip_size;
          if(g_trade.Buy(InpLotSize,_Symbol,ask,sl,tp,"MTFDemo"))
            {
             PrintFormat("BUY | D1 EMA=%.5f | H4 RSI=%.1f | H1 cross up",d1_ema,h4_rsi);
            }
         }
       else
          if(bearish_trend && bearish_mom && bearish_cross)
            {
             double sl = bid + InpStopLossPips * pip_size;
             double tp = bid - InpTakeProfitPips * pip_size;
             if(g_trade.Sell(InpLotSize,_Symbol,bid,sl,tp,"MTFDemo"))
               {
                PrintFormat("SELL | D1 EMA=%.5f | H4 RSI=%.1f | H1 cross down",d1_ema,h4_rsi);
               }
            }
      }
    //+------------------------------------------------------------------+

    Советник намеренно сделан компактным. Все значимые решения — например, когда читать, что читать и как проверять, — укладываются примерно в 30 строк кода. Движок берёт на себя примерно 60 строк кода управления дескрипторами, которые иначе окружали бы эту основную логику. Сама стратегия остаётся читаемой в отрыве от остального кода.


    Раздел 5: Проверка правильности

    Проверка отсутствия утечек ресурсов/дескрипторов (неосвобождённых экземпляров индикаторов)

    Самый простой способ убедиться в отсутствии утечки ресурсов / утечки дескрипторов (неосвобождённых экземпляров индикаторов) — использовать GetHandleCount() в периодическом операторе вывода во время бэктеста. Функция возвращает внутреннее значение g_mtf_count движка — количество дескрипторов, зарегистрированных при запуске. Это число не должно изменяться в ходе теста. Если это происходит, значит где-то за пределами движка создаётся дескриптор.

    Добавьте этот блок внутрь функции OnTick() во время тестирования. Переменная g_bar_count увеличивается каждый раз, когда функция IsNewBar() возвращает true:

    //--- Leak verification: print handle count every 100 bars
    g_bar_count++;
    if(g_bar_count % 100 == 0)
      {
       PrintFormat("MTFDemo: Bar %d | Engine handles: %d | Expected: 4",
                   g_bar_count, GetHandleCount());
      }

    Запустите бэктест за весь тестовый период и просмотрите вывод журнала. Каждая строка этого вывода должна показывать одно и то же число — в данном случае четыре, что соответствует четырём индикаторам, зарегистрированным в OnInit(). Если в какой-либо строке показано другое число, проверьте советник и все подключаемые файлы на наличие вызовов iMA(), iRSI() или любых других вызовов iXxx(), расположенных вне OnInit(). Именно этот вызов является источником утечки.

    Удалите оператор вывода перед запуском в рабочую эксплуатацию. Это не создаёт измеримых накладных расходов при интервале в 100 баров, но держать диагностический вывод включённым в рабочем советнике — это лишний шум в Журнале.

    Количество дескрипторов в мониторе производительности MetaTrader 5 за 12-месячный прогон MTFDemo.mq5 по EURUSD H1 в Тестере стратегий

    Рис. 3: Количество дескрипторов в мониторе производительности MetaTrader 5 за 12-месячный прогон MTFDemo.mq5 по EURUSD H1 в Тестере стратегий. Количество дескрипторов остаётся ровно равным 4 от первого бара до последнего, что подтверждает: RegisterHandle() и ReleaseAll() правильно управляют жизненным циклом движка, а в ходе теста не создаются новые дескрипторы и не происходит их утечки.

    Проверка точности индекса бара

    Самый быстрый способ убедиться, что движок считывает данные из нужного бара, — напрямую сравнить их с графиком. Прикрепите MTFDemo.mq5 к демо-графику EURUSD H1, затем откройте три отдельных графика — EURUSD D1, EURUSD H4 и второй EURUSD H1 — и добавьте на каждый соответствующие индикаторы: EMA(200) на D1, RSI(14) на H4, а также EMA(20) и EMA(50) на H1. Как только открывается новый бар H1, срабатывает диагностический блок, и в журнале «Эксперты» выводятся все четыре значения.

    Сверьте каждое выведенное значение с соответствующим графиком. Значение EMA D1 в журнале должно совпадать со значением EMA(200), отображаемым в Окне данных при наведении курсора на последний закрытый дневной бар на графике D1. Значение RSI H4 должно совпадать со значением RSI(14) на последнем закрытом баре H4. Значения EMA H1 должны совпадать с соответствующими значениями на последнем закрытом баре H1, считанными с отдельного графика H1, а не с того, к которому прикреплён советник. Если все четыре значения совпадают, значит индекс бара 1 работает во всём движке так, как задумано.

    Примечание: никогда не сравнивайте значение EMA D1 с EMA, построенной непосредственно на графике H1. EMA(200), рассчитанная по 200 дневным барам, и EMA(200), рассчитанная по 200 часовым барам, имеют одинаковый номер периода, но используют совершенно разные данные. Они дадут разные значения, и это корректно. Всегда сравнивайте каждое выведенное значение с графиком соответствующего таймфрейма.

    Если кажется, что значение отстаёт от графика на один бар, проверьте, что именно отображает индикатор на графике. Окно данных на графике в реальном времени обычно отображает текущий формирующийся бар с индексом 0, поскольку курсор мыши находится на текущей цене. Советник считывает данные с индекса 1. Эта разница в один бар намеренная: советник использует стабильный завершённый бар, тогда как на графике отображается текущий, всё ещё движущийся бар.

    Рис. 4: Проверка EMA на D1

    Рис. 4: Проверка EMA на D1. Сопоставление вывода журнала «Эксперты» с Окном данных графика D1, чтобы убедиться, что значение EMA(200) совпадает с последним закрытым дневным баром (бар 1).

    Рис. 5: Синхронизация RSI на H4

    Рис. 5: Синхронизация RSI на H4. Сверка считанного из журнала значения RSI(14) на H4 с последним завершённым баром H4 для подтверждения точности получения данных по нескольким таймфреймам.

    Рис. 6: Внутридневная проверка на H1

    Рис. 6: Внутридневная проверка на H1. Сравнение значений пересечения EMA на H1 из журнала с отдельным графиком H1, чтобы проверить, что индексация по бару 1 последовательно применяется на всех зарегистрированных в движке таймфреймах.

    Проверка на закрытом рынке

    Один из практических моментов заключается в том, что торговля парой EURUSD и большинством других валютных пар не ведётся в выходные дни. Если тики не поступают, функция OnTick() никогда не срабатывает, и любая проверка, зависящая от неё, просто не запустится — даже несмотря на то, что буферы индикаторов полностью загружены, а данные готовы к считыванию. MTFDemo.mq5 справляется с этим без каких-либо специальных настроек со стороны читателя.

    В советник включён односекундный таймер, запускаемый в функции OnInit(). Благодаря этому у него появляется независимый «пульс», который работает независимо от того, открыт рынок или закрыт. Функция OnTimer() опрашивает IsReady() при каждом срабатывании этого таймера, и как только все четыре буфера индикаторов возвращают корректные данные, диагностический блок срабатывает автоматически, выводя значения EMA на D1, RSI на H4 и оба значения EMA на H1 в журнал «Эксперты». Как только это происходит, таймер отключает сам себя с помощью функции EventKillTimer() и больше не играет никакой роли. Затем советник до конца сессии продолжает работать исключительно через OnTick(), без остаточной нагрузки от таймера.

    На практике это означает, что проверка совершенно не зависит от часов работы рынка. Подключите MTFDemo.mq5 к графику в воскресенье вечером, и в течение одной-двух секунд в журнале «Эксперты» появятся все четыре значения, взятые из соответствующих закрытых баров. Откройте графики D1, H4 и H1 по отдельности, наведите курсор на последний закрытый бар на каждом из них и убедитесь, что значения в Окне данных совпадают с выведенными в журнал. Сравнение работает одинаково независимо от того, открылся ли рынок пять минут назад или откроется только через двенадцать часов.

    Запуск бэктеста в Тестере стратегий

    Для полного бэктеста используйте следующие настройки, чтобы получить содержательные результаты на всех трёх слоях таймфреймов. EURUSD на H1, диапазон дат не менее 12 месяцев (желательно два года, чтобы охватить разные рыночные режимы), режим «Каждый тик на основе реальных тиков» для максимально точного моделирования исполнения. Установите для параметра InpD1EmaPeriod значение 200, для InpH4RsiPeriod — 14, а периоды EMA оставьте равными 20 и 50.

    В начале бэктеста будет наблюдаться период прогрева — примерно первые 200 дневных баров, то есть около 10 месяцев, — в течение которого IsReady() возвращает false и сделки не открываются. Это корректное поведение. Для достоверного расчёта индикатору EMA(200) на D1 требуется 200 завершённых дневных баров. Если задать дату начала бэктеста на несколько месяцев раньше интересующего периода, прогрев завершится до начала окна анализа.

    Рис. 7: Результат бэктеста MTFDemo.mq5 на EURUSD H1 (январь 2021 г. — декабрь 2023 г.)

    Рис. 7: Результат бэктеста MTFDemo.mq5 на EURUSD H1 (январь 2021 г. — декабрь 2023 г.). Стратегия не оптимизирована под прибыльность; тест подтверждает, что распределение сигналов соответствует заданным условиям сессии и что движок корректно обрабатывает предварительно загруженные исторические данные для фильтра EMA(200) на D1. Качество истории на уровне 99 % гарантирует точность мультитаймфреймовых расчётов на всех трёх таймфреймах.

    Совет по использованию Тестера стратегий

    При бэктестировании мультитаймфреймового советника на H1 MetaTrader 5 автоматически загружает в Тестере данные D1 и H4 наряду с данными H1. Вам не нужно вручную предварительно загружать данные старшего таймфрейма. Тестер обрабатывает это самостоятельно, если выбран режим «Каждый тик на основе реальных тиков». Если в качестве более быстрой альтернативы используется OHLC на M1, бары H4 и D1 синтезируются из данных M1, что достаточно точно для проверки стратегии, но может давать небольшие расхождения во времени на границах баров по сравнению с полным тиковым тестом.


    Раздел 6: Расширение движка

    Добавление пользовательского индикатора

    Вспомогательная функция AddCustom() в файле MTFEngine.mqh поддерживает любой пользовательский индикатор, скомпилированный в MetaEditor. Предусмотрены две перегруженные формы. Первая принимает только символ, таймфрейм, путь и метку — для индикаторов, использующих значения входных параметров по умолчанию. Вторая принимает до четырёх дополнительных строковых параметров, которые позиционно сопоставляются с объявленными входными переменными индикатора.

    Строка пути следует тем же соглашениям, что и панель «Навигатор» в MetaEditor. Для индикатора, скомпилированного по пути MQL5/Indicators/MyFolder/TrendScore.mq5, путь будет таким: «MyFolder\\TrendScore» — относительно папки MQL5/Indicators/, без расширения .ex5, с удвоенными обратными косыми чертами, как того требует экранирование строк в MQL5.

    Регистрация пользовательского индикатора без входных параметров:

    //--- Indicator with no configurable inputs, or using its defaults
    int g_slot_custom = -1;
    
    int OnInit()
      {
       g_slot_custom = AddCustom(_Symbol, PERIOD_H4,
                                 "MyFolder\\TrendScore",
                                 "TrendScore_H4");
    
       if(g_slot_custom < 0)
          return INIT_FAILED;
       return INIT_SUCCEEDED;
      }

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

    Входные параметры передаются в виде строк после аргумента label. Они позиционно сопоставляются с входными переменными индикатора в том порядке, в котором эти переменные объявлены в исходном коде индикатора. Если индикатор объявляет input int InpPeriod = 14 как первый входной параметр, а input double InpLevel = 1.5 — как второй, вызов выглядит так:

    //--- Indicator with two inputs: int Period and double Level
    //--- Pass them as strings in declaration order
    g_slot_custom = AddCustom(_Symbol, PERIOD_H4,
                              "MyFolder\\MyIndicator",
                              "MyInd_H4",
                              "14",    // maps to input #1 (InpPeriod = 14)
                              "1.5");  // maps to input #2 (InpLevel = 1.5)

    Передавайте только столько параметров, сколько индикатор фактически объявляет. Лишние параметры функция iCustom() игнорирует без вывода сообщений. Если передать слишком мало параметров, непереданные входные параметры получат значения по умолчанию; это правильное поведение, когда нужно переопределить только часть входных параметров.

    Чтение буферов пользовательских индикаторов:

    Нумерация буферов осуществляется в том порядке, в котором в исходном коде индикатора вызывается функция SetIndexBuffer(), начиная с нуля. Если индикатор назначает буфер 0 своей основной выходной линии, а буфер 1 — сигнальной линии, чтение будет выглядеть так:

    void OnTick()
      {
       if(!IsNewBar()) return;
       if(!IsReady())  return;
    
       //--- Buffer 0: main output line of the custom indicator
       double custom_main   = ReadBuffer(g_slot_custom, 0);
    
       //--- Buffer 1: signal line (if the indicator has one)
       double custom_signal = ReadBuffer(g_slot_custom, 1);
      }

    Если назначение буферов в исходном коде индикатора не сразу понятно из кода, добавьте индикатор на график и наведите курсор мыши на одну из отрисованных им линий. В Окне данных индекс буфера отображается рядом со значением — «Value[0]» для буфера 0, «Value[1]» для буфера 1 и так далее. Именно это число необходимо передать как параметр buffer_num функции ReadBuffer().

    Чтение буферов пользовательских индикаторов осуществляется точно так же, как и чтение встроенных. Движок не делает различия между ними. После регистрации через AddCustom() индекс слота ведёт себя так же, как индекс слота, возвращаемый AddMA() или AddRSI(), а ReadBuffer(), форма для массива ReadBuffer() и IsReady() применяются без каких-либо изменений.

    Добавление дополнительных слоёв таймфреймов

    Константа MTF_MAX_HANDLES движка определяет, сколько индикаторов можно зарегистрировать. Её значение по умолчанию — 20 — с запасом покрывает большинство стратегий; пять индикаторов на четырёх таймфреймах займут все 20 слотов. Если более сложной стратегии требуется больше слотов, увеличьте значение константы перед компиляцией. Других изменений не требуется.

    Добавление слоя с недельным таймфреймом осуществляется точно по той же схеме, что и добавление любого другого слоя. Вызовите AddMA() или AddRSI(), указав PERIOD_W1 в качестве таймфрейма, сохраните возвращаемый индекс слота и считывайте данные из него с помощью ReadBuffer() в OnTick(). Движок не делает различия между недельными, дневными и внутридневными таймфреймами. Единственное практическое соображение заключается в том, что недельным индикаторам требуется соответственно больше исторических баров для периода прогрева, поэтому при использовании PERIOD_W1 следует заложить дополнительный предварительный период в бэктесте.

    Несколько символов

    Перед тем как прикрепить к графику советник, использующий несколько символов, проверьте одно: каждый символ, указанный в вызове AddXxx(), должен быть виден в окне «Обзор рынка» до выполнения OnInit(). Если символ отсутствует, функция iXxx() внутри вспомогательной функции завершится с ошибкой, а RegisterHandle() запишет в журнал ошибку недопустимого дескриптора для этого слота. В этом случае движок вернет -1 для данного индекса слота, и проверка в OnInit() распознает это как сбой.

    Надежный способ гарантировать доступность — вызвать SymbolSelect() в самом начале OnInit(), до любых вызовов AddXxx(). Передайте значение true в качестве второго аргумента, чтобы добавить символ в «Обзор рынка», если его там ещё нет:

    int OnInit()
      {
       //--- Ensure both symbols are in Market Watch before creating handles
       if(!SymbolSelect("AUDJPY", true))
         {
          PrintFormat("MTFDemo: SymbolSelect failed for AUDJPY. Error: %d",
                      GetLastError());
          return INIT_FAILED;
         }
       if(!SymbolSelect("USDJPY", true))
         {
          PrintFormat("MTFDemo: SymbolSelect failed for USDJPY. Error: %d",
                      GetLastError());
          return INIT_FAILED;
         }
    
       //--- Now safe to register indicators on both symbols
       g_slot_audjpy_d1 = AddMA("AUDJPY", PERIOD_D1, 50);
       g_slot_usdjpy_d1 = AddMA("USDJPY", PERIOD_D1, 50);
    
       if(g_slot_audjpy_d1 < 0 || g_slot_usdjpy_d1 < 0)
          return INIT_FAILED;
       return INIT_SUCCEEDED;
      }

    SymbolSelect() возвращает false, если символ именно с таким именем отсутствует на сервере брокера. Некоторые брокеры добавляют к стандартным названиям символов такие суффиксы, как .m, # или .pro. Если SymbolSelect() завершилась с ошибкой, откройте «Обзор рынка», прокрутите полный список символов, найдите правильное название этого инструмента у вашего брокера и обновите строку в вызове AddXxx(), чтобы она ему соответствовала.


    Заключение

    Две ошибки, о которых здесь идёт речь, — утечки дескрипторов при создании индикаторов во время выполнения и использование индекса 0 в логике сигналов — не являются редкими пограничными случаями. Они вновь и вновь встречаются в примерах на форумах, в конструкторах советников и в рабочем коде, поскольку опираются на необходимость помнить о дисциплине, а не на принудительно заданную структуру. MTFEngine.mqh устраняет эти сценарии сбоев на уровне API: дескрипторы регистрируются только один раз (в OnInit()), чтение по умолчанию выполняется с последнего закрытого бара (bar_shift = 1), готовность старшего таймфрейма проверяется для каждого дескриптора, а все дескрипторы освобождаются одним вызовом ReleaseAll() в OnDeinit().

    На практике это означает, что вы можете преобразовать советник, которому ранее требовались многочисленные глобальные дескрипторы и шаблонный код, в компактную реализацию, где индикаторы регистрируются через AddXxx(), значения считываются через ReadBuffer(), а перед принятием решений выполняются проверки IsReady()/IsNewBar(). Критерии успешности просты и измеримы: значение GetHandleCount() остается постоянным во время длительных прогонов и бэктестов, в журнале не появляются строки «IndicatorCreate failed», использование памяти не растет устойчиво вверх, а число сигналов больше не расходится между прогонами с индексом 0 и индексом 1. Поместите подключаемый файл в папку /Include/, замените разрозненные вызовы iXxx()/CopyBuffer() вспомогательными функциями движка, запустите быстрый бэктест в режиме «Каждый тик» и просмотрите диагностические сообщения движка — и вы получите практическую, воспроизводимую уверенность в том, что мультитаймфреймовые данные считываются безопасно, а ресурсы управляются корректно.

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

    # Имя Тип Описание
    1 MTFEngine.mqh
    Подключаемый файл Содержит RegisterHandle(), IsReady(), IsNewBar(), ReadBuffer(), ReadPrevBuffer(), ReleaseAll(), а также вспомогательные функции AddMA(), AddRSI(), AddATR(), AddStochastic(), AddMACD(), AddBands() и AddCustom().
    2 MTFDemo.mq5
    Демонстрационный советник Реализует трёхуровневую мультитаймфреймовую стратегию с трендовым фильтром EMA(200) на D1, импульсным фильтром RSI(14) на H4 и входом по пересечению EMA(20)/EMA(50) на H1. Полностью работоспособен для бэктестирования и работы на демо-счёте в реальном времени.
    3 Index0MTF.mq5 Демонстрационный советник Демонстрирует нестабильные сигналы, вызванные чтением значений индикатора с текущего формирующегося бара (индекс 0).
    4 Index1MTF.mq5 Демонстрационный советник Демонстрирует стабильные, подтверждённые сигналы при чтении значений индикатора с последнего закрытого бара (индекс 1).

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

    Прикрепленные файлы |
    MTFEngine.mqh (10.2 KB)
    MTFDemo.mq5 (8.06 KB)
    Index0MTF.mq5 (3.06 KB)
    Index1MTF.mq5 (3.08 KB)
    Нейронная сеть на практике: Рождение C_Neuron Нейронная сеть на практике: Рождение C_Neuron
    В статье показано, как инкапсулировать нейрон в MQL5 с помощью класса C_Neuron, с весами, смещением и числом входов, задаваемым параметром. Мы подробно разберём вычисление функции потерь на основе метода наименьших квадратов и организацию данных обучения в массивах. Так можно легко менять входы и повторять эксперименты, не изменяя реализацию.
    От начального к среднему уровню: Подокна (II) От начального к среднему уровню: Подокна (II)
    В статье подробно рассматривается использование подокон в MetaTrader 5 и показывается, как направление вычислений в OnCalculate влияет на буферы и построение средних. На практике объясняется, как связываются индикаторы с помощью FIRST INDICATOR’S DATA и PREVIOUS INDICATOR’S DATA, каков эффект цепочного удаления зависимых индикаторов и как ведёт себя indicatorseparatewindow. Читатель научится диагностировать проблемы с отображением и создавать индикаторы, которые будут корректно работать в подокнах.
    Особенности написания экспертов Особенности написания экспертов
    Написание и тестирование экспертов в торговой системе MetaTrader 4.
    MQL5 Bootstrap (IV): Вспомогательные средства для трейлинг-стопов и переноса стоп-лосса в безубыток MQL5 Bootstrap (IV): Вспомогательные средства для трейлинг-стопов и переноса стоп-лосса в безубыток
    В этой статье представлены переиспользуемые утилиты MQL5 для управления трейлинг-стопами и переносом стоп-лосса в безубыток. В ней рассматриваются трейлинг-стоп с фиксированным расстоянием в пунктах, трейлинг на основе скользящей средней, ATR и Parabolic SAR, трейлинг в денежном выражении и периодический трейлинг, а также перенос стоп-лосса в безубыток по пунктам и в денежном выражении. Предусмотрены пороги активации, логика пошагового переноса, защита от переноса стопа в обратном направлении и проверка ограничений брокера. Примеры кода и классы Bootstrap показывают, как интегрировать эти вспомогательные средства в советники, чтобы стандартизировать управление позициями и сократить дублирование кода.