Трейдинг с экономическим календарем MQL5 (Часть 7): Подготовка к тестированию стратегий с анализом новостей
Введение
В этой статье мы продолжаем серию, посвященную Экономическому календарю MQL5. Мы подготовим торговую систему к тестированию стратегий в нерыночном (не live) режиме, добавив в нее данные экономических событий. Для основы мы будем использовать Часть 6, где мы реализовали автоматизацию входов в сделки с анализом новостей и таймерами обратного отсчета. Сегодня мы будем загружать новости из файла ресурсов, а также будет применять пользовательские фильтры для имитации реальных условий в тестере. Структура статьи включает следующие темы:
Приступим.
Важность интеграции статических данных
В среде MQL5 исторические данные экономических событий не хранятся длительное время, поэтому для разработки и тестирования стратегий на новостях статические данные нужно интегрировать. В отличие от реальной торговли, где платформа может получать новости в реальном времени, тестер не имеет доступа к динамическим обновлениям. Он не хранит огромные архивы прошлых событий, поэтому протестировать новостную стратегию стандартными средствами не получится. Однако мы можем загрузить данные из внешних источников и сохранить в нужном формате, например в файле или в виде добавляемых ресурсов. Так мы получим контроль над единым набором данных, который можно будет затем использовать в разных тестах и тестировать разные стратегии в одинаковых условиях.
Помимо обхода ограничений платформы, мы получим дополнительные преимущества, которые недоступны при работе с live-лентами. Экономический календарь содержит важные сведения о событии — дату и время событий, валюты, важность новости. Однако они не всегда сохраняются в формате, пригодном для алгоритмического анализа на длительных периодах. Мы можем структурировать эти данные вручную и адаптировать под собственные задачи. Например, фильтровать по конкретным валютам или важности, чтобы глубже анализировать влияние новостей на поведение рынка независимо от того, доступны они или нет.
Кроме того, такой подход повышает эффективность и автономность. Если у нас есть предварительно собранный набор статических данных, во время тестирования мы не зависим от интернет-соединения или сторонних сервисов. То есть у нас меньше переменных, которые могут исказить результаты. Это также дает возможность моделировать редкие или специфические сценарии, крупные экономические новости, сформировать наборы данных на годы или более точные по каким-то ключевым событиям. Это сложно воспроизвести в реальном времени или с ограниченным хранилищем платформы. Таким образом, мы переносим точность событий реального времени в историю, и нам доступно больше возможностей в тестере.
Полученные данные нужно как-то хранить, и MQL5 дает множество вариантов: текстовые форматы (txt), CSV, форматы American National Standards Institute (ANSI), бинарные (bin), Unicode, а также различные варианты организации баз данных.

Мы будем использовать не самый простой, но наиболее удобный формат — 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, как показано ниже.

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

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

Чтобы использовать данные в режиме тестера, нам нужно сохранить их в 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Нейросети в трейдинге: Потоковые модели с остаточной высокочастотной адаптацией (Окончание)
Знакомство с языком MQL5 (Часть 28): Освоение API и функции WebRequest в языке MQL5 (II)
Переосмысливаем классические стратегии (Часть 14): Высоковероятные ситуации
Разработка инструментария для анализа движения цен (Часть 20): Внешние библиотеки (IV) — Correlation Pathfinder
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования