English 中文 Deutsch 日本語
preview
Трейдинг с экономическим календарем MQL5 (Часть 7): Подготовка к тестированию стратегий с анализом новостей

Трейдинг с экономическим календарем MQL5 (Часть 7): Подготовка к тестированию стратегий с анализом новостей

MetaTrader 5Тестер |
76 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В этой статье мы продолжаем серию, посвященную Экономическому календарю MQL5. Мы подготовим торговую систему к тестированию стратегий в нерыночном (не live) режиме, добавив в нее данные экономических событий. Для основы мы будем использовать Часть 6, где мы реализовали автоматизацию входов в сделки с анализом новостей и таймерами обратного отсчета. Сегодня мы будем загружать новости из файла ресурсов, а также будет применять пользовательские фильтры для имитации реальных условий в тестере. Структура статьи включает следующие темы:

  1. Важность интеграции статических данных
  2. Реализация средствами MQL5
  3. Тестирование
  4. Заключение

Приступим.


Важность интеграции статических данных

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

Помимо обхода ограничений платформы, мы получим дополнительные преимущества, которые недоступны при работе с live-лентами. Экономический календарь содержит важные сведения о событии — дату и время событий, валюты, важность новости. Однако они не всегда сохраняются в формате, пригодном для алгоритмического анализа на длительных периодах. Мы можем структурировать эти данные вручную и адаптировать под собственные задачи. Например, фильтровать по конкретным валютам или важности, чтобы глубже анализировать влияние новостей на поведение рынка независимо от того, доступны они или нет.

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

Полученные данные нужно как-то хранить, и MQL5 дает множество вариантов: текстовые форматы (txt), CSV, форматы American National Standards Institute (ANSI), бинарные (bin), Unicode, а также различные варианты организации баз данных.

Форматы хранения данных в MQL5

Мы будем использовать не самый простой, но наиболее удобный формат — CSV. Данные всегда будут под рукой, и нам не придется тратить кучу времени на тестирование стратегии, что сэкономит массу времени и энергии. Поехали.


Реализация средствами MQL5

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

//+------------------------------------------------------------------+
//|                                    MQL5 NEWS CALENDAR PART 7.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://youtube.com/@ForexAlgo-Trader?"
#property version   "1.00"
#property strict

//---- Input parameter for start date of event filtering
input datetime StartDate = D'2025.03.01'; // Download Start Date
//---- Input parameter for end date of event filtering
input datetime EndDate = D'2025.03.21'; // Download End Date
//---- Input parameter to enable/disable time filtering
input bool ApplyTimeFilter = true;
//---- Input parameter for hours before event to consider
input int HoursBefore = 4;
//---- Input parameter for minutes before event to consider
input int MinutesBefore = 10;
//---- Input parameter for hours after event to consider
input int HoursAfter = 1;
//---- Input parameter for minutes after event to consider
input int MinutesAfter = 5;
//---- Input parameter to enable/disable currency filtering
input bool ApplyCurrencyFilter = true;
//---- Input parameter defining currencies to filter (comma-separated)
input string CurrencyFilter = "USD,EUR,GBP,JPY,AUD,NZD,CAD,CHF"; // All 8 major currencies
//---- Input parameter to enable/disable impact filtering
input bool ApplyImpactFilter = true;

//---- Enumeration for event importance filtering options
enum ENUM_IMPORTANCE {
   IMP_NONE = 0,                  // None
   IMP_LOW,                       // Low
   IMP_MEDIUM,                    // Medium
   IMP_HIGH,                      // High
   IMP_NONE_LOW,                  // None,Low
   IMP_NONE_MEDIUM,               // None,Medium
   IMP_NONE_HIGH,                 // None,High
   IMP_LOW_MEDIUM,                // Low,Medium
   IMP_LOW_HIGH,                  // Low,High
   IMP_MEDIUM_HIGH,               // Medium,High
   IMP_NONE_LOW_MEDIUM,           // None,Low,Medium
   IMP_NONE_LOW_HIGH,             // None,Low,High
   IMP_NONE_MEDIUM_HIGH,          // None,Medium,High
   IMP_LOW_MEDIUM_HIGH,           // Low,Medium,High
   IMP_ALL                        // None,Low,Medium,High (default)
};
//---- Input parameter for selecting importance filter
input ENUM_IMPORTANCE ImportanceFilter = IMP_ALL; // Impact Levels (Default to all)

Здесь мы настраиваем базовые входные параметры и перечисление, позволяющее кастомизировать способ обработки экономических событий торговой системой при тестировании стратегий. Определяем начальную и конечную даты (StartDate и EndDate) как переменные типа datetime. У нас они установлены в значения 1 марта 2025 года и 21 марта 2025 года — это диапазон для загрузки и анализа данных событий. Нам нужен временной фильтр событий, поэтому добавляем переменную bool ApplyTimeFilter со значением true по умолчанию, а также HoursBefore (4 часа), MinutesBefore (10 минут), HoursAfter (1 час) и MinutesAfter (5 минут), которые определяют временное окно учета событий относительно конкретного бара.

Для фильтра по валютам добавим переменную ApplyCurrencyFilter (по умолчанию true) и CurrencyFilter — значение string, содержащее все восемь основных валют: "USD, EUR, GBP, JPY, AUD, NZD, CAD, CHF", которые можно будет выбирать для тестирования. Также у нас будет фильтрацию по важности — параметр ApplyImpactFilter в true и перечисление ENUM_IMPORTANCE со значениями IMP_NONE, IMP_LOW, IMP_MEDIUM, IMP_HIGH и их комбинации до IMP_ALL, при этом ImportanceFilter по умолчанию установлен в IMP_ALL, чтобы включить события с любым уровнем важности. Результат показан ниже.

Вид входных параметров

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

Дефолтный формат календаря в MQL5

Код для получения этого формата показан ниже.

//---- Structure to hold economic event data
struct EconomicEvent {
   string eventDate;      //---- Date of the event
   string eventTime;      //---- Time of the event
   string currency;       //---- Currency affected by the event
   string event;          //---- Event description
   string importance;     //---- Importance level of the event
   double actual;         //---- Actual value of the event
   double forecast;       //---- Forecasted value of the event
   double previous;       //---- Previous value of the event
};

//---- Array to store all economic events
EconomicEvent allEvents[];
//---- Array for currency filter values
string curr_filter[];
//---- Array for importance filter values
string imp_filter[];

Сначала определяем структуру EconomicEvent (struct), которая инкапсулирует данные события, включая eventDate и eventTime в виде строковых данных для параметров времени, currency для идентификации рынка, event для описания и importance для важности новости, а также actual, forecast и previous типа double для хранения числовых результатов события.

Для хранения и обработки этих событий создаем три массива: allEvents — массив структур EconomicEvent для хранения всех загруженных событий, curr_filter — строковый массив для хранения валют, указанных во входном параметре CurrencyFilter, и imp_filter — строковый массив для управления уровнями важности, выбранными через ImportanceFilter. Это повторяет стандартную структуру, за исключением того, что раздел Period мы смещаем в начало структуры для хранения дат событий. Далее нам необходимо получить фильтры от пользователя, интерпретировать их в понятной форме и инициализировать. Чтобы сохранить модульность кода, будем использовать функции.

//---- Function to initialize currency and impact filters
void InitializeFilters() {
   //---- Currency Filter Section
   //---- Check if currency filter is enabled and has content
   if (ApplyCurrencyFilter && StringLen(CurrencyFilter) > 0) {
      //---- Split the currency filter string into array
      int count = StringSplit(CurrencyFilter, ',', curr_filter);
      //---- Loop through each currency filter entry
      for (int i = 0; i < ArraySize(curr_filter); i++) {
         //---- Temporary variable for trimming
         string temp = curr_filter[i];
         //---- Remove leading whitespace
         StringTrimLeft(temp);
         //---- Remove trailing whitespace
         StringTrimRight(temp);
         //---- Assign trimmed value back to array
         curr_filter[i] = temp;
         //---- Print currency filter for debugging
         Print("Currency filter [", i, "]: '", curr_filter[i], "'");
      }
   } else if (ApplyCurrencyFilter) {
      //---- Warn if currency filter is enabled but empty
      Print("Warning: CurrencyFilter is empty, no currency filtering applied");
      //---- Resize array to zero if no filter applied
      ArrayResize(curr_filter, 0);
   }
}

Здесь мы настраиваем часть фильтрации по валютам в функции InitializeFilters, это позволит использовать фильтры при тестировании. Начинаем с проверки, установлена ли переменная ApplyCurrencyFilter в значение true и содержит ли строка CurrencyFilter данные - для этого используем функцию StringLen. Если данные есть, разбиваем строку CurrencyFilter (там значения через запятую, например, "USD, EUR, GBP"), на массив curr_filter с помощью функции StringSplit. Количество элементов сохраним в переменной count.

Затем мы проходим по каждому элементу массива curr_filter в цикле for, присваивая его временному string-значению temp, очищаем от начальных и конечных пробелов с помощью функций StringTrimLeft и StringTrimRight, после чего обновляем значение в curr_filter и выводим его через функцию Print для целей отладки (например, "Currency filter [0]: 'USD'"). Однако, если ApplyCurrencyFilter включен, но CurrencyFilter пустой, мы используем функцию Print для вывода предупреждения "Warning: CurrencyFilter is empty, no currency filtering applied". При этом изменяем размер массива до нуля с помощью функции ArrayResize, фактически отключая фильтрацию. С такой аккуратной инициализацией валютный фильтр будет формироваться корректно на основе указанных пользователем данных. Для фильтра по уровню важности применяем аналогичную логику.

//---- Impact Filter Section (using enum)
//---- Check if impact filter is enabled
if (ApplyImpactFilter) {
   //---- Switch based on selected importance filter
   switch (ImportanceFilter) {
      case IMP_NONE:
         //---- Resize array for single importance level
         ArrayResize(imp_filter, 1);
         //---- Set importance to "None"
         imp_filter[0] = "None";
         break;
      case IMP_LOW:
         //---- Resize array for single importance level
         ArrayResize(imp_filter, 1);
         //---- Set importance to "Low"
         imp_filter[0] = "Low";
         break;
      case IMP_MEDIUM:
         //---- Resize array for single importance level
         ArrayResize(imp_filter, 1);
         //---- Set importance to "Medium"
         imp_filter[0] = "Medium";
         break;
      case IMP_HIGH:
         //---- Resize array for single importance level
         ArrayResize(imp_filter, 1);
         //---- Set importance to "High"
         imp_filter[0] = "High";
         break;
      case IMP_NONE_LOW:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Low";
         break;
      case IMP_NONE_MEDIUM:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Medium";
         break;
      case IMP_NONE_HIGH:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "High";
         break;
      case IMP_LOW_MEDIUM:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "Low";
         //---- Set second importance level
         imp_filter[1] = "Medium";
         break;
      case IMP_LOW_HIGH:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "Low";
         //---- Set second importance level
         imp_filter[1] = "High";
         break;
      case IMP_MEDIUM_HIGH:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "Medium";
         //---- Set second importance level
         imp_filter[1] = "High";
         break;
      case IMP_NONE_LOW_MEDIUM:
         //---- Resize array for three importance levels
         ArrayResize(imp_filter, 3);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Low";
         //---- Set third importance level
         imp_filter[2] = "Medium";
         break;
      case IMP_NONE_LOW_HIGH:
         //---- Resize array for three importance levels
         ArrayResize(imp_filter, 3);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Low";
         //---- Set third importance level
         imp_filter[2] = "High";
         break;
      case IMP_NONE_MEDIUM_HIGH:
         //---- Resize array for three importance levels
         ArrayResize(imp_filter, 3);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Medium";
         //---- Set third importance level
         imp_filter[2] = "High";
         break;
      case IMP_LOW_MEDIUM_HIGH:
         //---- Resize array for three importance levels
         ArrayResize(imp_filter, 3);
         //---- Set first importance level
         imp_filter[0] = "Low";
         //---- Set second importance level
         imp_filter[1] = "Medium";
         //---- Set third importance level
         imp_filter[2] = "High";
         break;
      case IMP_ALL:
         //---- Resize array for all importance levels
         ArrayResize(imp_filter, 4);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Low";
         //---- Set third importance level
         imp_filter[2] = "Medium";
         //---- Set fourth importance level
         imp_filter[3] = "High";
         break;
   }
   //---- Loop through impact filter array to print values
   for (int i = 0; i < ArraySize(imp_filter); i++) {
      //---- Print each impact filter value
      Print("Impact filter [", i, "]: '", imp_filter[i], "'");
   }
} else {
   //---- Notify if impact filter is disabled
   Print("Impact filter disabled");
   //---- Resize impact filter array to zero
   ArrayResize(imp_filter, 0);
}

