English 中文 Deutsch 日本語
preview
Трейдинг с экономическим календарем MQL5 (Часть 6): Автоматизация входа в сделку с анализом новостей и таймерами обратного отсчета

Трейдинг с экономическим календарем MQL5 (Часть 6): Автоматизация входа в сделку с анализом новостей и таймерами обратного отсчета

MetaTrader 5Трейдинг |
327 3
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

  1. Понимание требований торговой логики
  2. Реализация торговой логики на MQL5
  3. Создание и управление таймерами обратного отсчета
  4. Тестирование торговой логики
  5. Заключение

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


Понимание требований торговой логики

Для нашей автоматизированной торговой системы первым шагом будет определение того, какие новостные события являются подходящими кандидатами для сделки. Мы определяем событие-кандидат как событие, которое попадает в конкретный временной интервал (задаваемый смещением) относительно его запланированного времени выхода. Затем мы включим входные данные для торговых режимов, например, торговлю до выхода новостей. Например, в режиме trade before (торговать до) событие будет учитываться только в том случае, если текущее время находится между запланированным временем выхода новости за вычетом смещения (например, 5 минут) и фактическим временем появления новости. Таким образом, мы начнем торговать за 5 минут до фактического выхода новости.

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

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

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

Панель


Реализация торговой логики на MQL5

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

#include <Trade\Trade.mqh> // Trading library for order execution
CTrade trade;             // Global trade object


//================== Trade Settings ==================//
// Trade mode options:
enum ETradeMode {
   TRADE_BEFORE,     // Trade before the news event occurs
   TRADE_AFTER,      // Trade after the news event occurs
   NO_TRADE,         // Do not trade
   PAUSE_TRADING     // Pause trading activity (no trades until resumed)
};
input ETradeMode tradeMode      = TRADE_BEFORE; // Choose the trade mode

// Trade offset inputs:
input int        tradeOffsetHours   = 12;         // Offset hours (e.g., 12 hours)
input int        tradeOffsetMinutes = 5;          // Offset minutes (e.g., 5 minutes before)
input int        tradeOffsetSeconds = 0;          // Offset seconds
input double     tradeLotSize       = 0.01;       // Lot size for the trade

//================== Global Trade Control ==================//
// Once a trade is executed for one news event, no further trades occur.
bool tradeExecuted = false;
// Store the traded event’s scheduled news time for the post–trade countdown.
datetime tradedNewsTime = 0;
// Global array to store event IDs that have already triggered a trade.
int triggeredNewsEvents[];

В глобальном масштабе мы включаем библиотеку Trade\Trade.mqh, library using #include ь для включения исполнения ордеров и объявления глобального объекта CTrade с именем trade для обработки сделок. Мы определяем один из перечисленных типов ETradeMode с опциями TRADE_BEFORE (торговать до), TRADE_AFTER (торговать после), NO_TRADE (не торговать) и PAUSE_TRADING (пауза в торговле), и используем входную переменную tradeMode (по умолчанию TRADE_BEFORE для программы), чтобы определить, когда следует открывать сделки относительно новостей. Кроме того, мы настраиваем "входные" переменные tradeOffsetHours, tradeOffsetMinutes, tradeOffsetSeconds и tradeLotSize, чтобы указать смещение времени и размер сделки, в то время, как глобальные переменные tradeExecuted (логическое значение), tradedNewsTime (дата и время) и массив triggeredNewsEvents (массив int) помогают нам управлять сделками и предотвращать повторную торговлю на одной и той же новости. Затем мы можем включить торговую логику в функцию.

//--- Function to scan for news events and execute trades based on selected criteria
//--- It handles both pre-trade candidate selection and post-trade countdown updates
void CheckForNewsTrade() {
   //--- Log the call to CheckForNewsTrade with the current server time
   Print("CheckForNewsTrade called at: ", TimeToString(TimeTradeServer(), TIME_SECONDS));
   
   //--- If trading is disabled (either NO_TRADE or PAUSE_TRADING), remove countdown objects and exit
   if(tradeMode == NO_TRADE || tradeMode == PAUSE_TRADING) {
      //--- Check if a countdown object exists on the chart
      if(ObjectFind(0, "NewsCountdown") >= 0) {
         //--- Delete the countdown object from the chart
         ObjectDelete(0, "NewsCountdown");
         //--- Log that the trading is disabled and the countdown has been removed
         Print("Trading disabled. Countdown removed.");
      }
      //--- Exit the function since trading is not allowed
      return;
   }
   //--- Begin pre-trade candidate selection section
   
   //--- Define the lower bound of the event time range based on the user-defined start time offset
   datetime lowerBound = currentTime - PeriodSeconds(start_time);
   //--- Define the upper bound of the event time range based on the user-defined end time offset
   datetime upperBound = currentTime + PeriodSeconds(end_time);
   //--- Log the overall event time range for debugging purposes
   Print("Event time range: ", TimeToString(lowerBound, TIME_SECONDS), " to ", TimeToString(upperBound, TIME_SECONDS));
   
   //--- Retrieve historical calendar values (news events) within the defined time range
   MqlCalendarValue values[];
   int totalValues = CalendarValueHistory(values, lowerBound, upperBound, NULL, NULL);
   //--- Log the total number of events found in the specified time range
   Print("Total events found: ", totalValues);
   //--- If no events are found, delete any existing countdown and exit the function
   if(totalValues <= 0) {
      if(ObjectFind(0, "NewsCountdown") >= 0)
         ObjectDelete(0, "NewsCountdown");
      return;
   }
}

