English
preview
Использование Экономического календаря MQL5 для фильтрации новостей (Часть 4): Точное тестирование на истории со статическими данными

Использование Экономического календаря MQL5 для фильтрации новостей (Часть 4): Точное тестирование на истории со статическими данными

MetaTrader 5 — Тестер |
113 0
Solomon Anietie Sunday
Solomon Anietie Sunday
Содержание


Введение

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

В реальной торговле новостной фильтр советника работает: CalendarValueHistory() предоставляет информацию о предстоящих событиях, а советник может блокировать открытие новых сделок, приостанавливать изменение уровней SL/TP или закрывать позиции перед публикациями новостей. В Тестере стратегий эта же логика обычно становится "слепой": запросы к календарю терминала не возвращают исторические события либо возвращают неполные данные. Поэтому тестирование на исторических данных может показывать нереалистично сглаженные кривые эквити или необъяснимые "хвосты" просадки. Что особенно важно, это мешает проверить нужное поведение: обнаруживает ли советник новостные окна, блокирует ли открытие сделок, удаляет ли временно стоп-уровни с последующим восстановлением, а также закрывает ли позиции перед публикациями событий высокой важности?

Эта часть посвящена именно устранению этого пробела. Цель — сделать новостные события в Тестере стратегий детерминированными, чтобы можно было воспроизводить и проверять действия советника, связанные с новостями. Решение — статический предварительно загруженный CSV-файл с историческими событиями, который советник считывает в режиме тестера, в сочетании с кэшем валют и автоматическим переключателем MQL_TESTER. При таком подходе можно: (1) воспроизводить одну и ту же последовательность новостей в разных прогонах; (2) отслеживать явные маркеры в логе при обнаружении событий; (3) убедиться, что советник выполняет ожидаемые операции со стопами, открытием и закрытием позиций.

Проблема — исторические события экономического календаря недоступны

Когда советник установлен на график в режиме реальной торговли, CalendarValueHistory() возвращает предстоящие экономические события из внутренней базы данных календаря терминала. Это работает, поскольку база данных содержит будущие события. Однако в Тестере стратегий TimeCurrent() проходит по историческим датам. Если запрашиваются события календаря за уже прошедшую дату, например 15.01.2025, нет гарантии, что эти события все еще присутствуют в кэше терминала. На практике для большинства брокеров и более старых дат CalendarValueHistory() возвращает либо ноль записей, либо, в лучшем случае, лишь часть данных.

Почему это нарушает тестирование на исторических данных

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

При типичном тестировании на исторических данных:

  • Советник никогда не обнаруживает новостные окна, поскольку события не возвращаются.
  • Логика блокировки новых сделок никогда не срабатывает.
  • Процедуры временного удаления уровней стоп-лосса и тейк-профита остаются неактивными.
  • Логика закрытия позиций перед событиями высокой важности никогда не выполняется.

Это приводит к вводящим в заблуждение результатам: всплески волатильности игнорируются, а проверить поведение невозможно, поскольку ни логи, ни действия не подтверждают работу новостного фильтра.

В результате невозможно ответить на следующие вопросы:

  • Правильно ли советник определил новостное окно?
  • Было ли открытие новых сделок заблокировано в нужное время?
  • Были ли стоп-уровни удалены, а затем восстановлены?
  • Приводило ли перекрытие событий к образованию непрерывных периодов ограничений?