Для фильтра по важности начинаем с проверки, установлена ли переменная ApplyImpactFilter в true. Если да, используем оператор switch на основе перечисления ImportanceFilter, чтобы определить, какие уровни важности включить в массив imp_filter. Для одного выбранного уровня ("IMP_NONE", "IMP_LOW", "IMP_MEDIUM" или "IMP_HIGH") меняем размер "imp_filter" до 1 с помощью функции ArrayResize и присваиваем соответствующее значение string (например, "imp_filter[0] = 'None'"). Для вариантов с двумя уровнями (например, "IMP_NONE_LOW" или "IMP_MEDIUM_HIGH") меняем размер до 2 и указываем 2 значения (например, "imp_filter[0] = 'None', imp_filter[1] = 'Low'"). Для трех выбранных уровней ("IMP_LOW_MEDIUM_HIGH") меняем размер до 3, а для значения "IMP_ALL" — до 4, то есть включаем все "None", "Low", "Medium" и "High".

После настройки массива проходим в цикле по imp_filter, используя функцию ArraySize для определения его размера, и выводим каждое значение через Print для отладки (например, "Impact filter [0]: 'None'"). Если ApplyImpactFilter равно false, выводим другое сообщение с помощью функции Print: "Impact filter disabled", а также меняем размер массива imp_filter до нуля.

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

int OnInit() {
   //---- Initialize filters
   InitializeFilters();

   //---- Return successful initialization
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
   //---- Print termination reason
   Print("EA terminated, reason: ", reason);
}

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

Инициализация фильтров

Как видим, мы корректно инициализировали и прочитали входные параметры фильтров и сохранили их. Теперь нам осталось получить данные из live-потока и сохранить их. Логика здесь следующая: сначала необходимо один раз запустить программу в live-режиме, чтобы она загрузила данные из базы Экономического календаря MQL5, а затем использовать эти данные в режиме тестирования. Ниже показана логика инициализации.

//---- Check if not running in tester mode
if (!MQLInfoInteger(MQL_TESTER)) {
   //---- Validate date range
   if (StartDate >= EndDate) {
      //---- Print error for invalid date range
      Print("Error: StartDate (", TimeToString(StartDate), ") must be earlier than EndDate (", TimeToString(EndDate), ")");
      //---- Return initialization failure
      return(INIT_PARAMETERS_INCORRECT);
   }

   //---- Array to hold calendar values
   MqlCalendarValue values[];
   //---- Fetch calendar data for date range
   if (!CalendarValueHistory(values, StartDate, EndDate)) {
      //---- Print error if calendar data fetch fails
      Print("Error fetching calendar data: ", GetLastError());
      //---- Return initialization failure
      return(INIT_FAILED);
   }

   //---- Array to hold economic events
   EconomicEvent events[];
   //---- Counter for events
   int eventCount = 0;

   //---- Loop through calendar values
   for (int i = 0; i < ArraySize(values); i++) {
      //---- Structure for event details
      MqlCalendarEvent eventDetails;
      //---- Fetch event details by ID
      if (!CalendarEventById(values[i].event_id, eventDetails)) continue;

      //---- Structure for country details
      MqlCalendarCountry countryDetails;
      //---- Fetch country details by ID
      if (!CalendarCountryById(eventDetails.country_id, countryDetails)) continue;

      //---- Structure for value details
      MqlCalendarValue value;
      //---- Fetch value details by ID
      if (!CalendarValueById(values[i].id, value)) continue;

      //---- Resize events array for new event
      ArrayResize(events, eventCount + 1);
      //---- Convert event time to string
      string dateTimeStr = TimeToString(values[i].time, TIME_DATE | TIME_MINUTES);
      //---- Extract date from datetime string
      events[eventCount].eventDate = StringSubstr(dateTimeStr, 0, 10);
      //---- Extract time from datetime string
      events[eventCount].eventTime = StringSubstr(dateTimeStr, 11, 5);
      //---- Assign currency from country details
      events[eventCount].currency = countryDetails.currency;
      //---- Assign event name
      events[eventCount].event = eventDetails.name;
      //---- Map importance level from enum to string
      events[eventCount].importance = (eventDetails.importance == 0) ? "None" :    // CALENDAR_IMPORTANCE_NONE
                                      (eventDetails.importance == 1) ? "Low" :     // CALENDAR_IMPORTANCE_LOW
                                      (eventDetails.importance == 2) ? "Medium" :  // CALENDAR_IMPORTANCE_MODERATE
                                      "High";                                      // CALENDAR_IMPORTANCE_HIGH
      //---- Assign actual value
      events[eventCount].actual = value.GetActualValue();
      //---- Assign forecast value
      events[eventCount].forecast = value.GetForecastValue();
      //---- Assign previous value
      events[eventCount].previous = value.GetPreviousValue();
      //---- Increment event count
      eventCount++;
   }

}