Здесь мы определяем функцию CheckForNewsTrade, которая сканирует новости и совершает сделки на основе выбранных нами критериев. Мы начинаем с регистрации вызова с помощью функции Print, отображающей текущее время сервера, полученное через функцию TimeTradeServer. Затем мы проверяем, отключена ли торговля, сравнивая переменную tradeMode с режимом NO_TRADE или PAUSE_TRADING. Если да, используем функцию ObjectFind, чтобы определить, существует ли объект обратного отсчета NewsCountdown. Если объект найден, он удаляется с использованием ObjectDelete перед выходом из функции.

Затем функция вычисляет общий временной диапазон события, устанавливая lowerBound равным текущему времени за вычетом количества секунд от входного параметра start_time (преобразованного с помощью функции PeriodSeconds) и upperBound к текущему времени плюс секунды из входного параметра end_time. Этот общий временной диапазон затем регистрируется с помощью Print. Наконец, функция вызывает CalendarValueHistory для извлечения всех новостей в пределах определенного временного диапазона; если события не найдены, она очищает все существующие объекты обратного отсчета и выходит, тем самым подготавливая систему к последующему выбору событий-кандидатов и исполнению сделки.

//--- Initialize candidate event variables for trade selection
datetime candidateEventTime = 0;
string candidateEventName = "";
string candidateTradeSide = "";
int candidateEventID = -1;

