English 中文 Deutsch 日本語
preview
Трейдинг с экономическим календарем MQL5 (Часть 8): Оптимизируем тестирование новостных стратегий с помощью фильтров и логов

Трейдинг с экономическим календарем MQL5 (Часть 8): Оптимизируем тестирование новостных стратегий с помощью фильтров и логов

MetaTrader 5Трейдинг |
63 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

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

Теперь давайте рассмотрим все улучшения.


Визуальный хронограф для торговли на новостях в реальном времени и в офлайне

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

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

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

Дизайн визуализации для офлайн-режима


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

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

//---- Include trading library
#include <Trade\Trade.mqh>
CTrade trade;

//---- Define resource for CSV
#resource "\\Files\\Database\\EconomicCalendar.csv" as string EconomicCalendarData

Начнем с интеграции торговой библиотеки. Именно она обеспечивает выполнение сделок как при реальной торговле и при тестировании в офлайне. С помощью директивы #include <Trade\Trade.mqh> мы подключаем торговую библиотеку MQL5, в которой содержится класс CTrade для управления торговыми операциями. Объявляем объект CTrade с именем trade. Теперь приложение сможет программно выполнять операции покупки и продажи.

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

//---- Event name tracking
string current_eventNames_data[];
string previous_eventNames_data[];
string last_dashboard_eventNames[]; // Added: Cache for last dashboard event names in tester mode
datetime last_dashboard_update = 0; // Added: Track last dashboard update time in tester mode

//---- Filter flags
bool enableCurrencyFilter = true;
bool enableImportanceFilter = true;
bool enableTimeFilter = true;
bool isDashboardUpdate = true;
bool filters_changed = true;        // Added: Flag to detect filter changes in tester mode

//---- Event counters
int totalEvents_Considered = 0;
int totalEvents_Filtered = 0;
int totalEvents_Displayable = 0;

//---- Input parameters (PART 6)
sinput group "General Calendar Settings"
input ENUM_TIMEFRAMES start_time = PERIOD_H12;
input ENUM_TIMEFRAMES end_time = PERIOD_H12;
input ENUM_TIMEFRAMES range_time = PERIOD_H8;
input bool updateServerTime = true; // Enable/Disable Server Time Update in Panel
input bool debugLogging = false;    // Added: Enable debug logging in tester mode

//---- Input parameters for tester mode (from PART 7, minimal)
sinput group "Strategy Tester CSV Settings"
input datetime StartDate = D'2025.03.01'; // Download Start Date
input datetime EndDate = D'2025.03.21';   // Download End Date

//---- Structure for CSV events (from PART 7)
struct EconomicEvent {
   string eventDate;       // Date of the event
   string eventTime;       // Time of the event
   string currency;        // Currency affected
   string event;           // Event description
   string importance;      // Importance level
   double actual;          // Actual value
   double forecast;        // Forecast value
   double previous;        // Previous value
   datetime eventDateTime; // Added: Store precomputed datetime for efficiency
};

//---- Global array for tester mode events
EconomicEvent allEvents[];
EconomicEvent filteredEvents[]; // Added: Filtered events for tester mode optimization

//---- Trade settings
enum ETradeMode {
   TRADE_BEFORE,
   TRADE_AFTER,
   NO_TRADE,
   PAUSE_TRADING
};
input ETradeMode tradeMode = TRADE_BEFORE;
input int tradeOffsetHours = 12;
input int tradeOffsetMinutes = 5;
input int tradeOffsetSeconds = 0;
input double tradeLotSize = 0.01;

//---- Trade control
bool tradeExecuted = false;
datetime tradedNewsTime = 0;
int triggeredNewsEvents[];

Здесь мы сохраняем названия событий в current_eventNames_data, previous_eventNames_data и last_dashboard_eventNames, используя last_dashboard_eventNames для кэширования обновлений панели в режиме тестера, а также last_dashboard_update — для обновления только при необходимости, чтобы сократить избыточную обработку.

Мы управляем фильтрацией событий с помощью enableCurrencyFilter, enableImportanceFilter, enableTimeFilter и filters_changed, сбрасывая фильтры при значении true у filters_changed, чтобы обрабатывать только релевантные события. Параметр debugLogging в группе sinput group "General Calendar Settings" используется для логирования только сделок и обновлений.

Период тестирования задается переменными StartDate и EndDate в группе sinput group "Strategy Tester CSV Settings". Структура EconomicEvent включает поле eventDateTime для быстрого доступа ко времени события. Массив allEvents фильтруется в filteredEvents для более быстрой обработки, при этом tradeMode и связанные переменные используются для выполнения сделок. Это позволяет выбирать период тестирования, в рамках которого загружаются данные, и использовать тот же временной диапазон для моделирования. Ниже представлен пользовательский интерфейс.

Интерфейс пользовательских параметров

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