Здесь мы обрабатываем получение данных в live-режиме внутри функции OnInit, чтобы собрать данные экономических событий для последующего использования при тестировании. Мы начинаем с проверки, не находится что система в режиме тестера. Для этого используем функцию MQLInfoInteger с параметром MQL_TESTER. Если это так, проверяем, что StartDate меньше EndDate, и если нет - выводим ошибку и возвращаем INIT_PARAMETERS_INCORRECT. Далее мы объявляем массив MqlCalendarValue с именем values и получаем данные календаря между StartDate и EndDate с помощью функции CalendarValueHistory/ Если произошла ошибка, выводим ее с помощью GetLastError и возвращаем INIT_FAILED.

Затем мы инициализируем массив EconomicEvent с именем events и переменную eventCount типа integer. Она будет отслеживать количество событий в цикле по массиву values с помощью функции ArraySize. На каждой итерации мы получаем детали события в структуру MqlCalendarEvent eventDetails с помощью функции CalendarEventById, данные страны в структуру countryDetails из MqlCalendarCountry через CalendarCountryById, а данные значения — в структуру MqlCalendarValue value через CalendarValueById. Если произошла ошибка, пропускаем итерацию. Изменяем размер массива events с помощью функции ArrayResize, преобразуем время события в строку dateTimeStr с помощью функции TimeToString, извлекаем eventDate и eventTime с помощью функции StringSubstr, присваиваем currency из countryDetails, event из eventDetails.name, а также сопоставляем числовые значения важности со строками ("None", "Low", "Medium", "High"). В завершение задаем значения actual, forecast и previous из структуры value и увеличиваем eventCount. Так формируется полный набор данных событий для обработки в live-режиме. Теперь нам нужна функция для сохранения этой информации в файл.

//---- Function to write events to a CSV file
void WriteToCSV(string fileName, EconomicEvent &events[]) {
   //---- Open file for writing in CSV format
   int handle = FileOpen(fileName, FILE_WRITE | FILE_CSV, ',');
   //---- Check if file opening failed
   if (handle == INVALID_HANDLE) {
      //---- Print error message with last error code
      Print("Error creating file: ", GetLastError());
      //---- Exit function on failure
      return;
   }

   //---- Write CSV header row
   FileWrite(handle, "Date", "Time", "Currency", "Event", "Importance", "Actual", "Forecast", "Previous");
   //---- Loop through all events to write to file
   for (int i = 0; i < ArraySize(events); i++) {
      //---- Write event data to CSV file
      FileWrite(handle, events[i].eventDate, events[i].eventTime, events[i].currency, events[i].event,
                events[i].importance, DoubleToString(events[i].actual, 2), DoubleToString(events[i].forecast, 2),
                DoubleToString(events[i].previous, 2));
      //---- Print event details for debugging
      Print("Writing event ", i, ": ", events[i].eventDate, ", ", events[i].eventTime, ", ", events[i].currency, ", ",
            events[i].event, ", ", events[i].importance, ", ", DoubleToString(events[i].actual, 2), ", ",
            DoubleToString(events[i].forecast, 2), ", ", DoubleToString(events[i].previous, 2));
   }

   //---- Flush data to file
   FileFlush(handle);
   //---- Close the file handle
   FileClose(handle);
   //---- Print confirmation of data written
   Print("Data written to ", fileName, " with ", ArraySize(events), " events.");

   //---- Verify written file by reading it back
   int verifyHandle = FileOpen(fileName, FILE_READ | FILE_TXT);
   //---- Check if verification file opening succeeded
   if (verifyHandle != INVALID_HANDLE) {
      //---- Read entire file content
      string content = FileReadString(verifyHandle, (int)FileSize(verifyHandle));
      //---- Print file content for verification
      Print("File content after writing (size: ", FileSize(verifyHandle), " bytes):\n", content);
      //---- Close verification file handle
      FileClose(verifyHandle);
   }
}

Здесь мы создаем функцию WriteToCSV для экспорта данных экономических событий в CSV-файл. Сначала открываем файл, указанный в fileName, с помощью функции FileOpen в режиме FILE_WRITE | FILE_CSV с разделителем запятой, сохраняя результат в переменной handle. Если операция завершается неудачей и handle равен INVALID_HANDLE, функция Print выводит сообщение об ошибке с указанием GetLastError и выходим из функции с помощью return. Если файл открыт успешно, записываем строку заголовков с помощью функции FileWrite, определяем столбцы "Date", "Time", "Currency", "Event", "Importance", "Actual", "Forecast" и "Previous" для данных.

Затем проходим по массиву events, определяем его размер с помощью функции ArraySize, и для каждого события вызываем FileWrite, записывая его свойства — eventDate, eventTime, currency, event, importance, а также числовые значения actual, forecast и previous, преобразованные в строки с помощью функции DoubleToString (с форматированием до двух знаков после запятой), — одновременно выводя эти данные через Print для целей отладки.

После завершения цикла записываем все данные в файл с помощью функции FileFlush для handle, затем закрываем файл с помощью FileClose и подтверждаем успешное выполнение операции сообщением.