//--- Loop through all retrieved events to evaluate each candidate for trading
for(int i = 0; i < totalValues; i++) {
   //--- Declare an event structure to hold event details
   MqlCalendarEvent event;
   //--- Attempt to populate the event structure by its ID; if it fails, skip to the next event
   if(!CalendarEventById(values[i].event_id, event))
      continue;
   
   //----- Apply Filters -----
   
   //--- If currency filtering is enabled, check if the event's currency matches the selected filters
   if(enableCurrencyFilter) {
      //--- Declare a country structure to hold country details
      MqlCalendarCountry country;
      //--- Populate the country structure based on the event's country ID
      CalendarCountryById(event.country_id, country);
      //--- Initialize a flag to determine if there is a matching currency
      bool currencyMatch = false;
      //--- Loop through each selected currency filter
      for(int k = 0; k < ArraySize(curr_filter_selected); k++) {
         //--- Check if the event's country currency matches the current filter selection
         if(country.currency == curr_filter_selected[k]) {
            //--- Set flag to true if a match is found and break out of the loop
            currencyMatch = true;
            break;
         }
      }
      //--- If no matching currency is found, log the skip and continue to the next event
      if(!currencyMatch) {
         Print("Event ", event.name, " skipped due to currency filter.");
         continue;
      }
   }
   
   //--- If importance filtering is enabled, check if the event's impact matches the selected filters
   if(enableImportanceFilter) {
      //--- Initialize a flag to determine if the event's impact matches any filter selection
      bool impactMatch = false;
      //--- Loop through each selected impact filter option
      for(int k = 0; k < ArraySize(imp_filter_selected); k++) {
         //--- Check if the event's importance matches the current filter selection
         if(event.importance == imp_filter_selected[k]) {
            //--- Set flag to true if a match is found and break out of the loop
            impactMatch = true;
            break;
         }
      }
      //--- If no matching impact is found, log the skip and continue to the next event
      if(!impactMatch) {
         Print("Event ", event.name, " skipped due to impact filter.");
         continue;
      }
   }
   
   //--- If time filtering is enabled and the event time exceeds the upper bound, skip the event
   if(enableTimeFilter && values[i].time > upperBound) {
      Print("Event ", event.name, " skipped due to time filter.");
      continue;
   }
   
   //--- Check if the event has already triggered a trade by comparing its ID to recorded events
   bool alreadyTriggered = false;
   //--- Loop through the list of already triggered news events
   for(int j = 0; j < ArraySize(triggeredNewsEvents); j++) {
      //--- If the event ID matches one that has been triggered, mark it and break out of the loop
      if(triggeredNewsEvents[j] == values[i].event_id) {
         alreadyTriggered = true;
         break;
      }
   }
   //--- If the event has already triggered a trade, log the skip and continue to the next event
   if(alreadyTriggered) {
      Print("Event ", event.name, " already triggered a trade. Skipping.");
      continue;
   }

Здесь мы инициализируем переменные-кандидаты событий с помощью переменной даты и времени (candidateEventTime), двух "строковых" переменных (candidateEventName и candidateTradeSide) и переменной int ("candidateEventID"), установленной на -1. Далее мы перебираем каждое событие, полученное функцией CalendarValueHistory (хранящейся в массиве структур MqlCalendarValue) и используем функцию CalendarEventById для заполнения структуры MqlCalendarEvent данными о событии.

Далее мы применяем наши фильтры: если включена сортировка валют, мы извлекаем соответствующую структуру MqlCalendarCountry с помощью CalendarCountryById и проверяем, соответствует ли ее поле currency (валюта) какой-либо записи в массиве curr_filter_selected; если нет, мы регистрируем сообщение и пропускаем событие. Аналогично, если включена сортировка по важности, мы просматриваем массив imp_filter_selected, чтобы убедиться, что "важность" события соответствует одному из выбранных уровней, регистрируя и пропуская событие, если это не так.

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

//--- For TRADE_BEFORE mode, check if the current time is within the valid window (event time minus offset to event time)
if(tradeMode == TRADE_BEFORE) {
   if(currentTime >= (values[i].time - offsetSeconds) && currentTime < values[i].time) {
      //--- Retrieve the forecast and previous values for the event
      MqlCalendarValue calValue;
      //--- If unable to retrieve calendar values, log the error and skip this event
      if(!CalendarValueById(values[i].id, calValue)) {
         Print("Error retrieving calendar value for event: ", event.name);
         continue;
      }
      //--- Get the forecast value from the calendar data
      double forecast = calValue.GetForecastValue();
      //--- Get the previous value from the calendar data
      double previous = calValue.GetPreviousValue();
      //--- If either forecast or previous is zero, log the skip and continue to the next event
      if(forecast == 0.0 || previous == 0.0) {
         Print("Skipping event ", event.name, " because forecast or previous value is empty.");
         continue;
      }
      //--- If forecast equals previous, log the skip and continue to the next event
      if(forecast == previous) {
         Print("Skipping event ", event.name, " because forecast equals previous.");
         continue;
      }
      //--- If this candidate event is earlier than any previously found candidate, record its details
      if(candidateEventTime == 0 || values[i].time < candidateEventTime) {
         candidateEventTime = values[i].time;
         candidateEventName = event.name;
         candidateEventID = (int)values[i].event_id;
         candidateTradeSide = (forecast > previous) ? "BUY" : "SELL";
         //--- Log the candidate event details including its time and trade side
         Print("Candidate event: ", event.name, " with event time: ", TimeToString(values[i].time, TIME_SECONDS), " Side: ", candidateTradeSide);
      }
   }
}

Здесь мы оцениваем потенциальные новостные события при работе в режиме TRADE_BEFORE. Мы проверяем, соответствует ли текущее время, полученное с помощью функции TimeTradeServer допустимому торговому окну, которое простирается от запланированного времени события за вычетом определяемого пользователем смещения (offsetSeconds) до точного времени события, как определено ниже.

//--- Get the current trading server time
datetime currentTime = TimeTradeServer();
//--- Calculate the offset in seconds based on trade offset hours, minutes, and seconds
int offsetSeconds = tradeOffsetHours * 3600 + tradeOffsetMinutes * 60 + tradeOffsetSeconds;

Если условие выполняется, мы извлекаем прогноз события и предыдущие значения с помощью функции CalendarValueById для заполнения структуры MqlCalendarValue. Если извлечение не удалось, регистрируем сообщение об ошибке и пропускаем событие. Затем мы извлекаем прогнозные и предыдущие значения, используя методы GetForecastValue и GetPreviousValue соответственно. Если какое-либо из значений нулевое или они равны, мы регистрируем сообщение и переходим к следующему событию, чтобы гарантировать обработку только событий со значимыми данными.

Если событие соответствует требованиям и происходит раньше любого ранее определенного кандидата, мы обновляем переменные: candidateEventTime сохраняет время события, candidateEventName содержит имя события, candidateEventID записывает идентификатор события, а candidateTradeSide определяет направление сделки - покупка (если прогноз больше предыдущего значения) или продажа (если прогноз ниже). Наконец, мы регистрируем сведения о выбранном событии-кандидате, гарантируя отслеживание самого раннего допустимого события для исполнения сделки. Затем мы можем выбрать событие для исполнения сделки.

//--- If a candidate event has been selected and the trade mode is TRADE_BEFORE, attempt to execute the trade
if(tradeMode == TRADE_BEFORE && candidateEventTime > 0) {
   //--- Calculate the target time to start trading by subtracting the offset from the candidate event time
   datetime targetTime = candidateEventTime - offsetSeconds;
   //--- Log the candidate target time for debugging purposes
   Print("Candidate target time: ", TimeToString(targetTime, TIME_SECONDS));
   //--- Check if the current time falls within the trading window (target time to candidate event time)
   if(currentTime >= targetTime && currentTime < candidateEventTime) {
      //--- Loop through events again to get detailed information for the candidate event
      for(int i = 0; i < totalValues; i++) {
         //--- Identify the candidate event by matching its time
         if(values[i].time == candidateEventTime) {
            //--- Declare an event structure to store event details
            MqlCalendarEvent event;
   
         }
      }
   }
}

Мы проверяем, выбрано ли событие-кандидат и установлен ли режим торговли TRADE_BEFORE, путем проверки того, что candidateEventTime больше нуля. Затем мы вычисляем targetTime, вычитая заданное пользователем смещение (offsetSeconds) из запланированного времени события-кандидата, и регистрируем это целевое время для отладки с помощью функции Print. Затем мы определяем, попадает ли текущее время в допустимое торговое окно — между targetTime и временем события-кандидата — и если да, то мы проходим по массиву событий, чтобы идентифицировать событие-кандидат, сопоставляя его время, чтобы затем можно было приступить к получению дополнительных сведений и выполнению сделки.

//--- Attempt to retrieve the event details; if it fails, skip to the next event
if(!CalendarEventById(values[i].event_id, event))
   continue;
//--- If the current time is past the event time, log the skip and continue
if(currentTime >= values[i].time) {
   Print("Skipping candidate ", event.name, " because current time is past event time.");
   continue;
}
//--- Retrieve detailed calendar values for the candidate event
MqlCalendarValue calValue;
//--- If retrieval fails, log the error and skip the candidate
if(!CalendarValueById(values[i].id, calValue)) {
   Print("Error retrieving calendar value for candidate event: ", event.name);
   continue;
}
//--- Get the forecast value for the candidate event
double forecast = calValue.GetForecastValue();
//--- Get the previous value for the candidate event
double previous = calValue.GetPreviousValue();
//--- If forecast or previous is zero, or if they are equal, log the skip and continue
if(forecast == 0.0 || previous == 0.0 || forecast == previous) {
   Print("Skipping candidate ", event.name, " due to invalid forecast/previous values.");
   continue;
}
//--- Construct a news information string for the candidate event
string newsInfo = "Trading on news: " + event.name +
                  " (Time: " + TimeToString(values[i].time, TIME_SECONDS)+")";
//--- Log the news trading information
Print(newsInfo);
//--- Create a label on the chart to display the news trading information
createLabel1("NewsTradeInfo", 355, 22, newsInfo, clrBlue, 11);

Прежде чем входит в сделку, мы пытаемся получить подробную информацию о событии-кандидате с помощью функции CalendarEventById для заполнения структуры MqlCalendarEvent. Если извлечение не удалось, мы немедленно переходим к следующему событию. Затем мы проверяем, прошло ли уже текущее время (полученное с помощью TimeTradeServer) относительно запланированного времени события-кандидата. Если это так, мы регистрируем сообщение и пропускаем обработку этого события.

Затем мы извлекаем подробные значения календаря для события, используя CalendarValueById для заполнения структуры MqlCalendarValue, затем извлекаем значения forecast и previous с помощью методов GetForecastValue и GetPreviousValue соответственно. Если какое-либо из значений равно нулю или оба равны, мы регистрируем причину и пропускаем событие-кандидат. Наконец, мы создаем строку, содержащую ключевую новостную информацию, и регистрируем ее, а также отображаем эту информацию на графике с помощью функции createLabel1. Фрагмент кода функции приведен ниже.

//--- Function to create a label on the chart with specified properties
bool createLabel1(string objName, int x, int y, string text, color txtColor, int fontSize) {
   //--- Attempt to create the label object; if it fails, log the error and return false
   if(!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) {
      //--- Print error message with the label name and the error code
      Print("Error creating label ", objName, " : ", GetLastError());
      //--- Return false to indicate label creation failure
      return false;
   }
   //--- Set the horizontal distance (X coordinate) for the label
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x);
   //--- Set the vertical distance (Y coordinate) for the label
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y);
   //--- Set the text that will appear on the label
   ObjectSetString(0, objName, OBJPROP_TEXT, text);
   //--- Set the color of the label's text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, txtColor);
   //--- Set the font size for the label text
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);
   //--- Set the font style to "Arial Bold" for the label text
   ObjectSetString(0, objName, OBJPROP_FONT, "Arial Bold");
   //--- Set the label's anchor corner to the top left of the chart
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   //--- Redraw the chart to reflect the new label
   ChartRedraw();
   //--- Return true indicating that the label was created successfully
   return true;
}

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

//--- Initialize a flag to store the result of the trade execution
bool tradeResult = false;
//--- If the candidate trade side is BUY, attempt to execute a buy order
if(candidateTradeSide == "BUY") {
   tradeResult = trade.Buy(tradeLotSize, _Symbol, 0, 0, 0, event.name);
} 
//--- Otherwise, if the candidate trade side is SELL, attempt to execute a sell order
else if(candidateTradeSide == "SELL") {
   tradeResult = trade.Sell(tradeLotSize, _Symbol, 0, 0, 0, event.name);
}
//--- If the trade was executed successfully, update the triggered events and trade flags
if(tradeResult) {
   Print("Trade executed for candidate event: ", event.name, " Side: ", candidateTradeSide);
   int size = ArraySize(triggeredNewsEvents);
   ArrayResize(triggeredNewsEvents, size + 1);
   triggeredNewsEvents[size] = (int)values[i].event_id;
   tradeExecuted = true;
   tradedNewsTime = values[i].time;
} else {
   //--- If trade execution failed, log the error message with the error code
   Print("Trade execution failed for candidate event: ", event.name, " Error: ", GetLastError());
}
//--- Break out of the loop after processing the candidate event
break;

Сначала мы инициализируем логический флаг tradeResult для сохранения результата нашей попытки входа в рынок. Затем мы проверяем candidateTradeSide. При BUY (покупка) вызываем функцию trade.Buy с указанным tradeLotSize, символом (_Symbol), а также используем имя события в качестве комментария для уникальности и более легкой идентификации; если candidateTradeSide — это SELL, мы аналогичным образом вызываем trade.Sell. Если сделка выполнена успешно (т.е. tradeResult равен true), мы регистрируем сведения о выполнении, обновляем наш массив triggeredNewsEvents, изменяя его размер с помощью функции ArrayResize, добавляем идентификатор события, устанавливаем tradeExecuted на 'true' и записываем запланированное время события в tradedNewsTime. В противном случае мы регистрируем сообщение об ошибке с помощью GetLastError, а затем прерываем цикл, чтобы предотвратить обработку любых дальнейших событий-кандидатов. Вот пример сделки, открытой по диапазону событий.