//+------------------------------------------------------------------+
//| Filter events for tester mode                                    | // Added: Function to pre-filter events by date range
//+------------------------------------------------------------------+
void FilterEventsForTester() {
   ArrayResize(filteredEvents, 0);
   int eventIndex = 0;
   for (int i = 0; i < ArraySize(allEvents); i++) {
      datetime eventDateTime = allEvents[i].eventDateTime;
      if (eventDateTime < StartDate || eventDateTime > EndDate) {
         if (debugLogging) Print("Event ", allEvents[i].event, " skipped in filter due to date range: ", TimeToString(eventDateTime)); // Modified: Conditional logging
         continue;
      }
      ArrayResize(filteredEvents, eventIndex + 1);
      filteredEvents[eventIndex] = allEvents[i];
      eventIndex++;
   }
   if (debugLogging) Print("Tester mode: Filtered ", eventIndex, " events."); // Modified: Conditional logging
   filters_changed = false;
}

Здесь мы реализуем интеллектуальную фильтрацию событий для ускорения тестирования на истории за счет сокращения количества новостей обрабатываемых в тестере. Функция FilterEventsForTester очищает массив filteredEvents с помощью функции ArrayResize и заново формирует его из релевантных событий массива allEvents. Для каждого события мы проверяем время eventDateTime относительно дат StartDate и EndDate и пропускаем события вне заданного диапазона. При этом такие пропуски будем записывать в журнал только при debugLogging == true с использованием функции Print, чтобы журнал не засорялся лишней информацией.

Подходящие события копируются в filteredEvents по индексу eventIndex, который увеличивается при каждом добавлении, при этом используем функцию ArrayResize для динамического выделения памяти. Общее количество событий eventIndex выводится через Print только если включен параметр debugLogging. Переменную filters_changed устанавливаем в значение false для индикации завершения фильтрации. Напомню, мы осознанно сокращаем набор событий, чтобы ускорить последующую обработку и эффективную визуализацию новостей в офлайн-режиме. Эту функцию мы вызываем в обработчике события OnInit, чтобы предварительно отфильтровать новостные данные.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   //---- Create dashboard UI
   createRecLabel(MAIN_REC,50,50,740,410,clrSeaGreen,1);
   createRecLabel(SUB_REC1,50+3,50+30,740-3-3,410-30-3,clrWhite,1);
   createRecLabel(SUB_REC2,50+3+5,50+30+50+27,740-3-3-5-5,410-30-3-50-27-10,clrGreen,1);
   createLabel(HEADER_LABEL,50+3+5,50+5,"MQL5 Economic Calendar",clrWhite,15);

   //---- Create calendar buttons
   int startX = 59;
   for (int i = 0; i < ArraySize(array_calendar); i++) {
      createButton(ARRAY_CALENDAR+IntegerToString(i),startX,132,buttons[i],25,
                   array_calendar[i],clrWhite,13,clrGreen,clrNONE,"Calibri Bold");
      startX += buttons[i]+3;
   }

   //---- Initialize for live mode (unchanged)
   int totalNews = 0;
   bool isNews = false;
   MqlCalendarValue values[];
   datetime startTime = TimeTradeServer() - PeriodSeconds(start_time);
   datetime endTime = TimeTradeServer() + PeriodSeconds(end_time);
   string country_code = "US";
   string currency_base = SymbolInfoString(_Symbol,SYMBOL_CURRENCY_BASE);
   int allValues = CalendarValueHistory(values,startTime,endTime,NULL,NULL);

   //---- Load CSV events for tester mode
   if (MQLInfoInteger(MQL_TESTER)) {
      if (!LoadEventsFromResource()) {
         Print("Failed to load events from CSV resource.");
         return(INIT_FAILED);
      }
      Print("Tester mode: Loaded ", ArraySize(allEvents), " events from CSV.");
      FilterEventsForTester(); // Added: Pre-filter events for tester mode
   }

   //---- Create UI elements
   createLabel(TIME_LABEL,70,85,"Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)+
               "   |||   Total News: "+IntegerToString(allValues),clrBlack,14,"Times new roman bold");
   createLabel(IMPACT_LABEL,70,105,"Impact: ",clrBlack,14,"Times new roman bold");
   createLabel(FILTER_LABEL,370,55,"Filters:",clrYellow,16,"Impact");

   //---- Create filter buttons
   string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency";
   color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed;
   bool filter_curr_state = enableCurrencyFilter;
   createButton(FILTER_CURR_BTN,430,55,110,26,filter_curr_text,filter_curr_txt_color,12,clrBlack);
   ObjectSetInteger(0,FILTER_CURR_BTN,OBJPROP_STATE,filter_curr_state);

   string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance";
   color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed;
   bool filter_imp_state = enableImportanceFilter;
   createButton(FILTER_IMP_BTN,430+110,55,120,26,filter_imp_text,filter_imp_txt_color,12,clrBlack);
   ObjectSetInteger(0,FILTER_IMP_BTN,OBJPROP_STATE,filter_imp_state);

   string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time";
   color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed;
   bool filter_time_state = enableTimeFilter;
   createButton(FILTER_TIME_BTN,430+110+120,55,70,26,filter_time_text,filter_time_txt_color,12,clrBlack);
   ObjectSetInteger(0,FILTER_TIME_BTN,OBJPROP_STATE,filter_time_state);

   createButton(CANCEL_BTN,430+110+120+79,51,50,30,"X",clrWhite,17,clrRed,clrNONE);

   //---- Create impact buttons
   int impact_size = 100;
   for (int i = 0; i < ArraySize(impact_labels); i++) {
      color impact_color = clrBlack, label_color = clrBlack;
      if (impact_labels[i] == "None") label_color = clrWhite;
      else if (impact_labels[i] == "Low") impact_color = clrYellow;
      else if (impact_labels[i] == "Medium") impact_color = clrOrange;
      else if (impact_labels[i] == "High") impact_color = clrRed;
      createButton(IMPACT_LABEL+string(i),140+impact_size*i,105,impact_size,25,
                   impact_labels[i],label_color,12,impact_color,clrBlack);
   }

   //---- Create currency buttons
   int curr_size = 51, button_height = 22, spacing_x = 0, spacing_y = 3, max_columns = 4;
   for (int i = 0; i < ArraySize(curr_filter); i++) {
      int row = i / max_columns;
      int col = i % max_columns;
      int x_pos = 575 + col * (curr_size + spacing_x);
      int y_pos = 83 + row * (button_height + spacing_y);
      createButton(CURRENCY_BTNS+IntegerToString(i),x_pos,y_pos,curr_size,button_height,curr_filter[i],clrBlack);
   }

   //---- Initialize filters
   if (enableCurrencyFilter) {
      ArrayFree(curr_filter_selected);
      ArrayCopy(curr_filter_selected, curr_filter);
      Print("CURRENCY FILTER ENABLED");
      ArrayPrint(curr_filter_selected);
      for (int i = 0; i < ArraySize(curr_filter_selected); i++) {
         ObjectSetInteger(0, CURRENCY_BTNS+IntegerToString(i), OBJPROP_STATE, true);
      }
   }

   if (enableImportanceFilter) {
      ArrayFree(imp_filter_selected);
      ArrayCopy(imp_filter_selected, allowed_importance_levels);
      ArrayFree(impact_filter_selected);
      ArrayCopy(impact_filter_selected, impact_labels);
      Print("IMPORTANCE FILTER ENABLED");
      ArrayPrint(imp_filter_selected);
      ArrayPrint(impact_filter_selected);
      for (int i = 0; i < ArraySize(imp_filter_selected); i++) {
         string btn_name = IMPACT_LABEL+string(i);
         ObjectSetInteger(0, btn_name, OBJPROP_STATE, true);
         ObjectSetInteger(0, btn_name, OBJPROP_BORDER_COLOR, clrNONE);
      }
   }

   //---- Update dashboard
   update_dashboard_values(curr_filter_selected, imp_filter_selected);
   ChartRedraw(0);
   return(INIT_SUCCEEDED);
}

