preview
MetaTrader 5 и экономический календарь MQL5: как превратить новости в воспроизводимую торговую систему

MetaTrader 5 и экономический календарь MQL5: как превратить новости в воспроизводимую торговую систему

MetaTrader 5Примеры |
52 0
MetaQuotes
MetaQuotes

Введение

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

Рабочий процесс новостного трейдера выглядит так: Быстро открыть календарь новостей в интернет браузере и просмотреть события — не изменились ли → Опять же быстро оценить ближайшие события и принять решение — что и как торговать → Перейти в терминал MetaTrader 5 — либо поставить отложенные ордера, либо сидеть у терминала и ждать времени выхода новости, чтобы принять решение. При таком сценарии у трейдера часто происходит разрыв контекста, что приводит к задержкам в реакции на новости, а это ведет к убыткам.

Вы хотите, чтобы торговля на новостях работала как инженерная задача — с чёткими правилами, повторяемыми результатами и возможностью автоматического тестирования? Цель статьи — показать рабочую архитектуру новостного слоя для MetaTrader 5: единый источник данных, корректная работа с API календаря, механизм фильтрации и кэширования, экспорт исторических событий в ресурс для тестера и автоматическое переключение между Live и Tester — чтобы один и тот же код давал детерминированные результаты и в реальном времени, и на истории.

Main Trader's Problems

Рис.1: Главная проблема ручного трейдера — разрозненность инструментов


Ручная торговля на новостях устарела

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

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

В-третьих, задержки в реакции на новости это «бич» ручной торговли. Представьте, что вы трейдер FOREX  и торгуете на новости о занятости в несельскохозяйственном секторе (по-английски: Non Farm Payrolls — NFP) — знаменитые «нонфармы». Данные по NFP выходят, как правило, раз в месяц. Вас отвлекли, и вы пропустили NFP, и потеряли возможность заработать. Знакомая ситуация для многих трейдеров.

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

Reaction Comparison

Рис. 2: Робот всегда быстрей


Встроенный Экономический календарь в MetaTrader 5: единый источник информации

Для алгоритмизации новостной торговли, а значит и тестирования на истории используйте уникальный продукт от нашей компании — встроенный в MetaTrader 5 экономический календарь с доступом к новостям через MQL5-API. Именно это превратит вашу новостную торговлю из импровизации в проверяемый алгоритмический процесс. Доступны  как текущие события — в реальном времени, так и история событий — для оффлайн-тестирования.

Таблица 1:

Что делает календарь, встроенный в MetaTrader 5, уникальным:

Параметр
Скорость доступа
<100 мс — после загрузки новостей в терминал
Интеграция Нативная — на уровне ядра терминала + MQL-API
Тестирование Полностью поддерживается — после загрузки новостей в терминал (доступ в тестере через файлы, ресурсы или через SQLite)
Надёжность Высокая

В MetaTrader 5 есть превосходный тестер стратегий — быстрый, мульти-инструментальный, с возможностью использовать для тестирования и оптимизации компьютеры в локальной и глобальной сетях. Попробуйте «прогнать» свою новостную торговую систему на истории — возможно ваш подход к торговле изменится в лучшую сторону.


Функции экономического календаря в MetaTrader 5: обзор MQL5-API

Переход от ручного анализа к алгоритмическому начинается с понимания архитектуры данных. В MQL5 экономический календарь — это не просто таблица, а структурированная база данных, доступная через нативный API. Разберем, как правильно запрашивать события, в чем разница между Event и Value, и почему синхронизация времени — это необходимое условие успеха стратегии.

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

  • CalendarValueHistory — основной инструмент для первичной загрузки. Позволяет получить массив значений событий за заданный временной интервал. Это «тяжелая артиллерия», используемая при инициализации советника для заполнения кэша историческими данными или данными на неделю вперед.
  • CalendarValueLast — основная функция для роботов, работающих в реальном времени. Возвращает только изменившиеся или новые значения с момента последнего запроса (через механизм change_id). Это позволяет экономить трафик и ресурсы сервера, не запрашивая весь массив данных на каждом тике.
  • CalendarEventByCountry — получение описаний всех событий по стране. Возвращает список описаний событий для конкретной страны, заданной кодом по «ISO 3166-1 alpha-2». Необходима для построения фильтров (например, показать только события для «US» (США), «RU» (Россия), «CA» (Канада) и т.д.).
  • CalendarEventByCurrency — получение описаний всех событий по валюте. Возвращает список описаний событий для конкретной валюты, заданной кодом («USD», «EUR» и т.д.).
  • CalendarCountryById — получение свойств страны по 'id'.
  • CalendarEventById — получение свойств события по 'id'.
  • CalendarValueById — получение конкретного значения по 'id'.

Структуры данных: что возвращает календарный MQL5-API

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

Описания событий — используется в функциях CalendarEventById, CalendarEventByCountry и CalendarEventByCurrency:

struct MqlCalendarEvent
  {
   ulong                               id;                    // идентификатор события
   ENUM_CALENDAR_EVENT_TYPE            type;                  // тип события из перечисления ENUM_CALENDAR_EVENT_TYPE
   ENUM_CALENDAR_EVENT_SECTOR          sector;                // сектор, к которому относится событие
   ENUM_CALENDAR_EVENT_FREQUENCY       frequency;             // частота (периодичность) события
   ENUM_CALENDAR_EVENT_TIMEMODE        time_mode;             // режим времени события
   ulong                               country_id;            // идентификатор страны
   ENUM_CALENDAR_EVENT_UNIT            unit;                  // единица измерения значения экономического индикатора
   ENUM_CALENDAR_EVENT_IMPORTANCE      importance;            // важность события
   ENUM_CALENDAR_EVENT_MULTIPLIER      multiplier;            // множитель значения экономического индикатора
   uint                                digits;                // количество знаков после запятой
   string                              source_url;            // URL источника, где публикуется событие
   string                              event_code;            // код события
   string                              name;                  // текстовое имя события на языке терминала (в текущей кодировке терминала)
  };


Описания стран — используется в функциях CalendarCountryById и CalendarCountries:

struct MqlCalendarCountry
  {
   ulong                               id;                    // идентификатор страны по стандарту ISO 3166-1
   string                              name;                  // текстовое имя страны (в текущей кодировке терминала)
   string                              code;                  // кодовое имя страны ISO 3166-1 alpha-2
   string                              currency;              // код валюты страны
   string                              currency_symbol;       // символ/знак валюты страны
   string                              url_name;              // имя страны, используемое в URL на сайте mql5.com
  };


Значения событий — используется в функциях CalendarValueById, CalendarValueHistoryByEvent, CalendarValueHistory, CalendarValueLastByEvent и CalendarValueLast.

struct MqlCalendarValue
  {
   ulong                               id;                    // ID значения
   ulong                               event_id;              // ID события
   datetime                            time;                  // время и дата события
   datetime                            period;                // отчетный период события
   int                                 revision;              // ревизия публикуемого индикатора по отношению к отчетному периоду
   long                                actual_value;          // актуальное значение в миллионных долях или LONG_MIN, если значение не задано
   long                                prev_value;            // предыдущее значение в миллионных долях или LONG_MIN, если значение не задано
   long                                revised_prev_value;    // пересмотренное предыдущее значение в миллионных долях или LONG_MIN, если значение не задано
   long                                forecast_value;        // прогнозное значение в миллионных долях или LONG_MIN, если значение не задано
   ENUM_CALENDAR_EVENT_IMPACT          impact_type;           // потенциальное влияние на курс валюты
   //--- функции для проверки значений
   bool                         HasActualValue(void) const;   // возвращет true, если значение в поле actual_value задано
   bool                         HasPreviousValue(void) const; // возвращет true, если значение в поле prev_value задано
   bool                         HasRevisedValue(void) const;  // возвращет true, если значение в поле revised_prev_value задано
   bool                         HasForecastValue(void) const; // возвращет true, если значение в поле forecast_value задано
   //--- функции для получение значений
   double                       GetActualValue(void) const;   // возвращает actual_value или nan, если значение не задано
   double                       GetPreviousValue(void) const; // возвращает prev_value или nan, если значение не задано
   double                       GetRevisedValue(void) const;  // возвращает revised_prev_value или nan, если значение не задано
   double                       GetForecastValue(void) const; // возвращает forecast_value или nan, если значение не задано
  };