Комментарий к сделке

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


Создание и управление таймерами обратного отсчета

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

//--- Function to create a button on the chart with specified properties
bool createButton1(string objName, int x, int y, int width, int height, 
                   string text, color txtColor, int fontSize, color bgColor, color borderColor) {
   //--- Attempt to create the button object; if it fails, log the error and return false
   if(!ObjectCreate(0, objName, OBJ_BUTTON, 0, 0, 0)) {
      //--- Print error message with the button name and the error code
      Print("Error creating button ", objName, " : ", GetLastError());
      //--- Return false to indicate button creation failure
      return false;
   }
   //--- Set the horizontal distance (X coordinate) for the button
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x);
   //--- Set the vertical distance (Y coordinate) for the button
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y);
   //--- Set the width of the button
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, width);
   //--- Set the height of the button
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, height);
   //--- Set the text that will appear on the button
   ObjectSetString(0, objName, OBJPROP_TEXT, text);
   //--- Set the color of the button's text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, txtColor);
   //--- Set the font size for the button text
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);
   //--- Set the font style to "Arial Bold" for the button text
   ObjectSetString(0, objName, OBJPROP_FONT, "Arial Bold");
   //--- Set the background color of the button
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, bgColor);
   //--- Set the border color of the button
   ObjectSetInteger(0, objName, OBJPROP_BORDER_COLOR, borderColor);
   //--- Set the button's anchor corner to the top left of the chart
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   //--- Enable the background display for the button
   ObjectSetInteger(0, objName, OBJPROP_BACK, true);
   //--- Redraw the chart to reflect the new button
   ChartRedraw();
   //--- Return true indicating that the button was created successfully
   return true;
}
//--- Function to update the text of an existing label
bool updateLabel1(string objName, string text) {
   //--- Check if the label exists on the chart; if not, log the error and return false
   if(ObjectFind(0, objName) < 0) {
      //--- Print error message indicating that the label was not found
      Print("updateLabel1: Object ", objName, " not found.");
      //--- Return false because the label does not exist
      return false;
   }
   //--- Update the label's text property with the new text
   ObjectSetString(0, objName, OBJPROP_TEXT, text);
   //--- Redraw the chart to update the label display
   ChartRedraw();
   //--- Return true indicating that the label was updated successfully
   return true;
}

//--- Function to update the text of an existing label
bool updateLabel1(string objName, string text) {
   //--- Check if the label exists on the chart; if not, log the error and return false
   if(ObjectFind(0, objName) < 0) {
      //--- Print error message indicating that the label was not found
      Print("updateLabel1: Object ", objName, " not found.");
      //--- Return false because the label does not exist
      return false;
   }
   //--- Update the label's text property with the new text
   ObjectSetString(0, objName, OBJPROP_TEXT, text);
   //--- Redraw the chart to update the label display
   ChartRedraw();
   //--- Return true indicating that the label was updated successfully
   return true;
}

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