Мы используем функцию createRecLabel для построения панелей дашборда MAIN_REC, SUB_REC1 и SUB_REC2 с различными цветами и размерами. Также используем функцию createLabel для добавления HEADER_LABEL с текстом "MQL5 Economic Calendar", как и раньше. Кнопки календаря динамически создаются из array_calendar с использованием функций createButton и ArraySize. Для размещения на графике используем startX и buttons.

Для режима реальной торговли мы получаем события через функцию CalendarValueHistory в массив values, используя startTime и endTime, вычисленные через TimeTradeServer и PeriodSeconds. В режиме тестера используем функцию MQLInfoInteger, чтобы проверить, действительно ли в режиме тестирования MQL_TESTER и загружаем данные экономического календаря EconomicCalendarData через функцию LoadEventsFromResource в массив allEvents. Далее у нас идет практически самая важная функция FilterEventsForTester — она фильтрует события и заполняет массив filteredEvents.

Добавляем элементы интерфейса: с помощью функции createLabel добавляем метки TIME_LABEL (время), IMPACT_LABEL (важность) и FILTER_LABEL (фильтр). Также строим кнопки фильтрации FILTER_CURR_BTN, FILTER_IMP_BTN, FILTER_TIME_BTN и CANCEL_BTN с помощью функции createButton и ObjectSetInteger. В них задается состояние фильтрации filter_curr_state на основе значения из enableCurrencyFilter. Кнопки по важности и валютам создаются на основе impact_labels и curr_filter с использованием функции createButton. Также мы инициализируем фильтры curr_filter_selected и imp_filter_selected через ArrayFree и ArrayCopy. Дашборд обновляем функцией update_dashboard_values и перерисовываем на графике ChartRedraw, после чего возвращается INIT_SUCCEEDED если инициализация прошла успешно. После инициализации программы получаем следующий результат.

Результат инициализации

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   UpdateFilterInfo();
   CheckForNewsTrade();
   if (isDashboardUpdate) {
      if (MQLInfoInteger(MQL_TESTER)) {
         datetime currentTime = TimeTradeServer();
         datetime timeRange = PeriodSeconds(range_time);
         datetime timeAfter = currentTime + timeRange;
         if (filters_changed || last_dashboard_update < timeAfter) { // Modified: Update on filter change or time range shift
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            ArrayFree(last_dashboard_eventNames);
            ArrayCopy(last_dashboard_eventNames, current_eventNames_data);
            last_dashboard_update = currentTime;
         }
      } else {
         update_dashboard_values(curr_filter_selected, imp_filter_selected);
      }
   }
}

В OnTick используем функцию UpdateFilterInfo для обновления настроек фильтрации и функцию CheckForNewsTrade для оценки и исполнения сделок на основе событий. Если isDashboardUpdate установлен в значение true, проверяем значение MQL_TESTER с помощью функции MQLInfoInteger, чтобы запустить логику, добавленную для работы в тестере. Также вычисляем currentTime с помощью TimeTradeServer, timeRange с помощью PeriodSeconds на основе range_time, и timeAfter как сумму currentTime и timeRange.

В режиме тестера используем условие filters_changed или last_dashboard_update меньше, чем timeAfter. Это условие вызывает функцию update_dashboard_values с параметрами curr_filter_selected и imp_filter_selected. При этом массив last_dashboard_eventNames очищаем с помощью функции ArrayFree, данные из current_eventNames_data копируются в него через ArrayCopy, а last_dashboard_update обновляется значением currentTime, что минимизирует частоту обновлений. В режиме реальной торговли функция update_dashboard_values вызывается напрямую для постоянного обновления. Далее модифицируем используемые функции, учитывая внесенные изменения, особенно в части временного разделения.

//+------------------------------------------------------------------+
//| Load events from CSV resource                                    |
//+------------------------------------------------------------------+
bool LoadEventsFromResource() {
   string fileData = EconomicCalendarData;
   Print("Raw resource content (size: ", StringLen(fileData), " bytes):\n", fileData);
   string lines[];
   int lineCount = StringSplit(fileData, '\n', lines);
   if (lineCount <= 1) {
      Print("Error: No data lines found in resource! Raw data: ", fileData);
      return false;
   }
   ArrayResize(allEvents, 0);
   int eventIndex = 0;
   for (int i = 1; i < lineCount; i++) {
      if (StringLen(lines[i]) == 0) {
         if (debugLogging) Print("Skipping empty line ", i); // Modified: Conditional logging
         continue;
      }
      string fields[];
      int fieldCount = StringSplit(lines[i], ',', fields);
      if (debugLogging) Print("Line ", i, ": ", lines[i], " (field count: ", fieldCount, ")"); // Modified: Conditional logging
      if (fieldCount < 8) {
         Print("Malformed line ", i, ": ", lines[i], " (field count: ", fieldCount, ")");
         continue;
      }
      string dateStr = fields[0];
      string timeStr = fields[1];
      string currency = fields[2];
      string event = fields[3];
      for (int j = 4; j < fieldCount - 4; j++) {
         event += "," + fields[j];
      }
      string importance = fields[fieldCount - 4];
      string actualStr = fields[fieldCount - 3];
      string forecastStr = fields[fieldCount - 2];
      string previousStr = fields[fieldCount - 1];
      datetime eventDateTime = StringToTime(dateStr + " " + timeStr);
      if (eventDateTime == 0) {
         Print("Error: Invalid datetime conversion for line ", i, ": ", dateStr, " ", timeStr);
         continue;
      }
      ArrayResize(allEvents, eventIndex + 1);
      allEvents[eventIndex].eventDate = dateStr;
      allEvents[eventIndex].eventTime = timeStr;
      allEvents[eventIndex].currency = currency;
      allEvents[eventIndex].event = event;
      allEvents[eventIndex].importance = importance;
      allEvents[eventIndex].actual = StringToDouble(actualStr);
      allEvents[eventIndex].forecast = StringToDouble(forecastStr);
      allEvents[eventIndex].previous = StringToDouble(previousStr);
      allEvents[eventIndex].eventDateTime = eventDateTime; // Added: Store precomputed datetime
      if (debugLogging) Print("Loaded event ", eventIndex, ": ", dateStr, " ", timeStr, ", ", currency, ", ", event); // Modified: Conditional logging
      eventIndex++;
   }
   Print("Loaded ", eventIndex, " events from resource into array.");
   return eventIndex > 0;
}

Здесь мы загружаем историю новостей из CSV-файла для работы в режиме тестирования на истории с оптимизированной обработкой событий и выборочным логированием. Мы используем функцию LoadEventsFromResource для чтения данных из EconomicCalendarData в переменную fileData. В журнал выводим ее размер - для этого используем функции Print и StringLen. Затем с помощью функции StringSplit разделяем fileData на массив lines, при этом проверяем lineCount на наличие данных, и очищаем allEvents через функцию ArrayResize.