Важно:

Обратите внимание, что структура MqlCalendarValue предоставляет методы для проверки и получения значений из полей actual_value, forecast_value, prev_value и revised_prev_value. Перечисленные поля могут не иметь значений —  например, поле actual_value может быть не заполнено, поскольку новость еще не вышла. Наилучший способ получения значений — делать проверку и получать значения методами самой структуры.

Структуры связаны между собой следующими отношениями:

Calendar Relations

Рис. 3: Отношения календарных структур


Структура MqlCalendarCountry связана с MqlCalendarEvent посредством идентификатора страны. Форма связи «один ко многим» (1..*).

Структура MqlCalendarEvent связана с MqlCalendarValue посредством идентификатора события. Форма связи «один ко многим» (1..*).


Время релиза новости и серверное время

Все функции для работы с Экономическим календарем используют время торгового сервера TimeTradeServer(). Это означает, что время в структуре MqlCalendarValue и входные параметры с временем в функциях CalendarValueHistoryByEvent() и CalendarValueHistory() задаются в часовом поясе торгового сервера, а не в локальном времени пользователя.

Нет необходимости в конвертации: время события MqlCalendarValue::period можно напрямую сравнивать со временем, получаемым при вызове функций TimeCurrent() или TimeTradeServer(). В тестере функция TimeTradeServer() возвращает модельное время, идентичное времени в исторических данных. Логика работы с временными окнами («за 30 минут до новости») работает одинаково в реальном времени и на истории. Если брокер учитывает переход на летнее/зимнее время — календарь автоматически подстраивается под этот переход.


Практический пример — получение списка событий на сегодня (на текущие сутки):

//+------------------------------------------------------------------+
//| Получение значений календаря на текущие сутки                    |
//+------------------------------------------------------------------+
void GetTodayUSD_Events()
 {
//--- определяем границы периода в серверном времени
  datetime server_now = TimeTradeServer();
  datetime day_start  = server_now - (server_now % 86400);
  datetime day_end    = day_start + 86400;

  MqlCalendarValue    values[];
  MqlCalendarEvent    event;
  MqlCalendarCountry  country;

//--- запрашиваем значения только для USD
  if(CalendarValueHistory(values, day_start, day_end, NULL, "USD"))
   {
    Print("  Получено событий для USD: ", ArraySize(values));

    //--- проходим по массиву значений 
    for(int i = 0; i < ArraySize(values); i++)
     {
      //--- получим описание события
      if(CalendarEventById(values[i].event_id, event))
      {
        //--- получим описание страны
        if(CalendarCountryById(event.country_id, country))
        {
          Print("✅ Событие #", i);
          Print("ID события:       ", values[i].event_id);

          Print("Название события: ", event.name);
          Print("Сектор:           ", event.sector);
          Print("Источник:         ", event.source_url);

          Print("Название страны:  ", country.name);
          Print("URL страны:       ", country.url_name);

          Print("Время:            ", TimeToString(values[i].time, TIME_DATE | TIME_SECONDS));
          Print("Влияние:          ", values[i].impact_type);

          // ПРОВЕРКА И ВЫВОД ЗНАЧЕНИЙ
          if(values[i].HasActualValue())
            Print("Факт:             ", values[i].GetActualValue());

          if(values[i].HasRevisedValue())
            Print("Пересмотрено:     ", values[i].GetRevisedValue());

          if(values[i].HasForecastValue())
            Print("Прогноз:          ", values[i].GetForecastValue());

          if(values[i].HasPreviousValue())
            Print("Предыдущее:       ", values[i].GetPreviousValue());
        }
      }
     }
   }
  else
   {
    int error = GetLastError();
    if(error == 0)
     {
      Print("❌ CalendarValueHistory: No Events");
     }
    else
     {
      Print("❌ Ошибка CalendarValueHistory: ", error);
     }
   }
 }
//+------------------------------------------------------------------+

Функция выводит список событий для валюты «USD» на текущий день и содержимое основных полей структур, получаемых через MQL-API календаря. Полный код скрипта содержится в файле «GetTodayEvents-S.mq5», приложенном к статье.

Результат работы данной функции смотрим во вкладке «Инструменты\Эксперты» терминала MetaTrader 5. Видим, что получено два события для валюты «USD». На момент запроса эти события еще не произошли (новости не вышли). Поэтому поле MqlCalendarValue::actual_value не содержит значения — функция HasActualValue() возвращает false.

Для данных видов событий отсутствует (не содержит значения) поле MqlCalendarValue::forecast_value, которое проверяем функцией HasForecastValue(). Для других событий могут отсутствовать все четыре поля (prev_value, actual_value, forecast_value, revised_prev_value).

  Получено событий для USD: 2
✅ Событие #0
ID события:            840220005
Название события: Аукцион по размещению 3-месячных казначейских векселей
Сектор:                  1
Источник:              https://home.treasury.gov/
Название страны:  Соединенные Штаты
URL страны:           united-states
Время:                   2026.04.20 18:30:00
Влияние:                0
Предыдущее:         3.62
✅ Событие #1
ID события:            840220006
Название события: Аукцион по размещению 6-месячных казначейских векселей
Сектор:                  1
Источник:              https://home.treasury.gov/
Название страны:  Соединенные Штаты
URL страны:           united-states
Время:                   2026.04.20 18:30:00
Влияние:                0
Предыдущее:         3.61

Пояснения по приведенному коду:

Сначала получаем массив значений по всем событиям на заданном диапазоне времени с фильтром по валюте «USD». В цикле проходим по массиву значений и для каждого значения запрашиваем по идентификатору события MqlCalendarValue::event_id описание события функцией CalendarEventById(). Затем запрашиваем описание страны по идентификатору MqlCalendarEvent::country_id функцией CalendarCountryById().

Если функция CalendarValueHistory() возвращает false, но GetLastError() возвращает ноль (отсутствие ошибки) — это говорит о том, что событий по запросу с данными настройками нет.


Обработка ошибок и лимиты при работе с календарем

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

Коды ошибок:

  • ERR_CALENDAR_TIMEOUT (код 5200) — время ожидания ответа от сервера истекло. Проблема сети или сервер перегружен. Решение: Повторить запрос через паузу 5...10 секунд.
  • ERR_CALENDAR_NO_DATA (код 5201) — данные календаря еще не загружены. Календарь инициализируется асинхронно. Решение: Подождать 1...2 секунды и повторить.
  • ERR_CALENDAR_INVALID_DATE (код 5202) — некорректный диапазон дат. Ошибка в коде (например, дата начала больше даты конца). Решение: Исправить логику, повторный запрос бесполезен.
  • ERR_CALENDAR_INVALID_COUNTRY (код 5203) — неизвестный код страны/валюты. Ошибка в параметрах запроса. Решение: Проверить код (например, "US" вместо "USA").
  • ERR_CALENDAR_TOO_MANY_REQUESTS (код 5204) — превышен лимит запросов. Критическая ошибка. Решение: Увеличить интервал между запросами.

Ограничение частоты запросов:

Серверы, к которым подключается терминал MetaTrader 5, защищают инфраструктуру от перегрузок. Если советник будет вызывать CalendarValueHistory() на каждом тике или в цикле без задержек, сервер вернёт ошибку 5204 и временно заблокирует доступ к календарю для вашего терминала. Лучшая практика: загружайте данные один раз при старте в OnInit() на необходимый период.

Для обновления в реальном времени используйте CalendarValueLast() с сохранением значения возвращаемой переменной change_id — это позволит получать только изменения, а не весь массив данных. Обновляйте данные по таймеру в OnTimer() с интервалом не чаще 5–10 минут.

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

При запуске терминала («холодном» старте) и советника календарь не готов мгновенно. Данные подгружаются с сервера в фоне. Если вызовете функцию CalendarValueHistory() в самой первой строке OnInit(), вы, с большой вероятностью, получите ошибку 5201 — нет данных. Необходимо реализовать механизм опроса готовности с экспоненциальным или фиксированным тайм аутом.

Практический пример — функция загрузки данных календаря с обработкой ошибок:

//+------------------------------------------------------------------+
//| Функция загрузки календаря                                       |
//+------------------------------------------------------------------+
bool LoadCalendar(MqlCalendarValue& values[], const datetime from, const datetime to, const string country_code = NULL, const string currency = NULL, const int max_retries = 5)
 {
  int retry_count = 0;

  while(retry_count < max_retries)
   {
    ResetLastError();
    //--- пытаемся загрузить
    if(CalendarValueHistory(values, from, to, country_code, currency))
      return true; // Успех

    int error = GetLastError();

    //--- если ошибка "Нет данных" (5201) или "Таймаут" (5200) — ждем и повторяем
    if(error == 5201 || error == 5200)
     {
      retry_count++;
      Sleep(1000); // пауза 1 секунда перед повтором
      continue;
     }

    //--- если ошибка критическая (например, неверная дата) — прерываем загрузку сразу
    Print("❌ Critical Calendar Error: ", error);
    return false;
   }

  Print("❌ Failed to load calendar after ", max_retries, " attempts.");
  return false;
 }
//+------------------------------------------------------------------+

Пояснения по приведенному коду:

Учитываем возможность возникновения ошибок «Нет данных» (5201) и «Таймаут» (5200). Это ошибки устранимые. Обрабатываем их делая 1...5 секундную паузу и повторно запрашивая данные. При возникновении неустранимых ошибок, прерываем загрузку сразу. Неустранимые ошибки это — неверная дата, не правильный код валюты или страны и т.п.  Полный код скрипта содержится в файле «GetTodayEvents-S.mq5», приложенном к статье.


Чтобы избежать лимитов и обеспечить максимальную скорость реакции, в OnTimer() следует использовать механизм change_id. При старте загружаем полную историю событий и запоминаем последний change_id. В таймере запрашиваем только новые данные функцией CalendarValueLast(). Сервер вернёт только то, что изменилось (или false, если изменений нет), не тратя ресурсы на передачу старых данных.

Практический пример — функция обновления данных календаря с обработкой ошибок:

//+------------------------------------------------------------------+
//| Таймер: инкрементальное обновление                               |
//+------------------------------------------------------------------+
void OnTimer()
 {
  if(!is_initialized)
    return;

  MqlCalendarValue updates[];
  ResetLastError();

//--- API автоматически обновляет last_change_id по ссылке
  if(!CalendarValueLast(last_change_id, updates,
                        (InpCountryCode == "") ? NULL : InpCountryCode,
                        (InpCurrency    == "") ? NULL : InpCurrency))
   {
    int err = GetLastError();
    // 0 = SUCCESS/NO_NEW_DATA, 5402 = ERR_CALENDAR_NO_CHANGES
    if(err != 0 && err != 5402)
      Print("🔴️ CalendarValueLast error: ", err);
    return;
   }

  int cnt = ArraySize(updates);
  if(cnt == 0)
    return;

  Print("🟢 Received ", cnt, " updates. New change_id: ", last_change_id);
  if(InpPrintChanges)
    ArrayPrint(updates);
  SyncCache(updates);
 }
//+------------------------------------------------------------------+
//| Синхронизация кэша                                               |
//+------------------------------------------------------------------+
void SyncCache(const MqlCalendarValue &updates[])
 {
  int upd_cnt  = ArraySize(updates);
  int cache_sz = ArraySize(calendar_cache);

  for(int u = 0; u < upd_cnt; u++)
   {
    bool found = false;
    for(int c = 0; c < cache_sz; c++)
     {
      if(calendar_cache[c].id == updates[u].id)
       {
        calendar_cache[c] = updates[u];
        found = true;
        break;
       }
     }
    if(!found)
     {
      ArrayResize(calendar_cache, cache_sz + 1);
      calendar_cache[cache_sz] = updates[u];
      cache_sz++;
      total_events++;
     }
   }
 }
//+------------------------------------------------------------------+

Пояснения по приведенному коду:

Инкрементальное обновление событий происходит через обработчик событий от таймера OnTimer(). Вызывается функция календарного API CalendarValueLast() — в случае появления новых событий, они обновляют в загруженном ранее массиве структур MqlCalendarValue соответствующие элементы и выводится информация в журнал терминала. Если такого события в загруженном массиве не находится, то оно добавляется в массив.

Полный код скрипта содержится в файле «CalendarEventMonitor-EA.mq5», приложенном к статье.


Фильтрация событий: от общего к частному

Зачем фильтровать:

В экономическом календаре публикуется 60−90 событий ежедневно. Это как слушать непрерывный шум из сотен второстепенных индикаторов, праздников и речей чиновников, которые не оказывают немедленного влияния на рынок. Торговать по каждому из них — прямой путь к «переторговке» и сливу депозита. Задача новостного алготрейдера — оставить 3−5 событий, которые действительно двигают рынок.

Фильтрация в алгоритмической торговле на новостях эквивалентна повышению соотношения «сигнал/шум» до уровня, пригодного для принятия автоматических решений. В календарном MQL5-API этот процесс реализуется как многоуровневая система сито, через которое проходят массивы структур MqlCalendarEvent и MqlCalendarValue.

Критерии фильтрации:

Рынок реагирует не на сам факт выхода новости, а на отклонение факта от прогноза и уровень макроэкономического влияния. События с низким влиянием (MqlCalendarValue::impact_type < 3) часто игнорируются торговыми алгоритмами и маркет-мейкерами.

Без фильтрации вы получаете:

  • Ложные срабатывания стратегии на второстепенной статистике;
  • Торговлю в периоды расширенного спреда без волатильности;
  • Перегрузку логики советника ненужными проверками.

Цель фильтрации: сократить поток данных на 90%, оставив только события с высоким влиянием (ENUM_CALENDAR_EVENT_IMPORTANCE::CALENDAR_IMPORTANCE_HIGH), для выбранной трейдером целевой валюты, в заданном временном окне. 

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

Многоуровневая система фильтрации:

«Сырой» массив событий, загруженный из терминала, подобен необработанной руде: в нём содержатся тысячи записей — от малозначимых статистических отчётов до праздничных дней, которые не несут для алгоритмической стратегии никакой пользы.

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

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

Важная особенность новостного MQL5-API — структура MqlCalendarValue не содержит полей currency_id и country_id. Она хранит только event_id, время, значения индикаторов и тип влияния. Как это влияет на фильтрацию?

Валютная фильтрация выполняется на уровне API-запроса. Когда вы вызываете CalendarValueHistory(..., NULL, "USD"), терминал сам отбрасывает всё, кроме доллара. Массив, который вы получаете, уже отфильтрован по валюте. Поэтому в реальном времени не нужно проверять currency_id — достаточно фильтровать по времени, важности и наличию/значению прогноза.

Уровень 1: Валютный фильтр

Первый барьер — география события. Торгуя парой «EURUSD», робот должен реагировать только на события, влияющие на Еврозону («EUR») и США («USD»). Фильтрация сводится к запросу событий с кодами валюты торгуемого инструмента. Для кросс-курсов (например, «GBPJPY») необходимо запрашивать события по обеим валютам («GBP» и «JPY») и доллару США («USD»).

Уровень 2: Важность события

Все новости имеют различную важность по воздействию на экономику — фактически, на цену. Индекс потребительских цен (CPI) может сдвинуть рынок на 100 пунктов, в то время как индекс доверия потребителей ZEW — лишь на 10.

MQL5-API использует перечисление ENUM_CALENDAR_EVENT_IMPORTANCE для задания степени важности:

enum ENUM_CALENDAR_EVENT_IMPORTANCE
{
   CALENDAR_IMPORTANCE_NONE,      // степень важности не задана
   CALENDAR_IMPORTANCE_LOW,       // низкая важность
   CALENDAR_IMPORTANCE_MODERATE,  // средняя важность
   CALENDAR_IMPORTANCE_HIGH       // высокая важность
};

Новостные роботы должны игнорировать всё ниже HIGH, чтобы избежать ложных срабатываний на новостях с низкой ликвидностью.

Уровень 3: Код события

Даже среди событий высокой важности есть исключения. Например, выступление главы ЕЦБ или МВФ может быть помечено как важное, но алгоритмически его труднее обрабатывать, чем публикацию конкретных цифр. Поэтому нужна фильтрация по коду события: поле MqlCalendarEvent::event_code содержит уникальный идентификатор (например, "NONFARM", "CPI", "GDP"). Отбираем только события с цифрами экономических показателей, а не опросы и речи.

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