//--- Begin handling the post-trade countdown scenario
if(tradeExecuted) {
   //--- If the current time is before the traded news time, display the countdown until news release
   if(currentTime < tradedNewsTime) {
      //--- Calculate the remaining seconds until the traded news time
      int remainingSeconds = (int)(tradedNewsTime - currentTime);
      //--- Calculate hours from the remaining seconds
      int hrs = remainingSeconds / 3600;
      //--- Calculate minutes from the remaining seconds
      int mins = (remainingSeconds % 3600) / 60;
      //--- Calculate seconds remainder
      int secs = remainingSeconds % 60;
      //--- Construct the countdown text string
      string countdownText = "News in: " + IntegerToString(hrs) + "h " +
                             IntegerToString(mins) + "m " +
                             IntegerToString(secs) + "s";
      //--- If the countdown object does not exist, create it with a blue background
      if(ObjectFind(0, "NewsCountdown") < 0) {
         createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrBlue, clrBlack);
         //--- Log that the post-trade countdown was created
         Print("Post-trade countdown created: ", countdownText);
      } else {
         //--- If the countdown object exists, update its text
         updateLabel1("NewsCountdown", countdownText);
         //--- Log that the post-trade countdown was updated
         Print("Post-trade countdown updated: ", countdownText);
      }
   } else {
      //--- If current time is past the traded news time, calculate elapsed time since trade
      int elapsed = (int)(currentTime - tradedNewsTime);
      //--- If less than 15 seconds have elapsed, show a reset countdown
      if(elapsed < 15) {
         //--- Calculate the remaining delay for reset
         int remainingDelay = 15 - elapsed;
         //--- Construct the reset countdown text
         string countdownText = "News Released, resetting in: " + IntegerToString(remainingDelay) + "s";
         //--- If the countdown object does not exist, create it with a red background
         if(ObjectFind(0, "NewsCountdown") < 0) {
            createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrRed, clrBlack);
            //--- Set the background color property explicitly to red
            ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed);
            //--- Log that the post-trade reset countdown was created
            Print("Post-trade reset countdown created: ", countdownText);
         } else {
            //--- If the countdown object exists, update its text and background color
            updateLabel1("NewsCountdown", countdownText);
            ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed);
            //--- Log that the post-trade reset countdown was updated
            Print("Post-trade reset countdown updated: ", countdownText);
         }
      } else {
         //--- If 15 seconds have elapsed since traded news time, log the reset action
         Print("News Released. Resetting trade status after 15 seconds.");
         
         //--- If the countdown object exists, delete it from the chart
         if(ObjectFind(0, "NewsCountdown") >= 0)
            ObjectDelete(0, "NewsCountdown");
         //--- Reset the tradeExecuted flag to allow new trades
         tradeExecuted = false;
      }
   }
   //--- Exit the function as post-trade processing is complete
   return;
}

Здесь мы подробно рассмотрим сценарий обратного отсчета после совершения сделки. После совершения сделки мы сначала используем функцию TimeTradeServer для получения текущего времени сервера и его сравнения с tradedNewsTime, в котором хранится запланированное время выхода события-кандидата. Если текущее время еще предшествует tradedNewsTime, мы вычисляем оставшиеся секунды и преобразуем их в часы, минуты и секунды, создавая строку обратного отсчета в формате "News in: __h __m __s" (новости через: __ч __м __с) с помощью функции IntegerToString.

Затем мы проверяем наличие объекта NewsCountdown с помощью ObjectFind и либо создаем его (применяя пользовательскую функцию createButton1) с координатами X=50, Y=17, шириной 300, высотой 30 и синим фоном, либо обновляем (с опомщью updateLabel1), если объект уже существует. Однако если текущее время превысило tradedNewsTime, мы вычисляем прошедшее время. Если прошедшее время меньше 15 секунд, мы отображаем сообщение о сбросе в объекте обратного отсчета — "News Released, resetting in: XXs" (вышла новость, сброс через: XX с) — и явно устанавливаем его фоновый цвет на красный с помощью функции ObjectSetInteger.

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

if(currentTime >= targetTime && currentTime < candidateEventTime) {

        //---

}
else {
   //--- If current time is before the candidate event window, show a pre-trade countdown
   int remainingSeconds = (int)(candidateEventTime - currentTime);
   int hrs = remainingSeconds / 3600;
   int mins = (remainingSeconds % 3600) / 60;
   int secs = remainingSeconds % 60;
   //--- Construct the pre-trade countdown text
   string countdownText = "News in: " + IntegerToString(hrs) + "h " +
                          IntegerToString(mins) + "m " +
                          IntegerToString(secs) + "s";
   //--- If the countdown object does not exist, create it with specified dimensions and blue background
   if(ObjectFind(0, "NewsCountdown") < 0) {
      createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrBlue, clrBlack);
      //--- Log that the pre-trade countdown was created
      Print("Pre-trade countdown created: ", countdownText);
   } else {
      //--- If the countdown object exists, update its text
      updateLabel1("NewsCountdown", countdownText);
      //--- Log that the pre-trade countdown was updated
      Print("Pre-trade countdown updated: ", countdownText);
   }
}