Для проверки результата повторно открываем файл в режиме чтения с помощью FILE_READ | FILE_TXT и сохраняем хендл в verifyHandle. Если операция успешна, мы считываем все содержимое в строку content с помощью функции FileReadString, используя размер в байтах, полученный через FileSize, выводим его для проверки (например, "File content after writing (size: X bytes):\n" content"), и закрываем файл. Так мы сохраняем нужные данные и можем проверить их, чтобы иметь надежные ресурсы для тестирования стратегии. Теперь можно использовать эту функцию для сохранения данных.

//---- Define file path for CSV
string fileName = "Database\\EconomicCalendar.csv";

//---- Check if file exists and print appropriate message
if (!FileExists(fileName)) Print("Creating new file: ", fileName);
else Print("Overwriting existing file: ", fileName);

//---- Write events to CSV file
WriteToCSV(fileName, events);
//---- Print instructions for tester mode
Print("Live mode: Data written. To use in tester, manually add ", fileName, " as a resource and recompile.");

В завершение обработки данных в live-режиме задаем fileName равным "Database\EconomicCalendar.csv" и используем пользовательскую функцию FileExists для проверки его наличия. Затем мы вызываем функцию WriteToCSV с параметрами fileName и events для сохранения данных и выводим инструкцию через Print — "Live mode: Data written. To use in tester, add "fileName" as a resource and recompile." — для использования в тестере. Ниже приведён фрагмент кода пользовательской функции для проверки существования файла.

//---- Function to check if a file exists
bool FileExists(string fileName) {
   //---- Open file in read mode to check existence
   int handle = FileOpen(fileName, FILE_READ | FILE_CSV);
   //---- Check if file opened successfully
   if (handle != INVALID_HANDLE) {
      //---- Close the file handle
      FileClose(handle);
      //---- Return true if file exists
      return true;
   }
   //---- Return false if file doesn't exist
   return false;
}

В функции FileExists для проверки наличия файла при тестировании стратегии мы открываем fileName с помощью функции FileOpen в режиме FILE_READ | FILE_CSV; если handle не равен INVALID_HANDLE, мы закрываем его через FileClose и возвращаем true, в противном случае возвращаем false. Это подтверждает статус файла для обработки данных. После запуска в live-режиме мы получаем следующий результат.

Источник данных в режиме live

Как видим на картинке, данные сохранены и доступны.

Доступ к данным

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

//---- Define resource file for economic calendar data
#resource "\\Files\\Database\\EconomicCalendar.csv" as string EconomicCalendarData

Здесь мы интегрируем статический ресурс данных в программу для поддержки тестирования стратегий. С помощью директивы #resource встраиваем файл, расположенный по пути "\Files\Database\EconomicCalendar.csv", и привязываем его к строковой переменной EconomicCalendarData. Файл становится частью исполняемого модуля, и нам не нужно переживать, если вдруг он потеряется. Далее создадим функцию для загрузки содержимого файла.

//---- Function to load events from resource file
bool LoadEventsFromResource() {
   //---- Get data from resource
   string fileData = EconomicCalendarData;
   //---- Print raw resource content for debugging
   Print("Raw resource content (size: ", StringLen(fileData), " bytes):\n", fileData);

   //---- Array to hold lines from resource
   string lines[];
   //---- Split resource data into lines
   int lineCount = StringSplit(fileData, '\n', lines);
   //---- Check if resource has valid data
   if (lineCount <= 1) {
      //---- Print error if no data lines found
      Print("Error: No data lines found in resource! Raw data: ", fileData);
      //---- Return false on failure
      return false;
   }

   //---- Reset events array
   ArrayResize(allEvents, 0);
   //---- Index for event array
   int eventIndex = 0;

   //---- Loop through each line (skip header at i=0)
   for (int i = 1; i < lineCount; i++) {
      //---- Check for empty lines
      if (StringLen(lines[i]) == 0) {
         //---- Print message for skipped empty line
         Print("Skipping empty line ", i);
         //---- Skip to next iteration
         continue;
      }

      //---- Array to hold fields from each line
      string fields[];
      //---- Split line into fields
      int fieldCount = StringSplit(lines[i], ',', fields);
      //---- Print line details for debugging
      Print("Line ", i, ": ", lines[i], " (field count: ", fieldCount, ")");

      //---- Check if line has minimum required fields
      if (fieldCount < 8) {
         //---- Print error for malformed line
         Print("Malformed line ", i, ": ", lines[i], " (field count: ", fieldCount, ")");
         //---- Skip to next iteration
         continue;
      }

      //---- Extract date from field
      string dateStr = fields[0];
      //---- Extract time from field
      string timeStr = fields[1];
      //---- Extract currency from field
      string currency = fields[2];
      //---- Extract event description (handle commas in event name)
      string event = fields[3];
      //---- Combine multiple fields if event name contains commas
      for (int j = 4; j < fieldCount - 4; j++) {
         event += "," + fields[j];
      }
      //---- Extract importance from field
      string importance = fields[fieldCount - 4];
      //---- Extract actual value from field
      string actualStr = fields[fieldCount - 3];
      //---- Extract forecast value from field
      string forecastStr = fields[fieldCount - 2];
      //---- Extract previous value from field
      string previousStr = fields[fieldCount - 1];

      //---- Convert date and time to datetime format
      datetime eventDateTime = StringToTime(dateStr + " " + timeStr);
      //---- Check if datetime conversion failed
      if (eventDateTime == 0) {
         //---- Print error for invalid datetime
         Print("Error: Invalid datetime conversion for line ", i, ": ", dateStr, " ", timeStr);
         //---- Skip to next iteration
         continue;
      }

      //---- Resize events array for new event
      ArrayResize(allEvents, eventIndex + 1);
      //---- Assign event date
      allEvents[eventIndex].eventDate = dateStr;
      //---- Assign event time
      allEvents[eventIndex].eventTime = timeStr;
      //---- Assign event currency
      allEvents[eventIndex].currency = currency;
      //---- Assign event description
      allEvents[eventIndex].event = event;
      //---- Assign event importance
      allEvents[eventIndex].importance = importance;
      //---- Convert and assign actual value
      allEvents[eventIndex].actual = StringToDouble(actualStr);
      //---- Convert and assign forecast value
      allEvents[eventIndex].forecast = StringToDouble(forecastStr);
      //---- Convert and assign previous value
      allEvents[eventIndex].previous = StringToDouble(previousStr);
      //---- Print loaded event details
      Print("Loaded event ", eventIndex, ": ", dateStr, " ", timeStr, ", ", currency, ", ", event);
      //---- Increment event index
      eventIndex++;
   }

   //---- Print total events loaded
   Print("Loaded ", eventIndex, " events from resource into array.");
   //---- Return success if events were loaded
   return eventIndex > 0;
}

Определяем функцию LoadEventsFromResource для заполнения данных экономических событий из встроенного ресурса для теста. Мы присваиваем ресурс EconomicCalendarData переменной fileData и выводим его сырое содержимое с помощью функции Print, включая его размер, полученный через StringLen, для отладки. Разбиваем fileData на массив lines с помощью функции StringSplit, используя символ новой строки в качестве разделителя, сохраняем количество строк в lineCount. Если lineCount меньше или равен 1, выводим ошибку и возвращаем false. Устанавливаем массив allEvents в значение ноль с помощью ArrayResize и инициализируем eventIndex значением 0, после чего проходим по массиву lines, начиная с индекса 1 (пропускаем заголовок). Для каждой строки проверяем, пуста ли она, с помощью StringLen, и если да - выводим сообщение о пропуске и переходим к следующей итерации при необходимости. Если все хорошо, разбиваем строку на массив fields по запятым.

Если fieldCount меньше 8, выводим ошибку и пропускаем строку; иначе извлекаем dateStr, timeStr и currency, формируем строку event путем конкатенации полей (учитывая возможные запятые в тексте события) в цикле, затем получаем importance, actualStr, forecastStr и previousStr. Преобразуем dateStr и timeStr в eventDateTime с помощью функции StringToTime, пропуская запись с ошибкой, если преобразование не удалось, затем изменяем размер массива allEvents через ArrayResize, присваиваем все значения, при этом преобразуем числовые строки с помощью StringToDouble, выводим событие и увеличиваем eventIndex. В конце мы выводим общее количество загруженных событий eventIndex и возвращаем true, если данные были загружены. Теперь мы можем вызывать эту функцию при инициализации в режиме тестера.

else {
   //---- Check if resource data is empty in tester mode
   if (StringLen(EconomicCalendarData) == 0) {
      //---- Print error for empty resource
      Print("Error: Resource EconomicCalendarData is empty. Please run in live mode, add the file as a resource, and recompile.");
      //---- Return initialization failure
      return(INIT_FAILED);
   }
   //---- Print message for tester mode
   Print("Running in Strategy Tester, using embedded resource: Database\\EconomicCalendar.csv");

   //---- Load events from resource
   if (!LoadEventsFromResource()) {
      //---- Print error if loading fails
      Print("Failed to load events from resource.");
      //---- Return initialization failure
      return(INIT_FAILED);
   }
}

Здесь, если EconomicCalendarData пуста согласно StringLen, выводим ошибку и возвращаем INIT_FAILED; в противном случае выводим сообщение о режиме тестера через Print и вызываем LoadEventsFromResource. Если произошла ошибка, возвращаем INIT_FAILED. Так мы проверяем корректную загрузку данных событий для бэктестинга. Вот результат.

Загруженные данные в режиме тестера

На картинке видно, что данные успешно загружены. Искажения данных и пропуск пустых строк мы также корректно обработали. Теперь можем перейти к обработчику события OnTick и симулировать обработку данных так, как если бы мы находились в live-режиме. Здесь будем обрабатывать данные по барам, а не на каждом тике.

//---- Variable to track last bar time
datetime lastBarTime = 0;

//---- Tick event handler
void OnTick() {
   //---- Get current bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   //---- Check if bar time has changed
   if (currentBarTime != lastBarTime) {
      //---- Update last bar time
      lastBarTime = currentBarTime;

      //----
   }
}

Мы определяем lastBarTime как переменную типа datetime, инициализированную значением 0, для отслеживания времени предыдущего бара. В функции OnTick получаем время текущего бара с помощью функции iTime с параметрами _Symbol, _Period и индекс бара 0, сохраняем его в currentBarTime. Если currentBarTime отличается от lastBarTime, обновляем lastBarTime, чтобы система реагировала только на формирование нового бара для обработки событий. Далее определим функцию для обработки данных симуляции в формате, аналогичном тому, который мы использовали в предыдущей версии, как показано ниже.

//---- Function to filter and print economic events
void FilterAndPrintEvents(datetime barTime) {
   //---- Get total number of events
   int totalEvents = ArraySize(allEvents);
   //---- Print total events considered
   Print("Total considered data size: ", totalEvents, " events");

   //---- Check if there are events to filter
   if (totalEvents == 0) {
      //---- Print message if no events loaded
      Print("No events loaded to filter.");
      //---- Exit function
      return;
   }

   //---- Array to store filtered events
   EconomicEvent filteredEvents[];
   //---- Counter for filtered events
   int filteredCount = 0;

   //---- Variables for time range
   datetime timeBefore, timeAfter;
   //---- Apply time filter if enabled
   if (ApplyTimeFilter) {
      //---- Structure for bar time
      MqlDateTime barStruct;
      //---- Convert bar time to structure
      TimeToStruct(barTime, barStruct);

      //---- Calculate time before event
      MqlDateTime timeBeforeStruct = barStruct;
      //---- Subtract hours before
      timeBeforeStruct.hour -= HoursBefore;
      //---- Subtract minutes before
      timeBeforeStruct.min -= MinutesBefore;
      //---- Adjust for negative minutes
      if (timeBeforeStruct.min < 0) {
         timeBeforeStruct.min += 60;
         timeBeforeStruct.hour -= 1;
      }
      //---- Adjust for negative hours
      if (timeBeforeStruct.hour < 0) {
         timeBeforeStruct.hour += 24;
         timeBeforeStruct.day -= 1;
      }
      //---- Convert structure to datetime
      timeBefore = StructToTime(timeBeforeStruct);

      //---- Calculate time after event
      MqlDateTime timeAfterStruct = barStruct;
      //---- Add hours after
      timeAfterStruct.hour += HoursAfter;
      //---- Add minutes after
      timeAfterStruct.min += MinutesAfter;
      //---- Adjust for minutes overflow
      if (timeAfterStruct.min >= 60) {
         timeAfterStruct.min -= 60;
         timeAfterStruct.hour += 1;
      }
      //---- Adjust for hours overflow
      if (timeAfterStruct.hour >= 24) {
         timeAfterStruct.hour -= 24;
         timeAfterStruct.day += 1;
      }
      //---- Convert structure to datetime
      timeAfter = StructToTime(timeAfterStruct);

      //---- Print time range for debugging
      Print("Bar time: ", TimeToString(barTime), ", Time range: ", TimeToString(timeBefore), " to ", TimeToString(timeAfter));
   } else {
      //---- Print message if no time filter applied
      Print("Bar time: ", TimeToString(barTime), ", No time filter applied, using StartDate to EndDate only.");
      //---- Set time range to date inputs
      timeBefore = StartDate;
      timeAfter = EndDate;
   }

   //---- Loop through all events for filtering
   for (int i = 0; i < totalEvents; i++) {
      //---- Convert event date and time to datetime
      datetime eventDateTime = StringToTime(allEvents[i].eventDate + " " + allEvents[i].eventTime);
      //---- Check if event is within date range
      bool inDateRange = (eventDateTime >= StartDate && eventDateTime <= EndDate);
      //---- Skip if not in date range
      if (!inDateRange) continue;

      //---- Time Filter Check
      //---- Check if event is within time range if filter applied
      bool timeMatch = !ApplyTimeFilter || (eventDateTime >= timeBefore && eventDateTime <= timeAfter);
      //---- Skip if time doesn't match
      if (!timeMatch) continue;
      //---- Print event details if time passes
      Print("Event ", i, ": Time passes (", allEvents[i].eventDate, " ", allEvents[i].eventTime, ") - ",
            "Currency: ", allEvents[i].currency, ", Event: ", allEvents[i].event, ", Importance: ", allEvents[i].importance,
            ", Actual: ", DoubleToString(allEvents[i].actual, 2), ", Forecast: ", DoubleToString(allEvents[i].forecast, 2),
            ", Previous: ", DoubleToString(allEvents[i].previous, 2));

      //---- Currency Filter Check
      //---- Default to match if filter disabled
      bool currencyMatch = !ApplyCurrencyFilter;
      //---- Apply currency filter if enabled
      if (ApplyCurrencyFilter && ArraySize(curr_filter) > 0) {
         //---- Initially set to no match
         currencyMatch = false;
         //---- Check each currency in filter
         for (int j = 0; j < ArraySize(curr_filter); j++) {
            //---- Check if event currency matches filter
            if (allEvents[i].currency == curr_filter[j]) {
               //---- Set match to true if found
               currencyMatch = true;
               //---- Exit loop on match
               break;
            }
         }
         //---- Skip if currency doesn't match
         if (!currencyMatch) continue;
      }
      //---- Print event details if currency passes
      Print("Event ", i, ": Currency passes (", allEvents[i].currency, ") - ",
            "Date: ", allEvents[i].eventDate, " ", allEvents[i].eventTime,
            ", Event: ", allEvents[i].event, ", Importance: ", allEvents[i].importance,
            ", Actual: ", DoubleToString(allEvents[i].actual, 2), ", Forecast: ", DoubleToString(allEvents[i].forecast, 2),
            ", Previous: ", DoubleToString(allEvents[i].previous, 2));

      //---- Impact Filter Check
      //---- Default to match if filter disabled
      bool impactMatch = !ApplyImpactFilter;
      //---- Apply impact filter if enabled
      if (ApplyImpactFilter && ArraySize(imp_filter) > 0) {
         //---- Initially set to no match
         impactMatch = false;
         //---- Check each importance in filter
         for (int k = 0; k < ArraySize(imp_filter); k++) {
            //---- Check if event importance matches filter
            if (allEvents[i].importance == imp_filter[k]) {
               //---- Set match to true if found
               impactMatch = true;
               //---- Exit loop on match
               break;
            }
         }
         //---- Skip if importance doesn't match
         if (!impactMatch) continue;
      }
      //---- Print event details if impact passes
      Print("Event ", i, ": Impact passes (", allEvents[i].importance, ") - ",
            "Date: ", allEvents[i].eventDate, " ", allEvents[i].eventTime,
            ", Currency: ", allEvents[i].currency, ", Event: ", allEvents[i].event,
            ", Actual: ", DoubleToString(allEvents[i].actual, 2), ", Forecast: ", DoubleToString(allEvents[i].forecast, 2),
            ", Previous: ", DoubleToString(allEvents[i].previous, 2));

      //---- Add event to filtered array
      ArrayResize(filteredEvents, filteredCount + 1);
      //---- Assign event to filtered array
      filteredEvents[filteredCount] = allEvents[i];
      //---- Increment filtered count
      filteredCount++;
   }

   //---- Print summary of filtered events
   Print("After ", (ApplyTimeFilter ? "time filter" : "date range filter"),
         ApplyCurrencyFilter ? " and currency filter" : "",
         ApplyImpactFilter ? " and impact filter" : "",
         ": ", filteredCount, " events remaining.");

   //---- Check if there are filtered events to print
   if (filteredCount > 0) {
      //---- Print header for filtered events
      Print("Filtered Events at Bar Time: ", TimeToString(barTime));
      //---- Print filtered events array
      ArrayPrint(filteredEvents, 2, " | ");
   } else {
      //---- Print message if no events found
      Print("No events found within the specified range.");
   }
}

Используем функцию FilterAndPrintEvents для фильтрации и отображения экономических событий по заданному бару. Мы начинаем с вычисления totalEvents с помощью функции ArraySize для массива allEvents и выводим это значение. Если оно равно нулю, мы выходим из функции с помощью return. Инициализируем массив filteredEvents типа EconomicEvent и переменную filteredCount значением 0, затем определяем timeBefore и timeAfter для фильтра по времени. Если ApplyTimeFilter равно true, мы преобразуем barTime в структуру barStruct с помощью функции TimeToStruct, корректируем timeBeforeStruct, вычитая HoursBefore и MinutesBefore (исправляем отрицательные значения), и timeAfterStruct, добавляем HoursAfter и MinutesAfter (исправляем переполнения), затем преобразуем обе структуры в тип datetime с помощью функции StructToTime и выводим диапазон. В противном случае устанавливаем timeBefore и timeAfter равными StartDate и EndDate соответственно и выводим сообщение об отсутствии фильтрации по времени.

Проходим по массиву allEvents в цикле до totalEvents, преобразуем eventDate и eventTime каждого события в eventDateTime с помощью функции StringToTime, одновременно проверяем, попадает ли оно в диапазон StartDate–EndDate для условия inDateRange. Если нет - пропускаем событие. Для фильтрации по времени проверяем условие timeMatch с учетом ApplyTimeFilter и заданного диапазона и выводим детали, если они совпадают. Для валют мы определяем currencyMatch на основе ApplyCurrencyFilter и массива curr_filter, используя ArraySize и цикл, и выводим сообщение если они совпали. Для важности определяем impactMatch с ApplyImpactFilter и массивом imp_filter, также выводим сообщение при совпадении. Совпавшие события добавляются в массив filteredEvents с помощью функции ArrayResize, а filteredCount увеличивается.

В завершение мы выводим информацию и, если filteredCount больше нуля, выводим отфильтрованный список с помощью ArrayPrint. В противном случае выводим сообщение об отсутствии событий. Затем вызываем эту функцию в обработчике тиков.

void OnTick() {
   //---- Get current bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   //---- Check if bar time has changed
   if (currentBarTime != lastBarTime) {
      //---- Update last bar time
      lastBarTime = currentBarTime;
      //---- Filter and print events for current bar
      FilterAndPrintEvents(currentBarTime);
   }
}

После запуска программы мы получили следующий результат.

Итоговый анализ

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


Тестирование

Я показал весь процесс тестирования на видео:



Заключение

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

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

Прикрепленные файлы |
Нейросети в трейдинге: Потоковые модели с остаточной высокочастотной адаптацией (Окончание) Нейросети в трейдинге: Потоковые модели с остаточной высокочастотной адаптацией (Окончание)
Мы завершаем практическую интеграцию ResFlow в MQL5 через объект верхнего уровня CNeuronResFlow. Он объединяет LTR на базе EVA-Flow и HTR, формирует контекст и карты признаков, синхронизирует временные масштабы и реализует прямой и обратный проход с OpenCL. Тестирование на исторических данных EURUSD H1 показало согласованность потоков и выявило риски внутрисделочных просадок. Материал поможет собрать, обучить и проверить модель в MetaTrader 5.
Знакомство с языком MQL5 (Часть 28): Освоение API и функции WebRequest в языке MQL5 (II) Знакомство с языком MQL5 (Часть 28): Освоение API и функции WebRequest в языке MQL5 (II)
В этой статье вы научитесь получать ценовые данные с внешних платформ с помощью API и функции WebRequest на языке MQL5. Вы узнаете, как структурируются URL, как форматируются ответы API, как преобразовать серверные данные в читаемые строки, а также как находить конкретные значения в ответах JSON и получать их оттуда.
Переосмысливаем классические стратегии (Часть 14): Высоковероятные ситуации Переосмысливаем классические стратегии (Часть 14): Высоковероятные ситуации
В трейдерском сообществе хорошо известны торговые стратегии с высокой вероятностью успеха, но, к сожалению, они недостаточно четко определены. В этой статье мы попытаемся найти эмпирический и алгоритмический способы точного определения того, что представляет собой ситуация с высокой вероятностью успеха (high probability setup), а также выявить и использовать такие ситуации. Применяя деревья градиентного бустинга (Gradient Boosting Trees), мы продемонстрируем, как читатель может улучшить производительность произвольной торговой стратегии и более четко и понятно донести до компьютера точную задачу, которую необходимо выполнить.
Разработка инструментария для анализа движения цен (Часть 20): Внешние библиотеки (IV) — Correlation Pathfinder Разработка инструментария для анализа движения цен (Часть 20): Внешние библиотеки (IV) — Correlation Pathfinder
Correlation Pathfinder предлагает новый подход к пониманию динамики валютных пар в рамках серии инструментов для анализа ценового действия. Этот инструмент автоматизирует сбор и анализ данных, предоставляя информацию о взаимодействии таких валютных пар, как EURUSD и GBPUSD. Практическая информация в реальном времени поможет вам более эффективно управлять рисками и выявлять торговые возможности.