Filter-Levels

Рис.4: Многоуровневая фильтрация — ключ к успеху в новостной торговле

После первых трех уровней фильтрации отслеживается время выхода события на предмет попадания во временное окно — это происходит уже во время работы торгового эксперта.

Уровень 4: Временное окно

Финальный этап — проверка актуальности. Новость, которая выйдет через месяц, нам сейчас не нужна. Новость, которая была вчера, уже отработана рынком. Поэтому, событие считается активным, если TimeCurrent() попадает в интервал: заданное время до выхода новости и заданное время после выхода. Обычно используют настройки: 15–30 минут до выхода — для закрытия позиций и 60 минут после — для анализа волатильности и направления движения цены.


Бинарные ресурсы MQL5: перенос событий в оффлайн-тест для эффективного кэширования

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

Если робот в тестере вызовет функцию CalendarValueHistory(), он получит ошибку и пустой массив. Чтобы протестировать стратегию, нужно «зашить» исторические данные новостей внутрь исполняемого файла «.ex5». Для этого используем вариант с бинарными ресурсами MQL5. Почему именно этот вариант? Это самый быстрый способ доступа к структурированным данным — скорость доступа ограничевается только производительностью файловой системы компьютера.

Разберем пошагово, как превратить массив MqlCalendarValue[] в компактный бинарный файл и как встроить его в код так, чтобы тестер читал данные из оперативной памяти со скоростью молнии. Первым шагом является создание скрипта-экспортёра, который загрузит актуальные данные из календаря и сохранит их в бинарный файл.

Практический пример — скрипт экспорта событий в бинарный файл:

Полный код скрипта находится в файле «ExportCalendarForTester.mq5», приложенном к статье.


Шаг 1: Сборка и фильтрация массива

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

Список кодов событий (параметр InpEventCodes) по умолчанию пустой — это значит, что загружаются все типы событий. Можно сузить этот фильтр. Например, только «нонфармы» — тогда список кодов будет такой "NONFARM".

Флаг InpUseCommonDir задает куда сохранять бинарный файл: true — сохранять в общую папку для всех всех клиентских терминалов "\Terminal\Common\Files".

//--- входные параметры
input string                          InpCurrencies     = "USD";
input datetime                        InpDateFrom       = D'2025.01.01';
input datetime                        InpDateTo         = 0;
input string                          InpEventCodes     = "";
input ENUM_CALENDAR_EVENT_IMPORTANCE  InpMinImportance  = CALENDAR_IMPORTANCE_HIGH;
input string                          InpOutputFile     = "calendar_test_res.bin";
input bool                            InpUseCommonDir   = true;

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

  Print("🔄 Экспорт календаря:");
  Print("----------------------------------");
  Print("Параметры фильтрации:");
  Print(" 🟨 Интервал = ", InpDateFrom, " — ", InpDateTo);
  Print(" 🟨 Currencies = ", InpCurrencies, "\n 🟨 Event Codes = ", InpEventCodes, "\n 🟨 Min Importance = ", EnumToString(InpMinImportance));
  Print("----------------------------------");

//--- инициализация фильтра по валютам
  ArrayResize(currencies, 0);
  if(InpCurrencies == "")
    return;
  StringSplit(InpCurrencies, ',', currencies);
  currencies_size = ArraySize(currencies);
  for(int i = 0; i < currencies_size; i++)
    StringToUpper(currencies[i]);

//--- инициализация фильтра по кодам событий
  ArrayResize(event_codes, 0);
  if(InpEventCodes != "")
    StringSplit(InpEventCodes, ',', event_codes);
  event_codes_Size = ArraySize(event_codes);

//--- загрузка значений календаря с ФИЛЬТРОМ ПО ВАЛЮТЕ (если он задан)
  if(currencies_size > 0)
   {
    ArrayResize(values_size, currencies_size);
    ArrayFill(values_size, 0, currencies_size, 0);

    for(int i = 0; i < currencies_size; i++)
     {
      if(LoadCalendar(raw_values, InpDateFrom, InpDateTo, "", currencies[i]))
       {
        raw_values_size = ArraySize(raw_values);
        //--- загрузка событий с ФИЛЬТРОМ ПО ВАЖНОСТИ И КОДУ СОБЫТИЯ
        for(int k = 0; k < raw_values_size; k++)
         {
          //--- получим описание события
          if(CalendarEventById(raw_values[k].event_id, event))
           {
            //--- берем только индикаторы
            if(event.type != CALENDAR_TYPE_INDICATOR)
              continue;                                   // дальше фильтровать нечего

            //--- проверка по ВАЖНОСТИ
            if(event.importance < InpMinImportance)
              continue;                                   // дальше фильтровать нечего

            //--- проверка по КОДУ СОБЫТИЯ (если он задан)
            if(event_codes_Size > 0)
             {
              bool code_allowed = false;
              for(int c = 0; c < event_codes_Size; c++)
               {
                StringToUpper(event.event_code);
                if(StringFind(event.event_code, event_codes[c]) >= 0)
                 {
                  code_allowed = true;
                  break;
                 }
               }
              if(code_allowed == false)
                continue;
             }

            //--- пополним массив отфильтрованных событий
            int event_index = ArraySize(values);
            ArrayResize(values, event_index + 1);
            values[event_index] = raw_values[k];
            values_size[i]++;
           }
         }
        Print("✅ Получено значений ПО ВАЛЮТЕ \"", currencies[i], "\": ", raw_values_size, " → Из них отфильтровано: ", values_size[i]);
       }
      else
       {
        int error = GetLastError();
        if(error == 0)
          Print("⚠ LoadCalendar Info: No Events for ", currencies[i]);
        else
          Print("❌ LoadCalendar Error: ", error, " for ", currencies[i]);
        return;                                     // дальше фильтровать нечего
       }
     }
   }

Пояснения по приведенному коду:

  • В массиве values_size[] хранятся число отфильтрованных событий для каждого заданного во входных параметрах кода валюты.
  • Значению ноль для типа MQL5 datetime соответствует дата 1970.01.01 00:00:00.
  • Дополнительная фильтрация происходит по типу события. Фильтруются только события типа ENUM_CALENDAR_EVENT_TYPE::CALENDAR_TYPE_INDICATOR — это экономические события (не речи, а цыфры). Все остальные новости отсекаются. Это делается проверкой в самом начале фильтрации:
//--- берем только индикаторы
if(event.type != CALENDAR_TYPE_INDICATOR)
  continue;                                   // Дальше фильтровать нечего


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

Все возможные события по доллару США за всю доступную историю, высокой важности:

13:59:43.724    🔄 Экспорт календаря:
13:59:43.724    ----------------------------------
13:59:43.724    Параметры фильтрации:
13:59:43.724      🟨 Интервал = 1970.01.01 00:00:00 — 1970.01.01 00:00:00
13:59:43.724      🟨 Currencies = USD
13:59:43.724      🟨 Event Codes = 
13:59:43.724      🟨 Min Importance = CALENDAR_IMPORTANCE_HIGH
13:59:43.724    ----------------------------------
13:59:45.511    ✅ Получено значений ПО ВАЛЮТЕ "USD": 53346 → Из них отфильтровано: 9009
13:59:45.511    ----------------------------------
13:59:45.513    ✅ Saved: USD_calendar_test_res.bin   Size: 1153152 bytes  (9009 events)

Время получения массива отфильтрованных значений (структур MqlCalendarValue): 1.80 сек (0.20 мсек / на одну фильтрацию).


Все возможные события по доллару США, евро, японской йене —  за всю доступную историю, высокой важности:

14:03:08.724    🔄 Экспорт календаря:
14:03:08.724    ----------------------------------
14:03:08.724    Параметры фильтрации:
14:03:08.724      🟨 Интервал = 1970.01.01 00:00:00 — 1970.01.01 00:00:00
14:03:08.724      🟨 Currencies = USD,EUR,JPY
14:03:08.724      🟨 Event Codes = 
14:03:08.724      🟨 Min Importance = CALENDAR_IMPORTANCE_HIGH
14:03:08.724    ----------------------------------
14:03:10.488    ✅ Получено значений ПО ВАЛЮТЕ "USD": 53346 → Из них отфильтровано: 9009
14:03:10.947    ✅ Получено значений ПО ВАЛЮТЕ "EUR": 45139 → Из них отфильтровано: 1102
14:03:11.404    ✅ Получено значений ПО ВАЛЮТЕ "JPY": 18907 → Из них отфильтровано: 937
14:03:11.404    ----------------------------------
14:03:11.406    ✅ Saved: USD_calendar_test_res.bin   Size: 1153152 bytes  (9009 events)
14:03:11.407    ✅ Saved: EUR_calendar_test_res.bin   Size: 141056 bytes  (1102 events)
14:03:11.408    ✅ Saved: JPY_calendar_test_res.bin   Size: 119936 bytes  (937 events)

Время получения массива отфильтрованных значений (структур MqlCalendarValue): 2.68 сек (0.24 мсек / на одну фильтрацию).


События NFP («нонфармы»)  по доллару США за всю доступную историю, высокой важности:

14:07:22.203    🔄 Экспорт календаря:
14:07:22.203    ----------------------------------
14:07:22.203    Параметры фильтрации:
14:07:22.203      🟨 Интервал = 1970.01.01 00:00:00 — 1970.01.01 00:00:00
14:07:22.203      🟨 Currencies = USD
14:07:22.203      🟨 Event Codes = NONFARM
14:07:22.203      🟨 Min Importance = CALENDAR_IMPORTANCE_HIGH
14:07:22.203    ----------------------------------
14:07:22.290    ✅ Получено значений ПО ВАЛЮТЕ "USD": 53346 → Из них отфильтровано: 473
14:07:22.290    ----------------------------------
14:07:22.291    ✅ Saved: USD_calendar_test_res.bin   Size: 60544 bytes  (473 events)

Время получения массива отфильтрованных значений (структур MqlCalendarValue): 0.09 сек (0.18 мсек / на одну фильтрацию).

Замечания:

  1. Не обязательно задавать код события в фильтре полностью — достаточно задать уникальную часть строки с кодом. Например, для «нонфармов» полный код: «NONFARM-PAYROLLS» — достаточно задать «NONFARM».
  2. При работе торговой системы в реальном времени в терминале MetaTrader 5 вряд ли понадобится загрузка всех событий за всё время. Обычно загружают текущие события за сутки и на неделю вперед. Поэтому временем загрузки можно пренебречь в алгоритме новостного эксперта.


Шаг 2: Экспорт массива событий в бинарный файл

Функция SaveToBinary сохраняет отфильтрованные события в бинарный файл. Число файлов равно числу заданных во входных параметрах кодов валюты — каждая валюта экспортируется в свой бинарный файл. Причем имя файла начинается с префикса кода валюты. Например, «USD_calendar_test_res.bin». Так мы получаем взаимно однозначное соответствие между кодом валюты и списком событий для него.

//+------------------------------------------------------------------+
//| Сохранение массива в бинарный файл                               |
//+------------------------------------------------------------------+
void SaveToBinary(MqlCalendarValue &values[], const int &vls_size[], const string filename, const string &currencies[])
 {
  if(ArraySize(values) == 0)
   {
    Print("⚠️ Nothing to save");
    return;
   }

  Print("----------------------------------");
  int offset = 0;
  for(int i = 0; i < ArraySize(vls_size); i++)
   {
    int file_handle = FileOpen(currencies[i] + "_" + filename, FILE_WRITE | FILE_BIN | (InpUseCommonDir ? FILE_COMMON : 0));

    if(file_handle == INVALID_HANDLE)
     {
      Print("❌ FileSave failed: ", GetLastError());
      return;
     }

    FileWriteArray(file_handle, values, offset, vls_size[i]);
    FileFlush(file_handle);
    FileClose(file_handle);

    Print("✅ Saved: ", currencies[i] + "_" + filename, "   Size: ", vls_size[i] * sizeof(MqlCalendarValue), " bytes", "  (", vls_size[i], " events)");

    offset += vls_size[i];
   }
 }

После успешной отработки скрипта экспорта событий в бинарные файлы во вклвдке «Эксперты» MetaTrader 5 появятся сообщения вида:

14:28:42.077    ----------------------------------
14:28:42.078    ✅ Saved: USD_calendar_test_res.bin   Size: 58240 bytes  (455 events)
14:28:42.079    ✅ Saved: EUR_calendar_test_res.bin   Size: 7680 bytes  (60 events)


Шаг 3: Компиляция ресурса в советник

После запуска скрипта «ExportCalendarForTester.mq5» в папке «MQL5/Files/» появятся файлы «USD_calendar_test_res.bin» и «EUR_calendar_test_res.bin». Теперь их нужно «вшить» в советник. В начале файла советника (после #property директив) добавьте одну (если используется файл для одной валюты) или несколько строчек вида:

// Встраиваем бинарный файл как статический ресурс на этапе компиляции
#resource "\\Files\\USD_calendar_test_res.bin" as MqlCalendarValue USD_res_calendar_data[]
#resource "\\Files\\EUR_calendar_test_res.bin" as MqlCalendarValue EUR_res_calendar_data[]

Важно:

  1. Путь в #resource указывается относительно папки «MQL5/Files/». Префикс «\\Files\\» является обязательным.
  2. При компиляции советника во вкладке «Ошибки» MetaEditor должны появиться сообщения вида:
  • 'USD_calendar_test_res.bin' as 'const MqlCalendarValue USD_res_calendar_data[455]' 
  • 'EUR_calendar_test_res.bin' as 'const MqlCalendarValue EUR_res_calendar_data[60]' 

При старте советника в массивах USD_res_calendar_data[] и EUR_res_calendar_data[] будут находиться структуры сохраненных на этапе экспорта событий.


Интеграция в советник: автоматическое переключение режимов

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

//+------------------------------------------------------------------+
//| Инициализация: выбор источника данных                            |
//+------------------------------------------------------------------+
int OnInit()
 {
  Print("🔄 Инициализация новостного модуля:");

//--- инициализация фильтра по валютам
  ArrayResize(currencies, 0);
  if(InpCurrencies == "")
    return INIT_FAILED;
  StringSplit(InpCurrencies, ',', currencies);
  currencies_size = ArraySize(currencies);
  for(int i = 0; i < currencies_size; i++)
    StringToUpper(currencies[i]);

//--- инициализация фильтра по кодам событий
  ArrayResize(event_codes, 0);
  if(InpEventCodes != "")
    StringSplit(InpEventCodes, ',', event_codes);
  event_codes_Size = ArraySize(event_codes);

//--- определяем среду исполнения
  bool is_tester = MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_VISUAL_MODE);
  if(is_tester)
   {
    //--- РЕЖИМ ТЕСТЕРА: Загрузка из ресурса
    if(!LoadFromResource())
     {
      Print("❌ ERROR: Failed to load calendar from resource");
      return INIT_FAILED;
     }
    is_live_mode = false;
    Print("⚠️ Режим: TESTER (данные из ресурса)");
   }
  else
   {
    //--- РЕЖИМ LIVE: Загрузка из API
    if(!LoadFromCalendarAPI())
     {
      int error = GetLastError();
      Print("❌ ERROR: Failed to load calendar from API - ", error);
      return INIT_FAILED;
     }
    is_live_mode = true;

    Print("⚠️ Режим: LIVE (данные из API)");
   }

  return INIT_SUCCEEDED;
 }


Пояснения по приведенному коду:

Основная проверка делается в обработчике события инициализации эксперта OnInit():

   //--- определяем среду исполнения
   bool is_tester = MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_VISUAL_MODE);
   if(is_tester)
    {
      //--- РЕЖИМ ТЕСТЕРА: Загрузка из ресурса
      if(!LoadFromResource())
       {
        ...
       }
    }
   else
    {
      //--- РЕЖИМ LIVE: Загрузка из API
      if(!LoadFromCalendarAPI())
       {
        ...
       }
    }

Пояснения по приведенному коду:

Определяем в каком именно режиме находится выполняющаяся mql5-программа — в данном случае, наш эксперт. Нас интересуют флаги:

  • MQL_TESTER — признак работы запущенной программы в тестере;
  • MQL_OPTIMIZATION — признак работы запущенной программы в процессе оптимизации;
  • MQL_VISUAL_MODE — признак работы запущенной программы в визуальном режиме тестирования.