Если текущее время не попадает в торговое окно события-кандидата, то есть если текущее время не больше или равно targetTime (рассчитываемому как запланированное время события-кандидата за вычетом смещения) и все еще не меньше запланированного времени события-кандидата, мы предполагаем, что текущее время все еще опережает торговое окно, поэтому мы вычисляем оставшееся время до события-кандидата, вычитая текущее время из запланированного времени события-кандидата, а затем преобразуем эту разницу в часы, минуты и секунды.

Используя IntegerToString, мы формируем текстовую строку обратного отсчета в формате "News in: __h __m __s". Затем мы используем функцию ObjectFind для проверки того, существует ли уже объект NewsCountdown. Если нет, создаем его с помощью функции createButton1 с указанными размерами (X=50, Y=17, ширина=300, высота=30) и синим фоном, регистрируя создание предторгового обратного отсчета, в противном случае обновляем его текст с помощью updateLabel1 и регистрируем обновление. Наконец, если после анализа не выбрано ни одного события, мы просто удаляем наши объекты.

//--- If no candidate event is selected, delete any existing countdown and trade info objects
if(ObjectFind(0, "NewsCountdown") >= 0) {
   ObjectDelete(0, "NewsCountdown");
   ObjectDelete(0, "NewsTradeInfo");
   //--- Log that the pre-trade countdown was deleted
   Print("Pre-trade countdown deleted.");
}

Если ни одно событие-кандидат не выбрано, то есть, если ни одно событие не соответствует критериям для выполнения сделки, мы проверяем наличие объекта NewsCountdown с помощью функции ObjectFind. Если он найден, удаляем оба объекта NewsCountdown и NewsTradeInfo с графика, вызывая функцию ObjectDelete, чтобы избавиться от устаревшей информации об обратном отсчете или торговле.

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

//--- Function to delete trade-related objects from the chart and redraw the chart
void deleteTradeObjects(){
   //--- Delete the countdown object from the chart
   ObjectDelete(0, "NewsCountdown");
   //--- Delete the news trade information label from the chart
   ObjectDelete(0, "NewsTradeInfo");
   //--- Redraw the chart to reflect the deletion of objects
   ChartRedraw();
}

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
//---

   destroy_Dashboard();
   deleteTradeObjects();
}

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

//--- Function to log active filter selections in the Experts log
void UpdateFilterInfo() {
   //--- Initialize the filter information string with the prefix "Filters: "
   string filterInfo = "Filters: ";
   //--- Check if the currency filter is enabled
   if(enableCurrencyFilter) {
      //--- Append the currency filter label to the string
      filterInfo += "Currency: ";
      //--- Loop through each selected currency filter option
      for(int i = 0; i < ArraySize(curr_filter_selected); i++) {
         //--- Append the current currency filter value
         filterInfo += curr_filter_selected[i];
         //--- If not the last element, add a comma separator
         if(i < ArraySize(curr_filter_selected) - 1)
            filterInfo += ",";
      }
      //--- Append a semicolon to separate this filter's information
      filterInfo += "; ";
   } else {
      //--- Indicate that the currency filter is turned off
      filterInfo += "Currency: Off; ";
   }
   //--- Check if the importance filter is enabled
   if(enableImportanceFilter) {
      //--- Append the impact filter label to the string
      filterInfo += "Impact: ";
      //--- Loop through each selected impact filter option
      for(int i = 0; i < ArraySize(imp_filter_selected); i++) {
         //--- Append the string representation of the current importance filter value
         filterInfo += EnumToString(imp_filter_selected[i]);
         //--- If not the last element, add a comma separator
         if(i < ArraySize(imp_filter_selected) - 1)
            filterInfo += ",";
      }
      //--- Append a semicolon to separate this filter's information
      filterInfo += "; ";
   } else {
      //--- Indicate that the impact filter is turned off
      filterInfo += "Impact: Off; ";
   }
   //--- Check if the time filter is enabled
   if(enableTimeFilter) {
      //--- Append the time filter information with the upper limit
      filterInfo += "Time: Up to " + EnumToString(end_time);
   } else {
      //--- Indicate that the time filter is turned off
      filterInfo += "Time: Off";
   }
   //--- Print the complete filter information to the Experts log
   Print("Filter Info: ", filterInfo);
}

Создадим пустую функцию UpdateFilterInfo. Сначала мы инициализируем строку с префиксом "Filters:" (фильтры:), а затем проверяем, включен ли фильтр валют. Если да, то добавляем Currency: (валюта:) и проходим по массиву curr_filter_selected с помощью ArraySize, добавляя каждую валюту (разделяя их запятыми) и завершая точкой с запятой. Если фильтр отключен, то просто записываем "Currency: Off;". Затем мы выполняем аналогичный процесс для фильтра важности: если он включен, мы добавляем "Impact: " (важность:) и перебираем imp_filter_selected, преобразуя каждый выбранный уровень воздействия в строку EnumToString перед их добавлением или указываем "Impact: Off; ", если не включен.