Проходим в цикле по массиву lines, пропуская пустые строки (проверяем функцией StringLen). Напомню, пропуски выводим в журнал только если переменная debugLogging установлена в значение true. Функция StringSplit разбивает каждую строку на поля fields. При этом проверяем количество fieldCount, после чего извлекаем значения dateStr, timeStr, currency, event, importance, actualStr, forecastStr и previousStr, при необходимости динамически объединяя поля события.

Строки даты и времени dateStr и timeStr преобразуем во время eventDateTime с помощью функции StringToTime и сохраняем результат в allEvents[eventIndex].eventDateTime. Массив allEvents заполняем с помощью функций ArrayResize и StringToDouble. Успешные загрузки логируются условно, а функция возвращает true, если eventIndex больше нуля. Далее обновляем функцию, ответственную за обновление значений на дашборде. Она влияет на визуализацию сохраненных данных событий.

//+------------------------------------------------------------------+
//| Update dashboard values                                          |
//+------------------------------------------------------------------+
void update_dashboard_values(string &curr_filter_array[], ENUM_CALENDAR_EVENT_IMPORTANCE &imp_filter_array[]) {
   totalEvents_Considered = 0;
   totalEvents_Filtered = 0;
   totalEvents_Displayable = 0;
   ArrayFree(current_eventNames_data);

   datetime timeRange = PeriodSeconds(range_time);
   datetime timeBefore = TimeTradeServer() - timeRange;
   datetime timeAfter = TimeTradeServer() + timeRange;

   int startY = 162;

   if (MQLInfoInteger(MQL_TESTER)) {
      if (filters_changed) FilterEventsForTester(); // Added: Re-filter events if filters changed
      //---- Tester mode: Process filtered events
      for (int i = 0; i < ArraySize(filteredEvents); i++) {
         totalEvents_Considered++;
         datetime eventDateTime = filteredEvents[i].eventDateTime;
         if (eventDateTime < StartDate || eventDateTime > EndDate) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range."); // Modified: Conditional logging
            continue;
         }

         bool timeMatch = !enableTimeFilter;
         if (enableTimeFilter) {
            if (eventDateTime <= TimeTradeServer() && eventDateTime >= timeBefore) timeMatch = true;
            else if (eventDateTime >= TimeTradeServer() && eventDateTime <= timeAfter) timeMatch = true;
         }
         if (!timeMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to time filter."); // Modified: Conditional logging
            continue;
         }

         bool currencyMatch = !enableCurrencyFilter;
         if (enableCurrencyFilter) {
            for (int j = 0; j < ArraySize(curr_filter_array); j++) {
               if (filteredEvents[i].currency == curr_filter_array[j]) {
                  currencyMatch = true;
                  break;
               }
            }
         }
         if (!currencyMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter."); // Modified: Conditional logging
            continue;
         }

         bool importanceMatch = !enableImportanceFilter;
         if (enableImportanceFilter) {
            string imp_str = filteredEvents[i].importance;
            ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE :
                                                      (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW :
                                                      (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE :
                                                      CALENDAR_IMPORTANCE_HIGH;
            for (int k = 0; k < ArraySize(imp_filter_array); k++) {
               if (event_imp == imp_filter_array[k]) {
                  importanceMatch = true;
                  break;
               }
            }
         }
         if (!importanceMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to importance filter."); // Modified: Conditional logging
            continue;
         }

         totalEvents_Filtered++;
         if (totalEvents_Displayable >= 11) continue;
         totalEvents_Displayable++;

         color holder_color = (totalEvents_Displayable % 2 == 0) ? C'213,227,207' : clrWhite;
         createRecLabel(DATA_HOLDERS+string(totalEvents_Displayable),62,startY-1,716,26+1,holder_color,1,clrNONE);

         int startX = 65;
         string news_data[ArraySize(array_calendar)];
         news_data[0] = filteredEvents[i].eventDate;
         news_data[1] = filteredEvents[i].eventTime;
         news_data[2] = filteredEvents[i].currency;
         color importance_color = clrBlack;
         if (filteredEvents[i].importance == "Low") importance_color = clrYellow;
         else if (filteredEvents[i].importance == "Medium") importance_color = clrOrange;
         else if (filteredEvents[i].importance == "High") importance_color = clrRed;
         news_data[3] = ShortToString(0x25CF);
         news_data[4] = filteredEvents[i].event;
         news_data[5] = DoubleToString(filteredEvents[i].actual, 3);
         news_data[6] = DoubleToString(filteredEvents[i].forecast, 3);
         news_data[7] = DoubleToString(filteredEvents[i].previous, 3);

         for (int k = 0; k < ArraySize(array_calendar); k++) {
            if (k == 3) {
               createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY-(22-12),news_data[k],importance_color,22,"Calibri");
            } else {
               createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY,news_data[k],clrBlack,12,"Calibri");
            }
            startX += buttons[k]+3;
         }

         ArrayResize(current_eventNames_data, ArraySize(current_eventNames_data)+1);
         current_eventNames_data[ArraySize(current_eventNames_data)-1] = filteredEvents[i].event;
         startY += 25;
      }
   } else {

      //---- Live mode: Unchanged

   }
}