Если хотя бы один их этих флагов активен, то загружаем список событий из ресурса. В противном случае, загружаем список событий обычным образом с сервера через календарный API.


14:42:18.894    🔄 Инициализация новостного модуля:
14:42:18.905    ✅ Events from Server Loaded: 455 events for: USD
14:42:18.914    ✅ Events from Server Loaded: 60 events for: EUR
14:42:18.914    ⚠️ Режим: LIVE (данные из API)



Валидация цепочки преобразований данных: События → Бинарный файл → Компиляция в ресурс → Получение событий из ресурса

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

Для этого добавим вывод полученных из сервера/загруженных из ресурса событий в журнал терминала/тестера стратегий. Полный текстовый файл «ImportTesterLog.txt» с результатами прогона в тестере приложен к статье. Ниже приведен фрагмент лог-файла — начало и конец массива событий по каждому коду валюты.

Результат одиночного прогона в тестере стратегий, в визуальном режиме:

16:48:22.527    EURUSD,M5: testing of Experts\ImportCalendarValidation-EA.ex5 from 2026.01.01 00:00 to 2026.03.07 00:00 started with inputs:
16:48:22.527      InpCurrencies=USD,EUR
16:48:22.527      InpDateFrom=1735689600
16:48:22.527      InpDateTo=1767225600
16:48:22.527      InpEventCodes=
16:48:22.527      InpMinImportance=3
16:48:22.546   🔄 Инициализация новостного модуля:
16:48:22.546   ✅ Events from Resource Loaded: 455 events for: USD
16:48:22.546   ✅ Событие #0
16:48:22.546   ID:                    230215
16:48:22.546   ID события:      840140001
16:48:22.546   Время:              2025.01.02 16:30:00
16:48:22.546   Влияние:          CALENDAR_IMPACT_POSITIVE
16:48:22.546   Ревизия:           0
16:48:22.546   Факт:                211.0
16:48:22.546   Пересмотрено: 220.0
16:48:22.546   Прогноз:           219.0
16:48:22.546   Предыдущее:    219.0
16:48:22.546   ✅ Событие #1
16:48:22.546   ID:                     230641
16:48:22.546   ID события:       840500001
16:48:22.546   Время:              2025.01.02 17:45:00
16:48:22.546   Влияние:          CALENDAR_IMPACT_NEGATIVE
16:48:22.546   Ревизия:           3
16:48:22.546   Факт:                49.4
16:48:22.546   Прогноз:           50.5
16:48:22.546   Предыдущее:    49.7
 ...
16:48:22.553   ✅ Событие #453
16:48:22.553   ID:                     274419
16:48:22.553   ID события:       840510001
16:48:22.553   Время:              2025.12.30 17:45:00
16:48:22.553   Влияние:           CALENDAR_IMPACT_POSITIVE
16:48:22.553   Ревизия:            0
16:48:22.553   Факт:                 43.5
16:48:22.553   Прогноз:            42.4
16:48:22.553   Предыдущее:    36.3
16:48:22.553   ✅ Событие #454
16:48:22.553   ID:                     274484
16:48:22.553   ID события:       840140001
16:48:22.553   Время:              2025.12.31 16:30:00
16:48:22.553   Влияние:           CALENDAR_IMPACT_POSITIVE
16:48:22.553   Ревизия:            0
16:48:22.553   Факт:                 199.0
16:48:22.553   Пересмотрено:  215.0
16:48:22.553   Прогноз:            227.0
16:48:22.553   Предыдущее:     214.0
...
16:48:22.553   ✅ Events from Resource Loaded: 60 events for: EUR
16:48:22.553   ✅ Событие #0
16:48:22.553   ID:                    231322
16:48:22.553   ID события:      999030013
16:48:22.553   Время:             2025.01.07 13:00:00
16:48:22.553   Влияние:          CALENDAR_IMPACT_NA
16:48:22.553   Ревизия:          1
16:48:22.553   Факт:               2.4
16:48:22.553   Прогноз:          2.4
16:48:22.553   Предыдущее:   2.2
16:48:22.553   ✅ Событие #1
16:48:22.553   ID:                   187384
16:48:22.553   ID события:      999040007
16:48:22.553   Время:             2025.01.08 13:00:00
16:48:22.553   Влияние:          CALENDAR_IMPACT_POSITIVE
16:48:22.553   Ревизия:           0
16:48:22.553   Факт:                21.0
16:48:22.553   Пересмотрено: 17.8
16:48:22.553   Прогноз:           15.7
16:48:22.553   Предыдущее:    17.7
...
16:48:22.555   ✅ Событие #58
16:48:22.555   ID:                    204746
16:48:22.555   ID события:      999010006
16:48:22.555   Время:             2025.12.18 16:15:00
16:48:22.555   Влияние:          CALENDAR_IMPACT_NA
16:48:22.555   Ревизия:          0
16:48:22.555   Факт:             2.0
16:48:22.555   Предыдущее:       2.0
16:48:22.555   ✅ Событие #59
16:48:22.555   ID:                    204762
16:48:22.555   ID события:      999010007
16:48:22.555   Время:             2025.12.18 16:15:00
16:48:22.555   Влияние:          CALENDAR_IMPACT_NA
16:48:22.555   Ревизия:           0
16:48:22.555   Факт:                2.15
16:48:22.555   Предыдущее:    2.15
16:48:22.555   ⚠️ Режим: TESTER (данные из ресурса)

Видно, что все массивы событий загрузились из ресурса примерно за 10 мс, что не удивительно, — ресурс «вшит» в код советника и загружается вместе с ним при старте тестирования.

Такой же прогон (старт) эксперта в Live-режиме в терминале MetaTrader 5 дает аналогичный лог-файл со списком загруженных событий. Полный текстовый файл «LoadLiveLog.txt» с результатами Live-загрузки приложен к статье. Ниже приведен фрагмент лог-файла — начало и конец массива событий по каждому коду валюты.

Результат Live-загрузки событий из сервера:

17:27:25.250    🔄 Инициализация новостного модуля:
17:27:25.258    ✅ Получено значений ПО ВАЛЮТЕ "USD": 3589 → Из них отфильтровано: 455
17:27:25.258    ✅ Events from Server Loaded: 455 events for: USD
17:27:25.258    ✅ Событие #0
17:27:25.258    ID:                    230215
17:27:25.258    ID события:      840140001
17:27:25.258    Время:             2025.01.02 16:30:00
17:27:25.258    Влияние:          CALENDAR_IMPACT_POSITIVE
17:27:25.258    Ревизия:          0
17:27:25.258    Факт:               211.0
17:27:25.258    Пересмотрено: 220.0
17:27:25.258    Прогноз:           219.0
17:27:25.258    Предыдущее:    219.0
17:27:25.258    ✅ Событие #1
17:27:25.258    ID:                    230641
17:27:25.258    ID события:      840500001
17:27:25.258    Время:             2025.01.02 17:45:00
17:27:25.258    Влияние:          CALENDAR_IMPACT_NEGATIVE
17:27:25.258    Ревизия:          3
17:27:25.258    Факт:               49.4
17:27:25.258    Прогноз:          50.5
17:27:25.258    Предыдущее:   49.7
...
17:27:25.288    ✅ Событие #453
17:27:25.288    ID:                    274419
17:27:25.288    ID события:      840510001
17:27:25.288    Время:             2025.12.30 17:45:00
17:27:25.288    Влияние:          CALENDAR_IMPACT_POSITIVE
17:27:25.288    Ревизия:          0
17:27:25.288    Факт:               43.5
17:27:25.288    Прогноз:          42.4
17:27:25.288    Предыдущее:   36.3
17:27:25.288    ✅ Событие #454
17:27:25.288    ID:                    274484
17:27:25.288    ID события:      840140001
17:27:25.288    Время:             2025.12.31 16:30:00
17:27:25.288    Влияние:          CALENDAR_IMPACT_POSITIVE
17:27:25.288    Ревизия:          0
17:27:25.288    Факт:               199.0
17:27:25.288    Пересмотрено: 215.0
17:27:25.288    Прогноз:           227.0
17:27:25.288    Предыдущее:   214.0

17:27:25.301    ✅ Получено значений ПО ВАЛЮТЕ "EUR": 3116 → Из них отфильтровано: 0
17:27:25.301    ✅ Events from Server Loaded: 60 events for: EUR
17:27:25.301    ✅ Событие #0
17:27:25.301    ID:                    231322
17:27:25.301    ID события:      999030013
17:27:25.301    Время:             2025.01.07 13:00:00
17:27:25.301    Влияние:          CALENDAR_IMPACT_NA
17:27:25.301    Ревизия:          1
17:27:25.301    Факт:               2.4
17:27:25.301    Прогноз:          2.4
17:27:25.301    Предыдущее:   2.2
17:27:25.301    ✅ Событие #1
17:27:25.301    ID:                   187384
17:27:25.301    ID события:      999040007
17:27:25.301    Время:             2025.01.08 13:00:00
17:27:25.301    Влияние:          CALENDAR_IMPACT_POSITIVE
17:27:25.301    Ревизия:          0
17:27:25.301    Факт:               21.0
17:27:25.301    Пересмотрено:17.8
17:27:25.301    Прогноз:          15.7
17:27:25.301    Предыдущее:   17.7
...
17:27:25.303    ✅ Событие #58
17:27:25.303    ID:                   204746
17:27:25.303    ID события:      999010006
17:27:25.303    Время:             2025.12.18 16:15:00
17:27:25.303    Влияние:          CALENDAR_IMPACT_NA
17:27:25.303    Ревизия:          0
17:27:25.303    Факт:               2.0
17:27:25.303    Предыдущее:   2.0
17:27:25.303    ✅ Событие #59
17:27:25.303    ID:                   204762
17:27:25.303    ID события:      999010007
17:27:25.303    Время:             2025.12.18 16:15:00
17:27:25.303    Влияние:          CALENDAR_IMPACT_NA
17:27:25.303    Ревизия:          0
17:27:25.303    Факт:               2.15
17:27:25.303    Предыдущее:   2.15
17:27:25.303    ⚠️ Режим: LIVE (данные из API)

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

Разбор типичных ошибок и ложных ожиданий

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

1.«Календарь предсказывает движение»

Ложное ожидание:

Если выходит новость с важностью HIGH и факт сильно отличается от прогноза — цена гарантированно пойдёт в сторону отклонения. Достаточно купить при actual > forecast для валюты базы.

Реальность:

Экономический календарь — это источник данных, а не генератор торговых сигналов. Он сообщает что произошло, но не как рынок отреагирует. Почему цена может пойти против «очевидной» логики?

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

Правильный подход — используйте календарь как фильтр волатильности, а не как триггер входа:

//--- вместо
if(actual > forecast)
 OrderSend(...);

//--- используйте:
if(IsHighImpactNewsComingSoon(30)) 
 {
   //--- уменьшаем размер позиции или временно приостанавливаем торговлю
   ReduceRiskExposure();
 }
Золотое правило: Календарь отвечает на вопрос «Когда ждать повышенную волатильность?», а не «В какую сторону торговать?».


2.«Все HIGH-события одинаково важны»

Ложное ожидание:

Поле importance == CALENDAR_IMPORTANCE_HIGH означает, что событие гарантированно сдвинет рынок на 50+ пунктов. Можно торговать все такие новости одинаковым алгоритмом.

Реальность:

Конкретные значения в перечислении ENUM_CALENDAR_EVENT_IMPORTANCE — это субъективная оценка редакторов календаря, а не количественная метрика рыночного воздействия. Например, возьмём два события с меткой HIGH, но разной рыночной значимостью.

Первое событие — «Non-Farm Payrolls» (США) дает отклик по волатильности 80–150 пунктов, поскольку это ключевой индикатор занятости, влияет на политику ФРС. Второе событие — «Индекс деловой активности в строительстве» (Еврозона) дает отклик    10–30 пунктов, поскольку это узкосекторный индикатор, вторичен для ЕЦБ.

Правильный подход − дополните фильтрацию по важности списком приоритетных кодов событий:

//--- список событий, на которые действительно стоит реагировать
bool IsGoodEvent(const string event_code)
 {
   static const string tier1_codes[] = 
   {
      "NONFARM", "CPI", "GDP", "RATE", "FOMC", "ECB_RATE", 
      "RETAIL_SALES", "UNEMPLOYMENT", "PMI_MANUFACTURING"
   };
   
   for(int i = 0; i < ArraySize(tier1_codes); i++)
      if(StringFind(event_code, tier1_codes[i]) != -1)
         return true;
   return false;
 }

//--- использование в фильтре
if(event.importance == CALENDAR_IMPORTANCE_HIGH && IsGoodEvent(event.event_code))
 {
   //--- обрабатываем только действительно значимые новости
 }

Рекомендация:

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


3.«Использовали TimeLocal() вместо TimeTradeServer()»

Ложное ожидание:

Время события в структуре MqlCalendarValue::time указано в моём локальном часовом поясе (или в UTC), поэтому могу сравнивать его с TimeLocal() или TimeGMT().

Реальность:

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

Последствия этой ошибки серьёзны  — если ваш сервер в часовом поясе EET (UTC+2), а вы используете TimeLocal() (например, MSK, UTC+3), вы будете проверять события со сдвигом в 1 час. Робот может пропустить новость или, наоборот, среагировать на неё постфактум.

Правильный подход — всегда используйте TimeTradeServer() для всех сравнений времени:

//--- НЕПРАВИЛЬНО — риск рассинхронизации
datetime now = TimeLocal();
if(event.time - now < 1800)
 { ... }

//--- ПРАВИЛЬНО — гарантированная синхронизация
datetime now = TimeTradeServer();
if(event.time - now < 1800)
 { ... }

//--- в тестере TimeTradeServer() возвращает модельное время, поэтому логика работает идентично live

Рекомендация:

Добавьте в журнал отладки вывод TimeToString(TimeTradeServer(), TIME_MINUTES) и сравнивайте с временем события — они должны совпадать без конвертации.


4.«Забыли, что поля событий могут быть пустыми»

Ложное ожидание:

Поля actual_value, forecast_value и другие всегда содержат корректные числовые значения. Можно смело делить их на 1 000 000 и сравнивать.

Реальность:

Согласно документации, числовые поля структуры MqlCalendarValue хранят значения, умноженные на миллион или константу LONG_MIN, если значение не задано. Что происходит при игнорировании проверки:

//--- ошибка
double actual = values[i].actual_value / 1000000.0; // если actual_value == LONG_MIN, результат: -9223372036.854776

if(actual > forecast) // сравнение с "мусорным" числом вызовет ложный сигнал
  OpenBuy();

Правильный подход — используйте встроенные методы структуры MqlCalendarValue для безопасного получения значений:

//--- правильный подход - встроенные методы
if(values[i].HasActualValue() && values[i].HasForecastValue())
 {
  double actual = values[i].GetActualValue();
  double forecast = values[i].GetForecastValue();

  if(!MathIsNaN(actual) && !MathIsNaN(forecast))
   {
    double deviation = actual - forecast;
    // ... логика анализа
   }
 }
//+------------------------------------------------------------------+

Важно:

Функции GetActualValue(), GetForecastValue() и др. возвращают NaN при отсутствии данных. Всегда проверяйте результат через MathIsValidNumber() или MathIsNaN() перед использованием в расчётах.


5.«Вызов функций календарного API в OnTick()»

Ложное ожидание:

Чтобы всегда иметь актуальные данные, буду вызывать CalendarValueHistory() на каждом тике. Это гарантирует, что робот не пропустит свежую новость».

Реальность:

Функции календаря работают через удалённый сервер, к которому подключается MetaTrade 5, и имеют строгие лимиты частоты запросов. Вызов в OnTick() (который может срабатывать десятки раз в секунду) приведёт к проблемам:

  • Ошибке 5204 (ERR_CALENDAR_TOO_MANY_REQUESTS) — временная блокировка доступа к календарю.
  • Задержкам исполнения — сетевой запрос в торговом цикле увеличивает проскальзывание.
  • Избыточному трафику — загрузка одних и тех же данных многократно.
Правильный подход — используйте кэширование + инкрементальное обновление:
//--- глобальные переменные
MqlCalendarValue calendar_cache[];
bool cache_initialized = false;
long last_change_id = 0;

//+------------------------------------------------------------------+
int OnInit()
 {
//--- первоначальная загрузка истории (один раз при старте)
  datetime from = TimeCurrent() - 7 * 86400;
  datetime to = TimeCurrent() + 30 * 86400;

  if(CalendarValueHistory(calendar_cache, from, to, "USD"))
   {
    cache_initialized = true;
   }

//--- установка таймера для периодического обновления (не чаще 5-10 минут)
  EventSetTimer(300);
  return INIT_SUCCEEDED;
 }
//+------------------------------------------------------------------+
void OnTimer()
 {
  if(!cache_initialized)
    return;

  MqlCalendarValue updates[];

//--- запрашиваем только изменения с момента последнего обновления
  if(CalendarValueLast(last_change_id, updates, "USD") > 0)
   {
    if(ArraySize(updates) > 0)
     {
      //--- слияние новых данных с кэшем
      MergeUpdates(calendar_cache, updates);
      last_change_id = updates[ArraySize(updates) - 1].change_id;
     }
   }
 }
//+------------------------------------------------------------------+
void OnTick()
 {
//--- работа только с локальным кэшем — без сетевых задержек
  if(cache_initialized)
    ProcessNewsSignals(calendar_cache);
 }

Рекомендация:

OnTick() должен работать только с локальными данными. Сетевые запросы — только в OnInit(), OnTimer() или по событию пользователя.


6.«Забыли перекомпилировать эксперт для тестирования на новых событиях»

Ложное ожидание:

Я обновил файл ресурса «calendar_test_res.bin» в папке Files, теперь тестер автоматически увидит новые события. Перекомпиляция советника не нужна.

Реальность:

Директива #resource встраивает данные в исполняемый файл «.ex5» на этапе компиляции. Изменения во внешнем файле «.bin» не подхватываются динамически — советник продолжает использовать ту версию ресурса, которая была скомпилирована в момент сборки.

Симптомы ошибки:

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

Правильный подход — всегда перекомпилируйте советник после обновления ресурса:

//--- в начале файла советника
#resource "\\Files\\USD_calendar_test_res.bin" as MqlCalendarValue USD_res_calendar_data[]

//--- после обновления USD_calendar_test_res.bin:
// 1. Сохраните изменения в файле ресурса.
// 2. Нажмите F7 в MetaEditor (или Compile в меню).
// 3. Убедитесь, что в журнале компиляции нет ошибок.
// 4. Запустите тестирование заново.

Рекомендация:

Добавьте в журнал компиляции вывод версии ресурса (например, хэш или дату последнего обновления), чтобы визуально контролировать актуальность данных в «.ex5».

Замечание:

Ошибки, разобранные в этом разделе, возникают не из-за незнания синтаксиса MQL5, а из-за упрощённого представления о природе экономических данных (индикаторов). Тот, кто научится избегать этих шести ошибок, получит не просто работающий код, а устойчивую торговую систему, способную адаптироваться к изменениям рынка и требованиям экономических регуляторов. Именно такой подход отличает любителя от профессионала в алгоритмической торговле.


Заключение

Экономический календарь в терминале MetaTrader 5 — это мощный инструмент. Но как любой инструмент, он требует понимания контекста, дисциплины в использовании и учета ограничений платформы.

В статье показана конкретная схема реализации и использования новостного API для советника в MetaTrader 5. Статья даёт набор практических элементов, которые в совокупности превращают ручную новостную торговлю в воспроизводимый модуль:

  • понимание API календаря (Event vs Value, структуры MqlCalendarEvent/MqlCalendarValue, серверное время);
  • безопасные методы загрузки и обновления (CalendarValueHistory для первичной подгрузки, CalendarValueLast + change_id для инкрементальных обновлений);
  • обработка типовых ошибок и соблюдение лимитов запросов;
  • многоуровневая фильтрация по валюте, важности, коду события и временным окнам, чтобы снизить шум и оставить 3–5 значимых событий;
  • механизм экспорта отфильтрованных данных в бинарный ресурс и автоматическое переключение источника данных «реальное время» ↔ «тестер стратегий», обеспечивающее идентичность поведения в реальном времени и в бэктесте.

Критерии готовности модуля: успешно загружаются события для заданного периода; инкрементальные обновления работают через change_id без превышения лимитов; в тестере EA читает ресурс и выдаёт те же решения, что и в Live при идентичных входных данных.

Рекомендуемые следующие шаги: реализовать экспортёр, зашить .bin в #resource, проверить OnInit/OnTimer логику обновления и прогнать контролируемые бэктесты (например, сценарий «не торговать за 30 минут / возобновить через 60 минут»).

Это позволит вам перейти от гипотез к проверяемой, масштабируемой новостной торговой системе. Разработка новостных советников — это не просто программирование, это построение моста между макроэкономической теорией и рыночной практикой. 

Рекомендуемые материалы для углублённого изучения функций API календаря в MetaTrader 5 и их использования в новостной торговле:


Список файлов, приложенных к статье:

Название файла Описание
CalendarEventMonitor-EA.mq5  Файл, содержащий код тестового скрипта для проверки функции обновления данных календаря
ExportCalendarForTester-S.mq5 Файл, содержащий код тестового скрипта для проверки экспорта событий с заданными фильтрами в бинарный файл
GetTodayEvents-S.mq5 Файл, содержащий код тестового скрипта для проверки получения событий за текущие сутки
ImportCalendarValidation-EA.mq5 Файл, содержащий код тестового эксперта для проверки получения новостей в тестере стратегий из ресурса
ImportTesterLog.txt  Файл, содержащий результаты одиночного прогона эксперта «ImportCalendarValidation-EA.mq5» в тестере в визуальном режиме
LoadLiveLog.txt  Файл, содержащий результаты Live-загрузки событий в эксперте «ImportCalendarValidation-EA.mq5»
Греки опционов по Блэку — Шоулзу: Гамма и Дельта Греки опционов по Блэку — Шоулзу: Гамма и Дельта
Гамма и Дельта измеряют, как стоимость опциона реагирует на изменения цены базового актива. Дельта отражает скорость изменения цены опциона относительно базового актива, а Гамма измеряет, как сама Дельта изменяется по мере движения цены. Совместно они описывают направленную чувствительность и выпуклость опциона — критически важные параметры для динамического хеджирования и торговых стратегий, основанных на волатильности.
Разработка торговой стратегии на основе псевдокорреляции Пирсона Разработка торговой стратегии на основе псевдокорреляции Пирсона
Создание новых индикаторов на основе существующих - это мощный способ улучшить торговый анализ. Определив математическую функцию, которая интегрирует значения существующих индикаторов, трейдеры могут создавать гибридные индикаторы, объединяющие множество сигналов в единый эффективный инструмент. В данной статье представлен новый индикатор, созданный на основе трех осцилляторов с использованием модифицированной версии функции корреляции Пирсона, который мы называем Псевдокорреляцией Пирсона (PPC). Индикатор PPC предназначен для количественной оценки динамической корреляционной связи между осцилляторами и применения ее в рамках практической торговой стратегии.
Бимодальный Market Profile с дельтой и памятью в MQL5 Бимодальный Market Profile с дельтой и памятью в MQL5
Классический Market Profile сорокалетней давности до сих пор тиражируется в десятках индикаторов, которые отличаются только цветом баров. В статье я разбираю три концептуальные слепые зоны оригинальной теории — монолитную Value Area при бимодальных распределениях, слепоту TPO к агрессору и отсутствие памяти между сессиями — и строю индикатор, который закрывает каждую из них: детекция бимодальности с dead zone, ордер-флоу через CopyTicksRange с absorption detection, композитная память рынка с Naked POC и HVN/LVN. Полный исходный код прилагается.
Нейросети в трейдинге: Оценка риска по несогласованности представлений (ReGEN-TAD) Нейросети в трейдинге: Оценка риска по несогласованности представлений (ReGEN-TAD)
Статья раскрывает фреймворк ReGEN-TAD для оценки рыночного риска через несогласованность представлений, объединяющий генеративную проверку (реконструкция и прогноз) и ансамблевый Anomaly Score с факторной интерпретацией. Показана логика согласования параллельных представлений и их расхождений. На практике реализован первый шаг в MQL5 — свёрточный токенизатор, формирующий компактный эмбеддинг окна рынка для последующей диагностики режимов.