Наконец, мы обращаемся к фильтру времени, добавляя "Time: Up to " (время: до) вместе со строковым представлением входного параметра "end_time" (снова с помощью EnumToString) или Time: Off, если фильтр отключен. После объединения всех сегментов мы выводим полную информацию о фильтре в журнале "Эксперты", используя функцию Print. Это дает нам четкую картину действующих фильтров в реальном времени для устранения неполадок и проверки. Затем мы вызываем функции в обработчике OnChartEvent, а также OnTick.

//+------------------------------------------------------------------+
//|    OnChartEvent handler function                                 |
//+------------------------------------------------------------------+
void  OnChartEvent(
   const int       id,       // event ID  
   const long&     lparam,   // long type event parameter 
   const double&   dparam,   // double type event parameter 
   const string&   sparam    // string type event parameter 
){
      
   if (id == CHARTEVENT_OBJECT_CLICK){ //--- Check if the event is a click on an object
      UpdateFilterInfo();
      CheckForNewsTrade();
   }
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
//---
UpdateFilterInfo();
CheckForNewsTrade();
   if (isDashboardUpdate){
      update_dashboard_values(curr_filter_selected,imp_filter_selected);
   }
   
}

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

Конечный результат

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


Тестирование торговой логики

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



Заключение

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

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


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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Nikolay Moskalev
Nikolay Moskalev | 3 мар. 2025 в 16:06

Здравствуйте! Спасибо за проделанную работу.

У меня проблемы с отображением таблицы на 2К мониторе.

Allan Munene Mutiiria
Allan Munene Mutiiria | 3 мар. 2025 в 20:11
Nikolay Moskalev #:

Здравствуйте! Спасибо за проделанную работу.

У меня проблемы с отображением таблицы на 2К мониторе.

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

Hosna karkooti
Hosna karkooti | 28 июн. 2025 в 08:50
Здравствуйте, добрый день.
У меня есть несколько вопросов по вашему советнику. Буду очень признателен за ваши рекомендации:

1. Какие валютные пары вы рекомендуете торговать с помощью этого советника?


2. Какой метод вы рекомендуете использовать для закрытия сделок? (Стоп-лосс, по времени или другой подход?)


3. Какой минимальный баланс счета необходим для безопасного использования этого советника?


4. Если у меня есть счет на 200 долларов, какой файл или настройки вы бы порекомендовали?



Большое спасибо за поддержку. 🌹

Знакомство с языком MQL5 (Часть 13): Руководство для начинающих по созданию пользовательских индикаторов (II) Знакомство с языком MQL5 (Часть 13): Руководство для начинающих по созданию пользовательских индикаторов (II)
Эта статья проведет вас через создание пользовательского индикатора Heikin Ashi с нуля и продемонстрирует, как интегрировать пользовательские индикаторы в советник. В статье рассматриваются расчеты индикаторов, логика исполнения сделок и методы управления рисками для улучшения автоматизированных торговых стратегий.
Моделирование рынка (Часть 14): Сокеты (VIII) Моделирование рынка (Часть 14): Сокеты (VIII)
Многие программисты могут предположить, что нам следует отказаться от использования Excel и перейти непосредственно на Python, используя некоторые пакеты, позволяющие Python создавать Excel-файл, чтобы потом проанализировать результаты. Но, как уже говорилось в предыдущей статье, хотя это решение и является наиболее простым для многих программистов, оно не будет воспринято некоторыми пользователями. И в данном вопросе пользователь всегда прав. Мы, как программисты, должны найти способ заставить всё работать.
Моделирование рынка (Часть 15): Сокеты (IX) Моделирование рынка (Часть 15): Сокеты (IX)
В этой статье мы расскажем об одном из возможных решений того, что мы пытались показать, то есть как позволить пользователю Excel выполнить действие в MetaTrader 5 без отправки ордеров, открытия или закрытия позиции. Идея заключается в том, что пользователь использует Excel для проведения фундаментального анализа какого-то символа. И что при использовании только Excel, можно указать советнику, работающему в MetaTrader 5, открыть или закрыть определенную позицию.
Разрабатываем менеджер терминалов (Часть 3): Получаем информацию о счёте и добавляем конфигурацию Разрабатываем менеджер терминалов (Часть 3): Получаем информацию о счёте и добавляем конфигурацию
Добавляем в наше веб-приложение возможность получения и отображения информации о торговых счетах терминалов: о балансе, прибыли, статусе подключения и другой важной информации. Также реализуем гибкую систему конфигурации, позволяющую управлять параметрами приложения через внешний JSON-файл, и улучшаем пользовательский интерфейс главной страницы.