Для отображения отфильтрованных новостей используем функцию update_dashboard_values, сбрасывая значения totalEvents_Considered, totalEvents_Filtered, totalEvents_Displayable и очищая current_eventNames_data с помощью функции ArrayFree. Параметр timeRange задаем с помощью функции PeriodSeconds от range_time, а timeBefore и timeAfter вычисляем с помощью TimeTradeServer. Проверяем режим тестера через функцию MQLInfoInteger. Если filters_changed установлено в значение true, вызываем объявленную ранее функцию FilterEventsForTester для обновления filteredEvents.

Перебираем элементы filteredEvents с использованием функции ArraySize, увеличивая totalEvents_Considered и пропуская события вне диапазона от StartDate до EndDate. Также пропускает те, что не подходят под проверки enableTimeFilter, enableCurrencyFilter или enableImportanceFilter. Повторюсь, пропуски попадают в журнал, если включен параметр debugLogging.

Для первых 11 подходящих событий увеличиваем значение totalEvents_Displayable, создаем строки DATA_HOLDERS с помощью createRecLabel и используем функцию createLabel для заполнения значений news_data из полей filteredEvents. Нас интересуют значения eventDate и event, оформляем их в соответствии с importance_color и array_calendar. Сам массив current_eventNames_data расширяется динамически с помощью функции ArrayResize. В нем хранятся названия событий. Для работы в режиме тестера модифицируем функцию проверки и открытия сделок следующим образом:

//+------------------------------------------------------------------+
//| Check for news trade (adapted for tester mode trading)           |
//+------------------------------------------------------------------+
void CheckForNewsTrade() {
   if (!MQLInfoInteger(MQL_TESTER) || debugLogging) Print("CheckForNewsTrade called at: ", TimeToString(TimeTradeServer(), TIME_SECONDS)); // Modified: Conditional logging
   if (tradeMode == NO_TRADE || tradeMode == PAUSE_TRADING) {
      if (ObjectFind(0, "NewsCountdown") >= 0) {
         ObjectDelete(0, "NewsCountdown");
         Print("Trading disabled. Countdown removed.");
      }
      return;
   }

   datetime currentTime = TimeTradeServer();
   int offsetSeconds = tradeOffsetHours * 3600 + tradeOffsetMinutes * 60 + tradeOffsetSeconds;

   if (tradeExecuted) {
      if (currentTime < tradedNewsTime) {
         int remainingSeconds = (int)(tradedNewsTime - currentTime);
         int hrs = remainingSeconds / 3600;
         int mins = (remainingSeconds % 3600) / 60;
         int secs = remainingSeconds % 60;
         string countdownText = "News in: " + IntegerToString(hrs) + "h " +
                               IntegerToString(mins) + "m " + IntegerToString(secs) + "s";
         if (ObjectFind(0, "NewsCountdown") < 0) {
            createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrBlue, clrBlack);
            Print("Post-trade countdown created: ", countdownText);
         } else {
            updateLabel1("NewsCountdown", countdownText);
            Print("Post-trade countdown updated: ", countdownText);
         }
      } else {
         int elapsed = (int)(currentTime - tradedNewsTime);
         if (elapsed < 15) {
            int remainingDelay = 15 - elapsed;
            string countdownText = "News Released, resetting in: " + IntegerToString(remainingDelay) + "s";
            if (ObjectFind(0, "NewsCountdown") < 0) {
               createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrRed, clrBlack);
               ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed);
               Print("Post-trade reset countdown created: ", countdownText);
            } else {
               updateLabel1("NewsCountdown", countdownText);
               ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed);
               Print("Post-trade reset countdown updated: ", countdownText);
            }
         } else {
            Print("News Released. Resetting trade status after 15 seconds.");
            if (ObjectFind(0, "NewsCountdown") >= 0) ObjectDelete(0, "NewsCountdown");
            tradeExecuted = false;
         }
      }
      return;
   }

   datetime lowerBound = currentTime - PeriodSeconds(start_time);
   datetime upperBound = currentTime + PeriodSeconds(end_time);
   if (debugLogging) Print("Event time range: ", TimeToString(lowerBound, TIME_SECONDS), " to ", TimeToString(upperBound, TIME_SECONDS)); // Modified: Conditional logging

   datetime candidateEventTime = 0;
   string candidateEventName = "";
   string candidateTradeSide = "";
   int candidateEventID = -1;

   if (MQLInfoInteger(MQL_TESTER)) {
      //---- Tester mode: Process filtered events
      int totalValues = ArraySize(filteredEvents);
      if (debugLogging) Print("Total events found: ", totalValues); // Modified: Conditional logging
      if (totalValues <= 0) {
         if (ObjectFind(0, "NewsCountdown") >= 0) ObjectDelete(0, "NewsCountdown");
         return;
      }

      for (int i = 0; i < totalValues; i++) {
         datetime eventTime = filteredEvents[i].eventDateTime;
         if (eventTime < lowerBound || eventTime > upperBound || eventTime < StartDate || eventTime > EndDate) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range."); // Modified: Conditional logging
            continue;
         }

         bool currencyMatch = !enableCurrencyFilter;
         if (enableCurrencyFilter) {
            for (int k = 0; k < ArraySize(curr_filter_selected); k++) {
               if (filteredEvents[i].currency == curr_filter_selected[k]) {
                  currencyMatch = true;
                  break;
               }
            }
            if (!currencyMatch) {
               if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter."); // Modified: Conditional logging
               continue;
            }
         }

         bool impactMatch = !enableImportanceFilter;
         if (enableImportanceFilter) {
            string imp_str = filteredEvents[i].importance;
            ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE :
                                                      (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW :
                                                      (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE :
                                                      CALENDAR_IMPORTANCE_HIGH;
            for (int k = 0; k < ArraySize(imp_filter_selected); k++) {
               if (event_imp == imp_filter_selected[k]) {
                  impactMatch = true;
                  break;
               }
            }
            if (!impactMatch) {
               if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to impact filter."); // Modified: Conditional logging
               continue;
            }
         }

         bool alreadyTriggered = false;
         for (int j = 0; j < ArraySize(triggeredNewsEvents); j++) {
            if (triggeredNewsEvents[j] == i) {
               alreadyTriggered = true;
               break;
            }
         }
         if (alreadyTriggered) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " already triggered a trade. Skipping."); // Modified: Conditional logging
            continue;
         }

         if (tradeMode == TRADE_BEFORE) {
            if (currentTime >= (eventTime - offsetSeconds) && currentTime < eventTime) {
               double forecast = filteredEvents[i].forecast;
               double previous = filteredEvents[i].previous;
               if (forecast == 0.0 || previous == 0.0) {
                  if (debugLogging) Print("Skipping event ", filteredEvents[i].event, " because forecast or previous value is empty."); // Modified: Conditional logging
                  continue;
               }
               if (forecast == previous) {
                  if (debugLogging) Print("Skipping event ", filteredEvents[i].event, " because forecast equals previous."); // Modified: Conditional logging
                  continue;
               }
               if (candidateEventTime == 0 || eventTime < candidateEventTime) {
                  candidateEventTime = eventTime;
                  candidateEventName = filteredEvents[i].event;
                  candidateEventID = i;
                  candidateTradeSide = (forecast > previous) ? "BUY" : "SELL";
                  if (debugLogging) Print("Candidate event: ", filteredEvents[i].event, " with event time: ", TimeToString(eventTime, TIME_SECONDS), " Side: ", candidateTradeSide); // Modified: Conditional logging
               }
            }
         }
      }
   } else {

      //---- Live mode: Unchanged

   }
}

Для оценки и запуска сделок на основе новостей в режиме тестера с фильтрацией событий и выборочным логированием используем функцию CheckForNewsTrade. Если параметр debugLogging установлен в true, используем функцию Print для вывода в журнал, время при этом берется из TimeToString и TimeTradeServer. Выходим из функции, если переменная tradeMode равна NO_TRADE или PAUSE_TRADING. Для проверки наличия объекта NewsCountdown используем функцию ObjectFind, удаляем его с помощью ObjectDelete, выводим в журнал Print. Также управляем пост-торговыми состояниями, вычисляя currentTime через TimeTradeServer и offsetSeconds на основе tradeOffsetHours, tradeOffsetMinutes и tradeOffsetSeconds.

Если переменная tradeExecuted равна true, обрабатываем таймеры обратного отсчета tradedNewsTime, формируя вывод countdownText с помощью функции IntegerToString для отображения оставшегося времени или задержки, создавая или обновляя NewsCountdown через createButton1 или updateLabel1 в зависимости от результата ObjectFind. Стиль настраиваем с помощью ObjectSetInteger и записываем в журнал через Print. Через 15 секунд после исполнения сделки tradeExecuted сбрасывается, а объект удаляется через ObjectDelete с соответствующей записью в журнале.

В режиме тестера (определили функцией MQLInfoInteger проверив MQL_TESTER) мы обрабатываем filteredEvents, получая totalValues через ArraySize, выводим в журнал при необходимости, и выходим при отсутствии данных, предварительно очистив переменную NewsCountdown. Задаем lowerBound и upperBound через TimeTradeServer и PeriodSeconds на основе значений start_time и end_time, выводим диапазон в журнал при включенном debugLogging, и инициализируем candidateEventTime, candidateEventName, candidateEventID и candidateTradeSide для выбора сделки.