Без наблюдаемых подтверждений такого поведения результаты тестирования оказываются непроверяемыми и неполными.

    Почему "скользящее окно в 24 часа" не помогает

    Поскольку календарь перезагружается каждые несколько часов, может показаться, что по мере продвижения времени моделирования Тестер стратегий в конечном итоге подхватит события. Фундаментальная проблема остается: когда from равно текущему времени моделирования, CalendarValueHistory(from, to) возвращает только события, у которых event_time >= from. Функция не возвращает события, произошедшие ранее в ходе моделирования, если в тот момент они не были загружены, поскольку терминал обычно их не хранит. Другими словами, если во внутренней базе данных терминала отсутствуют исторические записи за 15 марта 2024 года, никакие перезагрузки не создадут их чудесным образом.

    Критерии успешности — что должно показывать корректное тестирование на исторических данных

    Прежде чем внедрять решение, необходимо определить, как должно выглядеть корректное тестирование на исторических данных при правильной работе новостного фильтра.

    Корректный и проверяемый результат должен включать следующие наблюдаемые проявления:

    1. Детерминированное обнаружение событий

    • Новостные события стабильно обнаруживаются с одинаковыми временными метками в нескольких прогонах.
    • В логе в ожидаемые моменты времени присутствуют четкие маркеры, например STATIC NEWS: или NEWS:.

    2. Корректные ограничения сделок, если включены

    • Во время заданных новостных окон блокируется открытие новых сделок. То есть в интервале от NewsMinutesBefore до NewsMinutesAfter новые сделки не открываются.

    3. Управление стоп-уровнями, если включено

    • При входе в новостное окно уровни стоп-лосса и тейк-профита временно удаляются.
    • После завершения новостного окна стоп-уровни корректно восстанавливаются.

    4. Управление позициями, если включено

    • Если включена соответствующая настройка, открытые позиции закрываются перед событиями высокой важности.

    5. Непрерывная обработка перекрывающихся событий

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

    6. Воспроизводимость

    • Запуск одного и того же теста с одними и теми же данными дает идентичные результаты.
    • Изменение CSV-файла приводит к предсказуемым изменениям поведения.

    Если эти условия не выполняются, новостной фильтр нельзя считать проверенным независимо от кривой эквити.


    Как решается проблема

    Надежное решение — статический файл новостей, предварительно загружаемый в память

    Чтобы тестирование на исторических данных было детерминированным и точным, необходимо отделить источник новостей от API календаря терминала в реальном времени. Подход прост, но эффективен:

    1. Создайте статический CSV-файл с предварительно отобранным списком исторических экономических событий: датой, временем, валютой, уровнем важности и названием события.
    2. Загрузите этот файл один раз при инициализации советника в Тестере стратегий, что определяется с помощью MQLInfoInteger(MQL_TESTER).
    3. Замените запрос к календарю в реальном времени быстрым поиском в памяти по статическим данным.
    4. Оставьте логику реальной торговли без изменений, поскольку одна и та же функция IsNewsTime() работает с любым источником.

    В результате получаем:

    • полный контроль над тем, какие события принимаются во внимание;
    • детерминированные результаты, поскольку при каждом запуске тестирования на исторических данных учитываются одни и те же новостные события;
    • отсутствие зависимости от полноты календаря брокера;
    • возможность тестировать сценарии "что если" путем редактирования CSV-файла.

    Что рассматривается в этой статье

    Эта часть посвящена добавлению загрузки новостей из статического файла для Тестера стратегий. Существующая логика обнаружения новостей для реальной торговли не изменяется. Добавляются следующие элементы:

    • новый входной параметр для указания пути к CSV-файлу или расположения по умолчанию;
    • функция разбора CSV, считывающая исторические события в сохраняемый в памяти массив;
    • изменение IsNewsTime(), позволяющее использовать статический массив при работе в режиме тестера;
    • дополнительно — отдельный инструмент для экспорта текущего экономического календаря терминала в CSV-файл для будущего тестирования на исторических данных.

    Все остальные функции, такие как временное удаление стоп-уровней, постоянное хранение и блокировка открытия новых сделок, остаются без изменений.

    Чему вы научитесь

    К концу этой части вы узнаете:

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

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

    Обзор реализации

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

    Основные компоненты, обязательные:

    • статический CSV-файл с историческими новостными событиями;
    • загрузчик, разбирающий эти события и сохраняющий их в памяти;
    • автоматическое переключение между режимами реальной торговли и тестера с помощью MQL_TESTER.

    Вспомогательные компоненты, необязательные, но рекомендуемые:

    • кэширование валют для оптимизации производительности;
    • вспомогательный скрипт для экспорта данных календаря в CSV-файл;
    • временный генератор сделок для проверки в ходе тестирования.

    Для обеспечения детерминированного тестирования на исторических данных требуются только основные компоненты. Вспомогательные компоненты повышают удобство использования, производительность и наглядность проверки.

    Проектирование формата статического файла новостей и архитектуры загрузки

    Прежде чем писать код, необходимо определить, как будут храниться исторические данные новостей и как советник будет получать к ним доступ. Тщательно продуманная архитектура позволяет впоследствии сэкономить время на отладке.

    Почему CSV?

    Формат "значения, разделенные запятыми" (CSV) — очевидный выбор:

    • удобен для чтения человеком — его можно открыть в любом текстовом редакторе или программе для работы с таблицами;
    • его легко редактировать — можно добавлять, удалять или изменять события без перекомпиляции советника;
    • внешние библиотеки не нужны — FileOpen() и FileReadString() MQL5 поддерживают его нативно;
    • файл легко переносить, поскольку он находится в папке \Files каталога данных терминала.

    Обязательные поля данных

    Из функции календаря в реальном времени isUpcomingNews() видно, что для каждого события советнику требуется всего несколько сведений:

    Поле Тип Описание Пример
    event_time datetime Запланированное время публикации новости (UTC) 2024.03.15 14:30:00
    currency string Затронутая валюта, например "USD" или "EUR" "USD"
    importance int 1 = низкая, 2 = средняя, 3 = высокая 3
    event_name string Необязательно; используется для отладки и логирования. "FOMC Statement"

    Идентификатор события, идентификатор страны и другие метаданные не нужны: они требовались только для API календаря в реальном времени.

    Предлагаемый формат CSV

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

    event_time,currency,importance,event_name
    2024.01.15 14:30:00,USD,3,FOMC Statement
    2024.01.15 15:00:00,USD,2,Industrial Production
    2024.01.16 09:00:00,EUR,2,German ZEW

    Правила:

    • даты указываются в формате YYYY.MM.DD HH:MM:SS — это соответствует формату, ожидаемому StringToTime();
    • валюта указывается трехбуквенным кодом;
    • важность: 3 — высокая, 2 — средняя, 1 — низкая, фильтрация настраивается позже;
    • название события необязательно, но рекомендуется — оно отображается в логах.

    Где хранить файл

    Система песочницы терминала требует поместить CSV-файл в папку MQL5\Files каталога данных терминала или одну из ее подпапок. Используется имя по умолчанию, например HistoricalNews.csv, но пользователь также может указать собственный путь с помощью входного параметра. Стратегия загрузки в советнике: поскольку файл статический, он загружается один раз в OnInit(), но только при запуске советника в Тестере стратегий. В реальной торговле по-прежнему используется API календаря в реальном времени.

    Вводится новый глобальный массив:

    // Structure to store historical news information
    struct StaticNewsEvent
      {
       datetime          eventTime;
       string            currency;
       int               importance;
       string            name;
      };
    StaticNewsEvent historicalNews[];

    Затем специальная функция LoadHistoricalNewsFromCSV() выполняет следующее:

    1. проверяет, выполняется ли код в режиме тестера, вызывая MQLInfoInteger(MQL_TESTER);
    2. открывает CSV-файл;
    3. считывает файл построчно, пропускает заголовок и разбирает каждое поле;
    4. заполняет массив historicalNews[];
    5. закрывает файл и выводит количество загруженных записей.

    Соображения по производительности

    Даже CSV-файл с несколькими тысячами событий невелик, а разбор занимает миллисекунды. После загрузки IsNewsTime() в режиме тестера просто перебирает массив historicalNews[] и проверяет временное окно. Сложность составляет O(n) на вызов, где n — общее количество событий, например 2000. IsNewsTime() вызывается один раз на каждом тике.


    Реализация загрузчика статических новостей

    Разбор CSV и определение режима тестера

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

    Шаг 1 — добавление входного параметра для CSV-файла

    Добавляется новый строковый входной параметр, позволяющий указать имя файла или полный путь относительно папки \Files. Разместите это в существующей группе входных параметров конфигурации новостного фильтра.

    input group  "New Filter Configuration"
    input bool   EnableNewsFilter = false;                           // Enable Economic News Filter
    input int    NewsMinutesBefore = 5;                              // Minutes before news to restrict
    input int    NewsMinutesAfter = 5;                               // Minutes after news to restrict
    input bool   RestrictNewTradesDuringNews = true;                 // Block new trades during news window
    input bool   SuspendStopsDuringNews = false;                     // Pause SL/TP modifications during the news window [NEW]
    input bool   CloseOpenTradesBeforeHighImpactNews = false;        // Close all trades before news
    input string SymbolCurrencyOverride = "";                        // Manual currency override e.g. "USD,JPY"
    enum ENUM_NEWS_IMPORTANCE_MODE
      {
       NEWS_HIGH_ONLY = 0,
       NEWS_MODERATE_ONLY,
       NEWS_HIGH_AND_MODERATE
      };
    input ENUM_NEWS_IMPORTANCE_MODE NewsImportanceMode = NEWS_HIGH_ONLY; // Which importance levels to consider
    // Cache reload interval
    input int CacheReloadHours = 6;                                  // How often to reload calendar cache (hours)
    
    // ADD NEW INPUT
    input string HistoricalNewsFile = "HistoricalNews.csv";  // CSV file for backtesting news events

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

    Шаг 2 — глобальное хранилище статических событий

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

    //+------------------------------------------------------------------+
    //| GLOBAL VARIABLES                                                 |
    //+------------------------------------------------------------------+
    MqlCalendarValue TodayEvents[];    // Calendar cache
    datetime lastCalendarLoad = 0;
    CTrade trade;                      // CTrade instance for order management
    
    // Structure to store removed stop information
    struct SavedStops
      {
       ulong             ticket;       // Trade ticket number
       double            sl;           // Original stop loss
       double            tp;           // Original take profit
      };
    SavedStops savedStops[];
    bool newsSuspended = false;
    
    // NEW GLOBAL VARIABLES AND STRUCTURE
    // Structure to store historical news information
    struct StaticNewsEvent
      {
       datetime          eventTime;
       string            currency;
       int               importance;
       string            name;
      };
    StaticNewsEvent historicalNews[];
    bool staticNewsLoaded = false;    // Flag to indicate that we've loaded the CSV
    
    string cachedCurrencies[];
    int cachedCurrencyCount = 0;
    bool currencyCacheValid = false;

    Шаг 3 — функция загрузки CSV-файла

    Создается отдельная функция, которая:

    • открывает файл из папки \Files (а точнее Common\Files) для совместимости с Тестером стратегий;
    • считывает файл построчно;
    • пропускает пустые строки и заголовок;
    • разбирает каждую строку с помощью StringSplit();
    • преобразует строку даты в datetime с помощью StringToTime();
    • сохраняет корректные события в массиве historicalNews[].

    Ниже приведена полная реализация функции, добавляемой после других функций советника с новостным фильтром:

    //+------------------------------------------------------------------+
    //| Load historical news from CSV file (for backtesting only)      |
    //+------------------------------------------------------------------+
    
    bool LoadHistoricalNewsFromCSV()
      {
    // Clear any previous data
       ArrayFree(historicalNews);
       staticNewsLoaded = false;
    
    // Only load in the tester
       if(!MQLInfoInteger(MQL_TESTER))
         {
          // Skip this function when not in strategy tester
          return false;
         }
    
    // If no filename provided, skip
       if(StringLen(HistoricalNewsFile) == 0)
         {
          Print("Static news loader: HistoricalNewsFile is empty – no news in backtest.");
          return false;
         }
    
    // Open the file (must be in \Common\Files\ folder for Strategy tester compatibility)
       int fileHandle = FileOpen(HistoricalNewsFile, FILE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON, ",");
       if(fileHandle == INVALID_HANDLE)
         {
          PrintFormat("Static news loader: Failed to open '%s'. Error: %d",
                      HistoricalNewsFile, GetLastError());
          return false;
         }
    
       int lineCount = 0;
       int eventCount = 0;
       string line;
    
       while(!FileIsEnding(fileHandle))
         {
          line = FileReadString(fileHandle);
          lineCount++;
    
          // Trim whitespace and skip empty lines
          StringTrimLeft(line);
          StringTrimRight(line);
          if(StringLen(line) == 0)
             continue;
    
          // Skip header line (starts with "event_time" or "date")
          string lowerLine = line;
          StringToLower(lowerLine);
          if(StringFind(lowerLine, "event_time") != -1 ||
             StringFind(lowerLine, "date") != -1 ||
             StringFind(lowerLine, "time") != -1)
            {
             // Skip header line
             continue;
            }
    
          // Split the line by comma
          string parts[];
          int partCount = StringSplit(line, ',', parts);
          if(partCount < 3)
            {
             continue;
            }
    
          // Trim each part
          for(int i = 0; i < partCount; i++)
            {
             StringTrimLeft(parts[i]);
             StringTrimRight(parts[i]);
            }
    
          // Parse fields
          datetime evTime = StringToTime(parts[0]);
          if(evTime == 0)
            {
             continue;
            }
          string currency = parts[1];
          StringToUpper(currency);
    
          int importance = (int)StringToInteger(parts[2]);
          if(importance < 1 || importance > 3)
            {
             // Importance level must be 1, or 2, or 3
             continue;
            }
    
          string name = (partCount >= 4) ? parts[3] : "";
    
          // Store the event
          int idx = ArraySize(historicalNews);
          ArrayResize(historicalNews, idx + 1);
          historicalNews[idx].eventTime = evTime;
          historicalNews[idx].currency = currency;
          historicalNews[idx].importance = importance;
          historicalNews[idx].name = name;
    
          eventCount++;
         }
    
       FileClose(fileHandle);
    
       staticNewsLoaded = (eventCount > 0);
       return staticNewsLoaded;
      }

    Объяснение основных частей:

    • MQLInfoInteger(MQL_TESTER) определяет режим тестирования на исторических данных;
    • FileOpen() с текстовым режимом FILE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON и запятой в качестве разделителя;
    • пропускаются строки, похожие на заголовки, то есть содержащие date или time без учета регистра;
    • StringToTime() ожидает формат YYYY.MM.DD HH:MM:SS;
    • значения важности сохраняются как целые числа — 3 = высокая, 2 = средняя, 1 = низкая — для удобства последующей фильтрации.

    Шаг 4 — создание кэша валют

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

    //+------------------------------------------------------------------+
    //| Build cached currency list for current symbol                    |
    //+------------------------------------------------------------------+
    void BuildCurrencyCache()
      {
       string relevant = GetRelevantCurrencies(_Symbol);
       if(StringLen(relevant) == 0)
         {
          currencyCacheValid = false;
          Print("Currency cache: No relevant currencies found for ", _Symbol);
          return;
         }
    
       cachedCurrencyCount = StringSplit(relevant, ',', cachedCurrencies);
       for(int i = 0; i < cachedCurrencyCount; i++)
         {
          StringTrimLeft(cachedCurrencies[i]);
          StringTrimRight(cachedCurrencies[i]);
          StringToUpper(cachedCurrencies[i]);
         }
       currencyCacheValid = true;
    
       Print("Currency cache built for ", _Symbol, ": ", relevant);
      }

    Шаг 5 — инициализация загрузчика статических новостей в OnInit()

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

    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       Print("News Filter Part initialized");
    
       if(EnableNewsFilter)
         {
          // Load static news for backtesting (does nothing if it's not running in Strategy Tester)
          LoadHistoricalNewsFromCSV();
    
          // Build currency cache for current symbol (used in both live and backtest)
          BuildCurrencyCache();
         }
    
    // Load persistent stop state after restart
       LoadStopsFromGlobals();
       if(ArraySize(savedStops) > 0)
         {
          Print("Recovered suspended trades from previous session.");
         }
    
    // Pre-load live calendar only if not in tester
       if(EnableNewsFilter && !MQLInfoInteger(MQL_TESTER))
          LoadTodayCalendarEvents();
    
       return INIT_SUCCEEDED;
      }

    Примечание: при реальной торговле LoadTodayCalendarEvents() по-прежнему вызывается как раньше. В режиме тестера этот шаг пропускается, поскольку используются статические данные.

    Шаг 6 — изменение isUpcomingNews() для использования статических данных и кэша валют

    Основную функцию обнаружения новостей необходимо изменить так, чтобы при работе в Тестере стратегий и загруженных статических данных она проверяла массив historicalNews[] вместо вызова API календаря в реальном времени.

    Приведенная ниже логика добавляется в начало isUpcomingNews().

    // --- BACKTEST MODE: use the static CSV data ---
       if(MQLInfoInteger(MQL_TESTER) && staticNewsLoaded)
         {
          if(!currencyCacheValid)
             return false;
    
          // Loop through static events
          for(int i = 0; i < ArraySize(historicalNews); i++)
            {
             // Importance filter
             bool okImportance = false;
             switch(NewsImportanceMode)
               {
                case NEWS_HIGH_ONLY:
                   okImportance = (historicalNews[i].importance == 3);
                   break;
                case NEWS_MODERATE_ONLY:
                   okImportance = (historicalNews[i].importance == 2);
                   break;
                case NEWS_HIGH_AND_MODERATE:
                   okImportance = (historicalNews[i].importance >= 2);
                   break;
               }
             if(!okImportance)
                continue;
    
             // Currency relevance using cached array
             bool affect = false;
             for(int j = 0; j < cachedCurrencyCount; j++)
               {
                if(historicalNews[i].currency == cachedCurrencies[j])
                  {
                   affect = true;
                   break;
                  }
               }
             if(!affect)
                continue;
    
             // Time window check
             datetime eventTime = historicalNews[i].eventTime;
             datetime windowStart = eventTime - (NewsMinutesBefore * 60);
             datetime windowEnd   = eventTime + (NewsMinutesAfter * 60);
    
             if(now >= windowStart && now <= windowEnd)
               {
                // Optional: print once per event to avoid spam
                static string lastDetectedKey = "";
                string key = historicalNews[i].name + "|" + TimeToString(eventTime);
                if(key != lastDetectedKey)
                  {
                   PrintFormat("STATIC NEWS: %s | Time: %s | Currency: %s",
                               historicalNews[i].name, TimeToString(eventTime), historicalNews[i].currency);
                   lastDetectedKey = key;
                  }
                return true;
               }
            }
          return false;
         }

    Ниже показано, как будет выглядеть вся функция isUpcomingNews() после добавления раздела для тестирования на исторических данных:

    //+-----------------------------------------------------------------------+
    //| Core Detection Logic                                                  |
    //| Returns true if 'now' is inside a news window for the specific symbol |
    //+-----------------------------------------------------------------------+
    bool isUpcomingNews(const string symbol)
      {
       datetime now = TimeCurrent();
    
    // --- BACKTEST MODE: use the static CSV data ---
       if(MQLInfoInteger(MQL_TESTER) && staticNewsLoaded)
         {
          if(!currencyCacheValid)
             return false;
    
          // Loop through static events
          for(int i = 0; i < ArraySize(historicalNews); i++)
            {
             // Importance filter
             bool okImportance = false;
             switch(NewsImportanceMode)
               {
                case NEWS_HIGH_ONLY:
                   okImportance = (historicalNews[i].importance == 3);
                   break;
                case NEWS_MODERATE_ONLY:
                   okImportance = (historicalNews[i].importance == 2);
                   break;
                case NEWS_HIGH_AND_MODERATE:
                   okImportance = (historicalNews[i].importance >= 2);
                   break;
               }
             if(!okImportance)
                continue;
    
             // Currency relevance using cached array
             bool affect = false;
             for(int j = 0; j < cachedCurrencyCount; j++)
               {
                if(historicalNews[i].currency == cachedCurrencies[j])
                  {
                   affect = true;
                   break;
                  }
               }
             if(!affect)
                continue;
    
             // Time window check
             datetime eventTime = historicalNews[i].eventTime;
             datetime windowStart = eventTime - (NewsMinutesBefore * 60);
             datetime windowEnd   = eventTime + (NewsMinutesAfter * 60);
    
             if(now >= windowStart && now <= windowEnd)
               {
                // Optional: print once per event to avoid spam
                static string lastDetectedKey = "";
                string key = historicalNews[i].name + "|" + TimeToString(eventTime);
                if(key != lastDetectedKey)
                  {
                   PrintFormat("STATIC NEWS: %s | Time: %s | Currency: %s",
                               historicalNews[i].name, TimeToString(eventTime), historicalNews[i].currency);
                   lastDetectedKey = key;
                  }
                return true;
               }
            }
          return false;
         }
    
    // --- LIVE MODE: use the original calendar API ---
    
    // Cache Management for live calendar
       if(lastCalendarLoad == 0 || (now - lastCalendarLoad) > (datetime)CacheReloadHours * 3600)
         {
          LoadTodayCalendarEvents();
         }
    
       if(ArraySize(TodayEvents) == 0)
          return false;
    
       if(!currencyCacheValid)
          return false;
    
    // Event Scan
       for(int i = 0; i < ArraySize(TodayEvents); i++)
         {
          MqlCalendarEvent ev;
          // Retrieve event details from ID
          if(!CalendarEventById(TodayEvents[i].event_id, ev))
             continue;
    
          // 1. Importance Filter
          bool okImportance = false;
          switch(NewsImportanceMode)
            {
             case NEWS_HIGH_ONLY:
                okImportance = (ev.importance == CALENDAR_IMPORTANCE_HIGH);
                break;
             case NEWS_MODERATE_ONLY:
                okImportance = (ev.importance == CALENDAR_IMPORTANCE_MODERATE);
                break;
             case NEWS_HIGH_AND_MODERATE:
                okImportance = (ev.importance == CALENDAR_IMPORTANCE_HIGH || ev.importance == CALENDAR_IMPORTANCE_MODERATE);
                break;
            }
          if(!okImportance)
             continue;
    
          // 2. Currency Relevance Filter using cached array
          string eventCurrency = GetCurrencyFromEventDirect(ev);
          if(StringLen(eventCurrency) == 0)
             continue;
    
          bool affect = false;
          for(int j = 0; j < cachedCurrencyCount; j++)
            {
             if(eventCurrency == cachedCurrencies[j])
               {
                affect = true;
                break;
               }
            }
          if(!affect)
             continue;
    
          // 3. Time Window Check
          datetime eventTime = TodayEvents[i].time;
          datetime windowStart = eventTime - (NewsMinutesBefore * 60);
          datetime windowEnd   = eventTime + (NewsMinutesAfter * 60);
    
          if(now >= windowStart && now <= windowEnd)
            {
             // Optional: Print once per detection to avoid log spam
             static string lastDetectedKey = "";
             string key = ev.name + "|" + TimeToString(eventTime);
             if(key != lastDetectedKey)
               {
                PrintFormat("NEWS: %s | Time: %s | Currency: %s", ev.name, TimeToString(eventTime), eventCurrency);
                lastDetectedKey = key;
               }
             return true;
            }
         }
    
       return false;
      }
    
    
    // Wrapper for external calls
    bool IsNewsTime(string symbol)
      {
       return isUpcomingNews(symbol);
      }

    Все массивы и функции предыдущей isUpcomingNews(), включая TodayEvents[] и CalendarValueHistory(), остаются без изменений. Добавлена только ветвь тестирования на исторических данных в начале кода, а режимы реальной торговли и тестирования изменены для использования кэша валют.

    Почему кэширование можно безопасно использовать в общем случае?

    Опасение Анализ
    Смена символа? Советник привязан к конкретному графику, поэтому символ не меняется во время работы. Если пользователь перетащит советник на другой график, OnInit() запускается заново, и кэш перестраивается.
    Изменение входных параметров? SymbolCurrencyOverride — входной параметр; для применения изменений требуется повторная инициализация. Снова запускается OnInit(), и кэш обновляется.
    Мультисимвольный советник Потребовался бы более сложный кэш, например отдельный для каждого символа, но это выходит за рамки данной серии статей. Для этого сценария кэширование достаточно безопасно.

    Шаг 7 — создание вспомогательного скрипта для экспорта календаря в реальном времени в CSV

    Чтобы упростить создание собственного HistoricalNews.csv, предоставляется отдельный скрипт, экспортирующий события экономического календаря терминала за указанный диапазон дат. Это не входит в состав советника, но является полезным инструментом.

    Создайте в MetaEditor новый файл скрипта: Файл → Создать → Скрипт. Укажите любое имя, например ExportCalendarToCSV.mq5, и добавьте следующий код:

    Укажите название, сведения об авторских правах, ссылку и версию скрипта.

    //+------------------------------------------------------------------+
    //|                                          ExportCalendarToCSV.mq5 |
    //|                                    Copyright 2026, soloharbinger |
    //|                      https://www.mql5.com/ru/users/soloharbinger |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2026, soloharbinger"
    #property link      "https://www.mql5.com/ru/users/soloharbinger"
    #property version   "1.00"

    Задайте входные параметры скрипта через свойство inputs, в отличие от обычного советника.

    #property script_show_inputs
    
    input datetime FromDate   = D'2024.01.01';
    input datetime ToDate     = D'2024.12.31';
    input string   OutputFile = "HistoricalNews.csv";

    Основная функция запуска скрипта называется OnStart().

    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
        // Script to export calendar events to CSV for backtesting
        MqlCalendarValue events[];
        int count = CalendarValueHistory(events, FromDate, ToDate);
        if(count <= 0)
        {
           Print("No calendar events found in the specified range");
           return;
        }
    
        int handle = FileOpen(OutputFile, FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_COMMON, ",");
        if(handle == INVALID_HANDLE)
        {
            Print("Failed to create file. Error: ", GetLastError());
            return;
        }
    
        // Write header
        FileWrite(handle, "event_time,currency,importance,event_name");
    
        int exported = 0;
        for(int i = 0; i < count; i++)
        {
            MqlCalendarEvent ev;
            if(!CalendarEventById(events[i].event_id, ev))
               continue;
            
            MqlCalendarCountry country;
    
            if(!CalendarCountryById(ev.country_id, country))
               continue;
    
            string timeStr = TimeToString(events[i].time, TIME_DATE|TIME_SECONDS);
            string currency = country.currency;
            int importance = 0;
            if(ev.importance == CALENDAR_IMPORTANCE_HIGH)
               importance = 3;
            else if(ev.importance == CALENDAR_IMPORTANCE_MODERATE)
               importance = 2;
            else importance = 1;
            
            string line = StringFormat("%s,%s,%d,\"%s\"", timeStr, currency, importance, ev.name);
            FileWrite(handle, line);
            exported++;
        }
    
        FileClose(handle);
        PrintFormat("Exported %d events to %s", exported, OutputFile);
      }
    //+------------------------------------------------------------------+

    Теперь трейдер может запустить этот скрипт один раз для каждого года, за который требуется провести тестирование на исторических данных, а затем поместить полученный CSV-файл в папку \Files. Обратите внимание на использование FILE_COMMON: благодаря этому экспортированный файл помещается в общую папку и становится доступным советнику при тестировании на исторических данных.

    Чего мы добились

    • Детерминированное тестирование на исторических данных. Теперь новостные события загружаются из статического файла, что обеспечивает стабильные результаты при каждом запуске.
    • Постоянно интегрированное кэширование валют устраняет повторное разбиение строк на каждом тике.
    • Четкое разделение: при тестировании на исторических данных используются статические данные, в реальной торговле — API календаря, при этом оба режима используют один и тот же кэш валют.
    • Вспомогательный скрипт для создания собственных CSV-файлов с историческими событиями на основе данных календаря в реальном времени.
    • Пользователь может выбрать включаемые события, отредактировав CSV-файл.
    • Эта реализация имеет обучающую ценность, поскольку демонстрирует разбор CSV, условное переключение логики с помощью MQL_TESTER и работу со структурированными данными.

    После завершения реализации в следующем разделе рассматриваются тестирование и валидация: пошаговое создание CSV-файла, добавление временного тестового генератора сделок и интерпретация результатов.


    Тестирование и валидация — проверка загрузчика статических новостей в Тестере стратегий

    Теперь, когда код реализован, необходимо убедиться, что советник правильно распознает новостные события при тестировании на исторических данных. В этом разделе описывается последовательная процедура тестирования, объясняется, как интерпретировать логи, и рассматриваются граничные случаи.

    Почему тестирование имеет решающее значение

    Загрузчик статических новостей вводит новый путь обработки данных, обходящий API календаря в реальном времени. Любая ошибка — неверный разбор данных, неправильное сопоставление валют или ошибки смещения на единицу на границах временного окна — незаметно нарушит работу новостного фильтра. Хуже того, советник продолжит работать, но без обнаружения новостей, что даст недостоверные результаты тестирования на исторических данных. Поэтому необходима явная проверка.

    Настройка тестовой среды

    Перед запуском полного тестирования на исторических данных подготовьте минимальную тестовую конфигурацию.

    Шаг 1 — создание CSV-файла с историческими новостями

    Нужен обычный текстовый файл .csv с новостными событиями, которые требуется смоделировать. Вот как правильно его создать:

    Вариант A — использование Блокнота в Windows

    1. Откройте Блокнот.
    2. Точно скопируйте следующие строки, включая заголовок:

    event_time,currency,importance,event_name
    2024.01.15 14:30:00,USD,3,FOMC Statement
    2024.01.15 15:00:00,EUR,2,German CPI
    2024.01.16 09:00:00,GBP,2,Unemployment Rate

    Сохраните файл под именем TestNews.csv. В раскрывающемся списке "Тип файла" выберите "Все файлы" — иначе Блокнот добавит расширение .txt.

    Файл необходимо поместить в папку Files в песочнице терминала.

    • Откройте MetaEditor 5 → "Файл" → "Открыть каталог данных".
    • Откройте папку \Files\ и поместите туда TestNews.csv.

    Вариант B — использование вспомогательного скрипта из приведенного выше шага 6 реализации загрузчика статических новостей.

    Скомпилируйте и запустите скрипт ExportCalendarToCSV.mq5 на графике в режиме реальной торговли. Скрипт выгрузит реальные события календаря за любую выбранную дату в папку \Files. Затем переименуйте полученный файл в TestNews.csv или скопируйте его под этим именем. Поместите файл в ту же папку, что описана выше.

    Шаг 2 — подготовка советника к тестированию — тестовый генератор сделок

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

    Вставьте следующий код в советник, например в OnTick() после логики перехода состояния новостного фильтра. Этот код будет открывать сделку на покупку в начале каждого часа, если открытых сделок нет.

    // Temporary trade simulator for testing (remove this after full verification) ---
       static datetime lastTradeTime = 0;
       if(EnableNewsFilter && !MQLInfoInteger(MQL_OPTIMIZATION))    // Only enabled for single backtest
         {
          if(TimeCurrent() - lastTradeTime >= 3600)    // Every hour
            {
             if(PositionsTotal() == 0)    // No open positions
               {
                double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
                double sl = price - 1000 * _Point;
                double tp = price + 1000 * _Point;
                double lot = 0.01;
                if(trade.Buy(lot, _Symbol, 0, sl, tp, "Test Trade"))
                   Print("Opened test trade at ", TimeToString(TimeCurrent()));
                lastTradeTime = TimeCurrent();
               }
            }
         }

    Тестовый генератор сделок открывает новую сделку каждый час, если открытых позиций нет. Он использует размер лота 0.01, стоп-лосс 100 пунктов и тейк-профит 100 пунктов. При начале новостного окна советник временно удаляет стоп-уровни, блокирует открытие новых сделок или закрывает все сделки до завершения новостного окна.

    Шаг 3 — настройка входных параметров советника

    Откройте Тестер стратегий и задайте входные параметры.

    Параметр Значение
    Включить новостной фильтр для экономических событий true
    HistoricalNewsFile TestNews.csv
    Сколько минут до новости применять ограничения 15
    Сколько минут после новости применять ограничения 15
    Режим важности новостей NEWS_HIGH_AND_MODERATE
    Приостановить изменение SL/TP на время новостного окна false, при необходимости true
    Блокировать открытие новых сделок во время новостного окна false, при необходимости true
    Закрыть все сделки перед публикацией новостей true, при необходимости false
    Период тестирования С 15.01.2024 по 17.01.2024
    Моделирование "Каждый тик" — чтобы точно учитывать временные окна

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

    Шаг 4 — запустите тестирование на исторических данных и просмотрите сообщения лога

    В тесте для этой статьи параметру "Приостановить изменение SL/TP на время новостного окна" присваивается значение true, чтобы просмотреть сообщения лога о временном удалении стоп-уровней.

    Проводятся два теста с использованием методов вариантов A и B, описанных в шаге 1.

    Вариант А

    В рамках первого теста для параметра "Приостановить изменение SL/TP на время новостного окна" устанавливается значение true, чтобы просмотреть сообщения лога о временном удалении стоп-уровней. Используются настройки из приведенной выше таблицы, после чего делается снимок экрана лога.

    Journal log from simulated news backtest

    Если вместо этого появится сообщение об ошибке, выполните диагностику:

    • INVALID_HANDLE — файл не найден, проверьте путь и убедитесь, что файл находится в папке \Terminal\Common\Files;
    • неверный формат даты — убедитесь, что даты точно соответствуют формату YYYY.MM.DD HH:MM:SS.

    Наблюдая за новостным окном на скриншоте, мы видим, что по мере приближения времени моделирования к первому событию (2024.01.15 14:30:00), примерно в 14:15, то есть за 15 минут до него, появляется сообщение о начале новостного окна. Тестовый генератор в это время не открывает новых сделок, а советник корректно восстанавливает стоп-уровни в нужный момент, в том числе при перекрытии новостных событий.

    Вариант B

    Для этого теста используется скрипт ExportCalendarToCSV.mq5. На этот раз файл новостей сохранен под именем TestNews2.csv и указан во входных параметрах советника. Тестирование проводится за более длительный период, поскольку используются реальные статические новостные данные.

    Результат соответствует тому, что показано на предыдущем скриншоте.

    Чтобы оценить преимущества этой реализации, проведите два тестирования на исторических данных за один и тот же период:

    • До — используется исходный советник или советник с пустым параметром HistoricalNewsFile. В логе не будет обнаружения новостей, поскольку фильтр никогда не активируется.
    • После — используется корректно заполненный CSV-файл. В логе будут отображаться новостные окна, временное удаление стоп-уровней и блокировки сделок.

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

    Далее проводится еще один тест. В следующем видео будет показано:

    • поведение, описанное выше;
    • поведение, при котором параметр "Закрыть все сделки перед публикацией новостей" успешно закрывает сделки перед новостным окном, позволяя тестовому генератору сделок продолжать открывать новые сделки.

      Тестирование граничных случаев

      1. Перекрытие нескольких событий: если два события происходят с небольшим интервалом, например USD в 14:30 и EUR в 14:45, советник должен непрерывно оставаться в новостном окне. Сообщение о начале появляется только один раз, а сообщение о завершении — только после закрытия последнего окна. Логика переходов между состояниями уже учитывает это, поскольку IsNewsTime() возвращает true в течение всего объединенного периода.
      2. Сделки с изначально нулевыми значениями SL/TP: в некоторых сделках стоп-лосс или тейк-профит могут быть не установлены, например 0.0. Функция временного удаления стоп-уровней должна сохранять 0.0, а затем восстанавливать 0.0. Логика восстановления при проверке цены должна пропускать корректировку нулевых уровней.
      3. CSV-файл с большим диапазоном дат: загрузите CSV-файл с событиями за несколько лет. Советник должен разобрать все события без снижения производительности. Можно добавить таймер для измерения времени загрузки.
      4. Отсутствующий CSV-файл: если файл не существует, советник выводит сообщение об ошибке и продолжает работу без обнаружения новостей. Тестирование на исторических данных запустится, но будет игнорировать новости. Это допустимо, если трейдер об этом знает.

      Распространенные проблемы и их решения

      Проблема Симптом Решение
      В CSV-файле используется неверный разделитель даты, например 2024-01-15. Функция StringToTime() возвращает 0. Замените дефисы точками или используйте StringReplace().
      CSV-файл, сохраненный в кодировке UTF-8 с BOM Первая строка содержит невидимые символы. Сохраните в ANSI или UTF-8 без BOM.
      Файл помещен в неверную папку. INVALID_HANDLE или ошибка 5004. Проверьте путь к файлам или переместите их в \Terminal\Common\Files\.
      Несоответствие часовых поясов Советник обнаруживает новости не в то время.
      Убедитесь, что время в CSV-файлах указано в UTC: MetaTrader 5 внутренне использует UTC.
      Нет открытых позиций для временного удаления стоп-уровней, блокировки или ограничения. В логе нет сообщений. Добавьте тестовый генератор сделок или подключите новостной фильтр к советнику, открывающему сделки.

      Что сделано в этом разделе

      • Практическое пошаговое руководство по созданию CSV-файла с точным указанием пути и кодировки.
      • В советник встроен простой генератор сделок, позволяющий увидеть влияние новостного фильтра на реальные позиции.
      • Четкая процедура тестирования для проверки загрузчика статических новостей. Выявление граничных случаев и описание их ожидаемого поведения.


      Заключение

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

      Что теперь у вас есть:

      • загрузчик статического CSV-файла, используемый советником только в режиме тестера, MQL_TESTER, и предоставляющий детерминированные исторические события;
      • компактный CSV-формат (event_time,currency,importance,event_name) и вспомогательный скрипт для экспорта данных календаря реального времени в этот формат;
      • кэшированный список валют для каждого символа, обеспечивающий быструю и надежную фильтрацию событий;
      • бесшовное переключение — в реальной торговле по-прежнему используется календарь терминала, а при тестировании на исторических данных — статический файл;
      • необязательный тестовый генератор сделок для проверки поведения во время тестирования.

      Измеримые критерии успеха

      • В логе в ожидаемые моменты времени появляются записи STATIC NEWS/NEWS.
      • Во время новостных окон советник блокирует открытие сделок, временно удаляет SL/TP или закрывает сделки в соответствии с входными параметрами.
      • Перекрывающиеся события формируют непрерывные новостные окна и единую последовательность переходов между состояниями.
      • Результаты воспроизводимы: один и тот же CSV-файл дает идентичные обнаружения и действия советника.

      Краткий контрольный список для проверки:

      1. поместите CSV-файл в папку Files (или Common\Files) терминала;
      2. укажите имя файла в параметре HistoricalNewsFile и включите новостной фильтр;
      3. запустите одно тестирование на исторических данных в режиме "Каждый тик" за период CSV-файла;
      4. проверьте маркеры в логе, сообщения о временном удалении и восстановлении стоп-уровней, а также закрытие и открытие сделок в ожидаемые моменты времени.

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


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

      Прикрепленные файлы |
      Нейросети в трейдинге: Управление риском через распределение результатов (Вычислительная основа) Нейросети в трейдинге: Управление риском через распределение результатов (Вычислительная основа)
      Продолжаем адаптацию AC-SRM к торговым задачам и переносим основные операции с распределением результатов на сторону OpenCL. Реализованы выборка из распределения, расчет спектральных весов и риск-чувствительная оценка, а также градиенты по значениям и вероятностям с учетом пути через квантильное кодирование. Полученные примитивы позволяют оценивать возможные торговые результаты с заданным отношением к риску и формировать согласованный обучающий сигнал для распределительной модели.
      Автоматизация индикатора рыночной энтропии: Торговая система на основе теории информации Автоматизация индикатора рыночной энтропии: Торговая система на основе теории информации
      В этой статье представлен советник, автоматизирующий ранее представленную методику рыночной энтропии. Он рассчитывает быструю и медленную энтропию, моментум и состояния сжатия, подтверждает сигналы и исполняет ордера с уровнями SL/TP и опциональным разворотом позиции. В результате получается практичный настраиваемый инструмент, применяющий сигналы на основе теории информации без ручной интерпретации.
      Как реализовать прогнозирование с помощью AutoARIMA в MQL5 Как реализовать прогнозирование с помощью AutoARIMA в MQL5
      В статье представлена реализация алгоритма AutoARIMA на языке MQL5, позволяющего строить модели ARIMA без ручной настройки. Метод оценивает d с помощью эвристики на основе дисперсии, подгоняет модель ARMA(p,q) посредством градиентной оптимизации с Adam и выбирает p и q по AICc. Код возвращает одношаговый прогноз цены, полученный путем дифференцирования, оценки модели и восстановления ряда до уровня цены, и может быть сразу вызван для ряда Close.
      От новичка до эксперта: Автоматизация внутридневных стратегий От новичка до эксперта: Автоматизация внутридневных стратегий
      Мы переводим идею ретеста EMA(50) в советник для внутридневной торговли, основанный на поведении цены. В исследовании формализуются трендовый уклон, взаимодействие с EMA (проколоть/пробить и закрыться), подтверждение реакции и дополнительные фильтры, после чего они реализуются в MQL5 с использованием модульных функций и дескрипторов с безопасным управлением ресурсами. Визуальное тестирование в Тестере стратегий позволяет проверить корректность сигналов. В результате получается понятный шаблон для кодирования дискреционных отскоков.