Проходим в цикле по массиву filteredEvents, пропускаем события вне диапазона от lowerBound до upperBound и от StartDate до EndDate, а также те, которые не проходят фильтрацию по валюте (enableCurrencyFilter со значением curr_filter_selected) или важности (enableImportanceFilter со значением imp_filter_selected). Если включен режим debugLogging, записываем пропуски в журнал функцией Print. С помощью ArraySize на массиве triggeredNewsEvents исключаются уже отработанные события.

В режиме TRADE_BEFORE мы ищем события в пределах offsetSeconds до eventDateTime, проверяем корректность forecast и previous, и выбираем самое раннее событие, записывая его в candidateEventTime, candidateEventName, candidateEventID и candidateTradeSide (BUY, если значение forecast больше previous, иначе SELL). Используем Print для записи в журнал, если включено debugLogging. Остальная логика режима реальной торговли остается без изменений. После компиляции получаем следующую визуализацию подтверждения сделок.

GIF-анимация подтверждения сделок

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


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

Тестируем программу, сначала запуская ее в режиме реальной торговли с загрузкой необходимых новостных данных. Затем запустим ее в тестере стратегий MetaTrader 5 с параметрами StartDate = 2025.03.01, EndDate = 2025.03.21 и отключенным логированием debugLogging. Используем CSV-файл в параметре EconomicCalendarData для моделирования сделок через CheckForNewsTrade с фильтрацией filteredEvents. GIF-fybvfwия ниже демонстрирует нашу панель, которая обновляется функцией update_dashboard_values только при срабатывании событий filters_changed или last_dashboard_update. На ней отображаются отфильтрованные события через createLabel. Также мы имеем чистые журналы сделок и обновлений. Тесты в режиме реальной торговли с использованием функции CalendarValueHistory подтверждают, что визуально все идентично. Итак, мы имеем быструю и наглядную работу программы в обоих режимах. Вот эта анимация:

Итоговая GIF-анимация


Заключение

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

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

Прикрепленные файлы |
Торговые инструменты на MQL5 (Часть 2): Улучшение интерактивного торгового помощника через динамическую визуализацию Торговые инструменты на MQL5 (Часть 2): Улучшение интерактивного торгового помощника через динамическую визуализацию
В этой статье мы обновим наш инструмент Trade Assistant, добавив функциональность панели перетаскивания и эффекты наведения курсора мыши, чтобы сделать интерфейс более интуитивно понятным и отзывчивым. Мы совершенствуем инструмент для проверки настроек ордеров в режиме реального времени, обеспечивая точные торговые настройки относительно рыночных цен. Мы также тестируем эти усовершенствования на исторических данных, чтобы подтвердить их надежность.
Машинное обучение и Data Science (Часть 38): Применение трансферного обучения (Transfer Learning) на валютных рынках Машинное обучение и Data Science (Часть 38): Применение трансферного обучения (Transfer Learning) на валютных рынках
Прорывы в области искусственного интеллекта, о которых пишут в новостях, от ChatGPT до беспилотных автомобилей, создаются не на основе отдельных моделей, а благодаря накопленным знаниям, перенесенным из различных моделей или общих областей. Теперь этот же подход "обучить один раз, применять везде" можно использовать для трансформации наших моделей ИИ в алгоритмической торговле. В этой статье мы узнаем, как можно использовать полученные с помощью различных инструментов данные для улучшения прогнозов посредством трансферного обучения.
Как использовать конечные разности для прогнозирования цен Как использовать конечные разности для прогнозирования цен
Рассматривается практическое использование конечных разностей в трейдинге: типы разностей, их связь с динамикой цены и биноминальное преобразование для фильтрации шумов. Описаны правила кодирования паттернов по уровням разностей и применение этих паттернов к прогнозу. Приведены наивные, адаптивные и вероятностные подходы, которые помогают сглаживать ряды, выделять повторяющиеся структуры и оценивать будущие движения.
Осциллятор Parafrac V2: Интеграция Parabolic SAR и среднего истинного диапазона (Average True Range) Осциллятор Parafrac V2: Интеграция Parabolic SAR и среднего истинного диапазона (Average True Range)
Осциллятор Parafrac V2 — передовой инструмент технического анализа, который объединяет индикатор Parabolic SAR (параболический индикатор «остановки и разворота») с индикатором среднего истинного диапазона (Average True Range, ATR), чтобы преодолеть ограничения своего предшественника, который полагался на фракталы и был склонен к тому, что пики сигнала заглушали предыдущие и текущие сигналы. Благодаря использованию показателя волатильности ATR, версия 2 предлагает более плавный и надежный метод обнаружения трендов, разворотов и расхождений, помогая трейдерам уменьшить перегрузку графиков и аналитический паралич.