English Deutsch 日本語
preview
Разработка советника для анализа новостных событий о пробоях на основе календаря на MQL5

Разработка советника для анализа новостных событий о пробоях на основе календаря на MQL5

MetaTrader 5Примеры |
311 5
Zhuo Kai Chen
Zhuo Kai Chen

Введение

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


Мотивация

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

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


Бэк-тестирование календарных новостей

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

  • Класс CCalendarEntry представляет собой одно событие экономического календаря с различными свойствами, относящимися к стране, деталями события и связанными с ним значениями (например, прогнозируемыми, фактическими, предыдущими значениями и т.д.).
  • Метод Compare() сравнивает два события календаря на основе их времени и важности, возвращая значение, указывающее, какое событие считается более важным. 
  • Метод ToString() преобразует данные о событии в удобочитаемый формат строки, включая важность события и другие соответствующие свойства.

//+------------------------------------------------------------------+
//| A class to represent a single economic calendar event            |
//+------------------------------------------------------------------+
class CCalendarEntry :public CObject {
public:
   ulong country_id;
   string country_name;
   string country_code;
   string country_currency;
   string country_currency_symbol;
   string country_url_name;
   
   ulong event_id;
   ENUM_CALENDAR_EVENT_TYPE event_type;
   ENUM_CALENDAR_EVENT_SECTOR event_sector;
   ENUM_CALENDAR_EVENT_FREQUENCY event_frequency;
   ENUM_CALENDAR_EVENT_TIMEMODE event_time_mode;
   ENUM_CALENDAR_EVENT_UNIT event_unit;
   ENUM_CALENDAR_EVENT_IMPORTANCE event_importance;
   ENUM_CALENDAR_EVENT_MULTIPLIER event_multiplier;
   uint event_digits;
   string event_source_url;
   string event_event_code;
   string event_name;
      
   ulong value_id;
   datetime value_time;
   datetime value_period;
   int value_revision;
   long value_actual_value;
   long value_prev_value;
   long value_revised_prev_value;
   long value_forecast_value;
   ENUM_CALENDAR_EVENT_IMPACT value_impact_type;
   
//+------------------------------------------------------------------+
//| Compare news importance function                                 |
//+------------------------------------------------------------------+
   int Compare(const CObject *node, const int mode = 0) const{
      CCalendarEntry* other = (CCalendarEntry*)node;
      if (value_time==other.value_time){
         return event_importance-other.event_importance;
      }
      return (int)(value_time -other.value_time);
   }
   
//+------------------------------------------------------------------+
//| Convert data to string function                                  |
//+------------------------------------------------------------------+
   string ToString(){
      string txt;
      string importance = "None";
      if(event_importance==CALENDAR_IMPORTANCE_HIGH)importance="High";
      else if(event_importance==CALENDAR_IMPORTANCE_MODERATE) importance = "Moderate";
      else if(event_importance==CALENDAR_IMPORTANCE_LOW)importance = "Low";
      StringConcatenate(txt,value_time,">",event_name,"(",country_code,"|",country_currency,")",importance);
      return txt;
     }
   
};

  • Класс  CCalendarHistory  управляет коллекцией объектов класса CCalendarEnrtry, расширяя функциональность CArrayObj  для работы с массивами и предоставлет методы для доступа к данным о событиях календаря и управления ими. 
  • Метод  operator[]  переопределен для возврата объекта  CCalendarEnrtry  с определенным индексом в коллекции, что позволяет получать доступ в виде массива к записям календаря. 
  • Метод  At()  возвращает указатель на CCalendarEnrtry  с указанным индексом. Это гарантирует, что индекс действителен до получения доступа к массиву.
  • Метод LoadCalendarEntriesFromFile()  загружает записи календаря из двоичного файла, считывая соответствующие данные (например, информацию о стране, сведения о событии) и заполняя объекты Ccalendarentry .

//+------------------------------------------------------------------+
//| A class to manage a collection of CCalendarEntry objects         |
//+------------------------------------------------------------------+
class CCalendarHistory :public CArrayObj{
public:
//overriding existing operators to better deal with calendar format data
   CCalendarEntry *operator[](const int index) const{return(CCalendarEntry*)At(index);}
   CCalendarEntry *At (const int index) const;
   bool LoadCalendarEntriesFromFile(string fileName);
   bool SaveCalendarValuesToFile(string filename);
   
};

CCalendarEntry *CCalendarHistory::At(const int index)const{
   if(index<0||index>=m_data_total)return(NULL);
   return (CCalendarEntry*)m_data[index];
   
}

//+------------------------------------------------------------------+
//| A function to load calendar events from your saved binary file   |
//+------------------------------------------------------------------+
bool CCalendarHistory::LoadCalendarEntriesFromFile(string fileName){
   CFileBin file;
   if(file.Open(fileName,FILE_READ|FILE_COMMON)>0){
      while(!file.IsEnding()){
         CCalendarEntry*entry = new CCalendarEntry();
         int len;
         file.ReadLong(entry.country_id);
         file.ReadInteger(len);
         file.ReadString(entry.country_name,len);
         file.ReadInteger(len);
         file.ReadString(entry.country_code,len);
         file.ReadInteger(len);
         file.ReadString(entry.country_currency,len);
         file.ReadInteger(len);
         file.ReadString(entry.country_currency_symbol,len);
         file.ReadInteger(len);
         file.ReadString(entry.country_url_name,len);
         
         file.ReadLong(entry.event_id);
         file.ReadEnum(entry.event_type);
         file.ReadEnum(entry.event_sector);
         file.ReadEnum(entry.event_frequency);
         file.ReadEnum(entry.event_time_mode);
         file.ReadEnum(entry.event_unit);
         file.ReadEnum(entry.event_importance);
         file.ReadEnum(entry.event_multiplier);
         file.ReadInteger(entry.event_digits);
         file.ReadInteger(len);
         file.ReadString(entry.event_source_url,len);
         file.ReadInteger(len);
         file.ReadString(entry.event_event_code,len);
         file.ReadInteger(len);
         file.ReadString(entry.event_name,len);
         
         file.ReadLong(entry.value_id);
         file.ReadLong(entry.value_time);
         file.ReadLong(entry.value_period);
         file.ReadInteger(entry.value_revision);
         file.ReadLong(entry.value_actual_value);
         file.ReadLong(entry.value_prev_value);
         file.ReadLong(entry.value_revised_prev_value);
         file.ReadLong(entry.value_forecast_value);
         file.ReadEnum(entry.value_impact_type);
         
         CArrayObj::Add(entry);        
      }
      Print(__FUNCTION__,">Loaded",CArrayObj::Total(),"Calendar Entries From",fileName,"...");
      CArray::Sort();
      file.Close();
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| A function to save calendar values into a binary file            |
//+------------------------------------------------------------------+
bool CCalendarHistory::SaveCalendarValuesToFile(string fileName){
   CFileBin file;
   if(file.Open(fileName,FILE_WRITE|FILE_COMMON)>0){
     datetime chunk_end   = TimeTradeServer();
      // Let's do ~12 months (adjust as needed).
      int months_to_fetch  = 12*25;  
      
      while(months_to_fetch > 0)
      {
         // For each month, we go back ~30 days
         datetime chunk_start = chunk_end - 30*24*60*60;
         if(chunk_start < 1) // Just a safety check
            chunk_start = 1;
         MqlCalendarValue values[];
         if(CalendarValueHistory(values, chunk_start, chunk_end))
         {
            // Write to file
            for(uint i = 0; i < values.Size(); i++)
            {
               MqlCalendarEvent event;
               if(!CalendarEventById(values[i].event_id,event))
                  continue;  // skip if not found
               
               MqlCalendarCountry country;
               if(!CalendarCountryById(event.country_id,country))
                  continue;  // skip if not found
      
       file.WriteLong(country.id);
       file.WriteInteger(country.name.Length());
       file.WriteString(country.name,country.name.Length());
       file.WriteInteger(country.code.Length());
       file.WriteString(country.code,country.code.Length());
       file.WriteInteger(country.currency.Length());
       file.WriteString(country.currency,country.currency.Length());
       file.WriteInteger(country.currency_symbol.Length());
       file.WriteString(country.currency_symbol, country.currency_symbol.Length());
       file.WriteInteger(country.url_name.Length());
       file.WriteString(country.url_name,country.url_name.Length());
       
       file.WriteLong(event.id);
       file.WriteEnum(event.type);
       file.WriteEnum(event.sector);
       file.WriteEnum(event.frequency);
       file.WriteEnum(event.time_mode);
       file.WriteEnum(event.unit);
       file.WriteEnum(event.importance);
       file.WriteEnum(event.multiplier);
       file.WriteInteger(event.digits);
       file.WriteInteger(event.source_url.Length());
       file.WriteString(event.source_url,event.source_url.Length());
       file.WriteInteger(event.event_code.Length());
       file.WriteString(event.event_code,event.event_code.Length());
       file.WriteInteger(event.name.Length());
       file.WriteString(event.name,event.name.Length());
       
       file.WriteLong(values[i].id);
       file.WriteLong(values[i].time);
       file.WriteLong(values[i].period);
       file.WriteInteger(values[i].revision);
       file.WriteLong(values[i].actual_value);
       file.WriteLong(values[i].prev_value);
       file.WriteLong(values[i].revised_prev_value);
       file.WriteLong(values[i].forecast_value);
       file.WriteEnum(values[i].impact_type);
     }
            Print(__FUNCTION__, " >> chunk ", 
                  TimeToString(chunk_start), " - ", TimeToString(chunk_end), 
                  ": saved ", values.Size(), " events.");
         }
         
         // Move to the previous chunk
         chunk_end = chunk_start;
         months_to_fetch--;
         
         // short pause to avoid spamming server:
         Sleep(500);
      }
     
     file.Close();
     return true;
   }
   return false;
}

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

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

Разработка торговой стратегии пробоя на основе новостей, при buy/sell стопы устанавливаются на ключевых уровнях непосредственно перед важными новостными событиями, может быть продиктована несколькими стратегическими и тактическими соображениями:

  • Импульсный ход: Важные новости часто вызывают значительные колебания цен. Размещение стоп-ордеров вблизи ключевых уровней может помочь трейдерам войти в рынок сразу же после пробоя, фиксируя значительные колебания цены.
  • Повышенное соотношение риска и вознаграждения: Быстрые, волатильные движения могут привести к благоприятному соотношению риска и прибыли, если стопы трейдера срабатывают в направлении пробоя.
  • Предсказуемость выпусков новостей: Поскольку время выхода важных новостей известно заранее, трейдеры могут точнее планировать входы и выходы, снижая неопределенность в отношении рыночного тайминга.
  • Ожидание резкого роста ликвидности: Выпуски новостей часто привлекают повышенное внимание участников рынка, что может привести к более надежным пробоям при пробитии ключевых уровней.
  • Заранее запланированное исполнение: Установка стоп-сигналов на ключевых технических уровнях до выхода новостей помогает избежать эмоционального принятия решений в момент рыночного шока, что приводит к более дисциплинированному исполнению.
  • Возможность автоматизации: Предварительное размещение ордеров может обеспечить автоматическое исполнение непосредственно сразу после появления новостей, обеспечивая быструю реакцию на изменения рынка без необходимости ручного вмешательства.

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

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

#define FILE_NAME "CalendarHistory.bin"
#include <Trade/Trade.mqh>
#include <CalendarHistory.mqh>
#include <Arrays/ArrayString.mqh>
CCalendarHistory calendar;
CTrade trade;
CArrayString curr;

ulong poss, buypos = 0, sellpos=0;
input int Magic = 0;
int barsTotal = 0;
int currentIndex = 0;
datetime s_lastUpdate = 0;
input int closeTime = 18;
input int slp = 1000;
input int Deviation = 1000;
input string Currencies = "USD";
input ENUM_CALENDAR_EVENT_IMPORTANCE Importance = CALENDAR_IMPORTANCE_HIGH;
input bool saveFile = true;

Функция инициализатора OnInit() обеспечивает следующее:

  • Если saveFile  имеет значение true, записи календаря сохраняются в файл с именем "CalendarHistory.bin".
  • Затем из этого файла загружаются события календаря. Но вы не можете сохранить и загрузить файл одновременно, потому что метод сохранения в конечном итоге закрывает файл.
  • Входная строка переменной Currencies  разбивается на массив отдельных валют, и этот массив сортируется. Итак, если вам нужны события, связанные как с USD, так и с EUR, просто введите "USD";"EUR".
  • Объекту CTrade  присваивается Магический  номер для идентификации сделок, инициированных этим советником.

//+------------------------------------------------------------------+
//| Initializer function                                             |
//+------------------------------------------------------------------+
int OnInit() {
   if(saveFile==true)calendar.SaveCalendarValuesToFile(FILE_NAME);
   calendar.LoadCalendarEntriesFromFile(FILE_NAME);
   string arr[];
   StringSplit(Currencies,StringGetCharacter(";",0),arr);
   curr.AddArray(arr);
   curr.Sort();
   trade.SetExpertMagicNumber(Magic);  
   return(INIT_SUCCEEDED);
}

Вот необходимые функции для выполнения задач по исполнению.

  • OnTradeTransaction: Отслеживает входящие торговые транзакции и обновляет позиции на покупку (buypos)  или продажу (sellpos) с помощью тикета ордера при добавлении ордера на покупку или продажу с указанным магическим номером.
  • executeBuy: Размещает ордер buy stop по заданной цене с рассчитанным стоп-лоссом и записывает полученный тикет ордера в buypos.
  • executeSell: Размещает ордер sell stop по заданной цене с рассчитанным стоп-лоссом и записывает полученный тикет ордера в sellpos.
  • IsCloseTime: Проверяет текущее время сервера, чтобы определить, прошел ли заданный час закрытия.
//+------------------------------------------------------------------+
//| A function for handling trade transaction                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) {
    if (trans.type == TRADE_TRANSACTION_ORDER_ADD) {
        COrderInfo order;
        if (order.Select(trans.order)) {
            if (order.Magic() == Magic) {
                if (order.OrderType() == ORDER_TYPE_BUY) {
                    buypos = order.Ticket();
                } else if (order.OrderType() == ORDER_TYPE_SELL) {
                    sellpos = order.Ticket();
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+
void executeBuy(double price) {
       double sl = price- slp*_Point;
       sl = NormalizeDouble(sl, _Digits);
       double lots=0.1;
       trade.BuyStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1);
       buypos = trade.ResultOrder();
       }

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+
void executeSell(double price) {
       double sl = price + slp * _Point;
       sl = NormalizeDouble(sl, _Digits);
       double lots=0.1;
       trade.SellStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1);
       sellpos = trade.ResultOrder();
       }
       
//+------------------------------------------------------------------+
//| Exit time boolean function                                       |
//+------------------------------------------------------------------+
bool IsCloseTime(){
   datetime currentTime = TimeTradeServer();
   MqlDateTime timeStruct;
   TimeToStruct(currentTime,timeStruct);
   int currentHour =timeStruct.hour;
   return(currentHour>closeTime);
}

Наконец, мы просто реализуем логику исполнения в функции OnTick().

//+------------------------------------------------------------------+
//| OnTick function                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
    int bars = iBars(_Symbol,PERIOD_CURRENT);
  
    if (barsTotal!= bars){
      barsTotal = bars;
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   datetime now = TimeTradeServer();
   datetime horizon = now + 5*60; // 5 minutes from now

   while (currentIndex < calendar.Total())
   {   
         CCalendarEntry*entry=calendar.At(currentIndex);
         if (entry.value_time < now)
         {
            currentIndex++;
            continue;
         }
         // Now if the next event time is beyond horizon, break out
         if (entry.value_time > horizon) 
            break;
         
         // If it is within the next 5 minutes, check other conditions:
         if (entry.event_importance >= Importance && curr.SearchFirst(entry.country_currency) >= 0 && buypos == sellpos )
         {
             double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
             executeBuy(bid + Deviation*_Point);
             executeSell(bid - Deviation*_Point);
         }
         currentIndex++;
      }
    if(IsCloseTime()){
       for(int i = 0; i<PositionsTotal(); i++){
         poss = PositionGetTicket(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss);      
      }
    }
     if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      buypos = 0;
      }
     if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      sellpos = 0;
      }     

   }
}

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

    int bars = iBars(_Symbol,PERIOD_CURRENT);
  
    if (barsTotal!= bars){
      barsTotal = bars;

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

while (currentIndex < calendar.Total())
   {   
         CCalendarEntry*entry=calendar.At(currentIndex);
         if (entry.value_time < now)
         {
            currentIndex++;
            continue;
         }
         // Now if the next event time is beyond horizon, break out
         if (entry.value_time > horizon) 
            break;
         
         // If it is within the next 5 minutes, check other conditions:
         if (entry.event_importance >= Importance && curr.SearchFirst(entry.country_currency) >= 0 && buypos == sellpos )
         {
             double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
             executeBuy(bid + Deviation*_Point);
             executeSell(bid - Deviation*_Point);
         }
         currentIndex++;
      }  

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

    if(IsCloseTime()){
       for(int i = 0; i<PositionsTotal(); i++){
         poss = PositionGetTicket(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss);      
      }
    }
     if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      buypos = 0;
      }
     if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      sellpos = 0;
      }     

Теперь скомпилируем программу и перейдем в терминал MetaTrader 5. Откроем любой график и перетянем этот советник на график следующим образом:

Dragging EA

Убедимся, что для параметра saveFile установлено значение "true".

set to true

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

Теперь можно протестировать стратегию в тестере стратегий.

parameters

Важные замечания о параметрах вашего бэк-тестирования:

  • Установим для переменной saveFile  значение "false", чтобы предотвратить закрытие двоичного файла данных при инициализации.

  • Выберем обоснованное отклонение и стоп-лосс. Слишком широкое отклонение не сможет отразить волатильность новостного события, в то время как слишком узкое отклонение или стоп-лосс будут подвержены проскальзыванию во время всплесков новостей.

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

Вот мой бэк-тест на SPIUSDc за период с 01.01.2019 по 12.01.2024 на 5–минутном таймфрейме.

установка

кривая эквити

результат

Ключевые результаты:

  • Коэффициент прибыльности: 1.26
  • Коэффициент Шарпа: 2.66
  • Количество сделок: 1604

Наконец, я предлагаю читателям использовать реальные тики, если таковые имеются. Для учета проскальзываний и высоких спредов во время важных новостных событий выберите стресс-тестирование с высокой задержкой. И последнее, но не менее важное: торгуйте на демо-счете в реальных условиях, чтобы убедиться в достоверности результатов вашего тестирования. У некоторых брокеров наблюдается значительное проскальзывание, в то время как у других оно может быть минимальным. При торговле по стратегии, основанной на высокой волатильности, всегда принимайте дополнительные меры для обеспечения прибыльности стратегии при реальной торговле.


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

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

Логика этого кода обновляет историю календаря, извлекая новые данные о событиях из API календаря, если с момента последнего обновления прошло более часа, затем обрабатывает и сохраняет сведения о событии, включая данные о стране, значении и прогнозе, в новом объекте CCalendarEntry  для каждого события.

//+------------------------------------------------------------------+
//| Update upcoming news events                                      |
//+------------------------------------------------------------------+
void UpdateCalendarHistory(CCalendarHistory &history)
{
   //upcoming event in the next hour
   datetime fromTime = TimeTradeServer()+3600;
   // For example, if it's been > 1hr since last update:
   if(fromTime - s_lastUpdate > 3600)
   {
      // Determine the time range to fetch new events
      // For instance, from s_lastUpdate to 'now'
      MqlCalendarValue values[];
      if(CalendarValueHistory(values, s_lastUpdate, fromTime))
      {
         for(uint i = 0; i < values.Size(); i++)
         {
            MqlCalendarEvent event;
            if(!CalendarEventById(values[i].event_id,event)) 
               continue;           
            MqlCalendarCountry country;
            if(!CalendarCountryById(event.country_id, country))
               continue;          
            // Create a new CCalendarEntry and fill from 'values[i]', 'event', 'country'
            CCalendarEntry *entry = new CCalendarEntry();
            entry.country_id = country.id;
            entry.value_time               = values[i].time;
            entry.value_period             = values[i].period;
            entry.value_revision           = values[i].revision;
            entry.value_actual_value       = values[i].actual_value;
            entry.value_prev_value         = values[i].prev_value;
            entry.value_revised_prev_value = values[i].revised_prev_value;
            entry.value_forecast_value     = values[i].forecast_value;
            entry.value_impact_type        = values[i].impact_type;
            // event data
            entry.event_id             = event.id;
            entry.event_type           = event.type;
            entry.event_sector         = event.sector;
            entry.event_frequency      = event.frequency;
            entry.event_time_mode      = event.time_mode;
            entry.event_unit           = event.unit;
            entry.event_importance     = event.importance;
            entry.event_multiplier     = event.multiplier;
            entry.event_digits         = event.digits;
            entry.event_source_url     = event.source_url;
            entry.event_event_code     = event.event_code;
            entry.event_name           = event.name;
            // country data
            entry.country_name         = country.name;
            entry.country_code         = country.code;
            entry.country_currency     = country.currency;
            entry.country_currency_symbol = country.currency_symbol;
            entry.country_url_name     = country.url_name;
            // Add to your in-memory calendar
            history.Add(entry);
         }
      }
      // Sort to keep chronological order
      history.Sort();     
      // Mark the last update time
      s_lastUpdate = fromTime;
   }
}

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

#include <Trade/Trade.mqh>
#include <CalendarHistory.mqh>
#include <Arrays/ArrayString.mqh>
CCalendarHistory calendar;
CArrayString curr;
CTrade trade;

ulong poss, buypos = 0, sellpos=0;
input int Magic = 0;
int barsTotal = 0;
datetime s_lastUpdate = 0;
input int closeTime = 18;
input int slp = 1000;
input int Deviation = 1000;
input string Currencies = "USD";
input ENUM_CALENDAR_EVENT_IMPORTANCE Importance = CALENDAR_IMPORTANCE_HIGH;

//+------------------------------------------------------------------+
//| Initializer function                                             |
//+------------------------------------------------------------------+
int OnInit() {
   trade.SetExpertMagicNumber(Magic);  
   string arr[];
   StringSplit(Currencies,StringGetCharacter(";",0),arr);
   curr.AddArray(arr);
   curr.Sort();
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Destructor function                                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   
  }

//+------------------------------------------------------------------+
//| OnTick function                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
    int bars = iBars(_Symbol,PERIOD_CURRENT);
  
    if (barsTotal!= bars){
      barsTotal = bars;
      UpdateCalendarHistory(calendar);
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   datetime now = TimeTradeServer();
   datetime horizon = now + 5*60; // 5 minutes from now

   // Loop over all loaded events
   for(int i = 0; i < calendar.Total(); i++)
   {
      CCalendarEntry *entry = calendar.At(i);

      // If event time is between 'now' and 'now+5min'
      if(entry.value_time > now && entry.value_time <= horizon&&buypos==sellpos&&entry.event_importance>=Importance&&curr.SearchFirst(entry.country_currency)>=0)
      {
        executeBuy(bid+Deviation*_Point);
        executeSell(bid-Deviation*_Point);
        }
     }
    if(IsCloseTime()){
       for(int i = 0; i<PositionsTotal(); i++){
         poss = PositionGetTicket(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss);      
      }
    }
     if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      buypos = 0;
      }
     if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      sellpos = 0;
      }     
   }
}

//+------------------------------------------------------------------+
//| A function for handling trade transaction                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) {
    if (trans.type == TRADE_TRANSACTION_ORDER_ADD) {
        COrderInfo order;
        if (order.Select(trans.order)) {
            if (order.Magic() == Magic) {
                if (order.OrderType() == ORDER_TYPE_BUY) {
                    buypos = order.Ticket();
                } else if (order.OrderType() == ORDER_TYPE_SELL) {
                    sellpos = order.Ticket();
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+
void executeBuy(double price) {
       double sl = price- slp*_Point;
       sl = NormalizeDouble(sl, _Digits);
       double lots=0.1;
       trade.BuyStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1);
       buypos = trade.ResultOrder();
       }

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+
void executeSell(double price) {
       double sl = price + slp * _Point;
       sl = NormalizeDouble(sl, _Digits);
       double lots=0.1;
       trade.SellStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1);
       sellpos = trade.ResultOrder();
       }

//+------------------------------------------------------------------+
//| Update upcoming news events                                      |
//+------------------------------------------------------------------+
void UpdateCalendarHistory(CCalendarHistory &history)
{
   //upcoming event in the next hour
   datetime fromTime = TimeTradeServer()+3600;
   // For example, if it's been > 1hr since last update:
   if(fromTime - s_lastUpdate > 3600)
   {
      // Determine the time range to fetch new events
      // For instance, from s_lastUpdate to 'now'
      MqlCalendarValue values[];
      if(CalendarValueHistory(values, s_lastUpdate, fromTime))
      {
         for(uint i = 0; i < values.Size(); i++)
         {
            MqlCalendarEvent event;
            if(!CalendarEventById(values[i].event_id,event)) 
               continue;           
            MqlCalendarCountry country;
            if(!CalendarCountryById(event.country_id, country))
               continue;          
            // Create a new CCalendarEntry and fill from 'values[i]', 'event', 'country'
            CCalendarEntry *entry = new CCalendarEntry();
            entry.country_id = country.id;
            entry.value_time               = values[i].time;
            entry.value_period             = values[i].period;
            entry.value_revision           = values[i].revision;
            entry.value_actual_value       = values[i].actual_value;
            entry.value_prev_value         = values[i].prev_value;
            entry.value_revised_prev_value = values[i].revised_prev_value;
            entry.value_forecast_value     = values[i].forecast_value;
            entry.value_impact_type        = values[i].impact_type;
            // event data
            entry.event_id             = event.id;
            entry.event_type           = event.type;
            entry.event_sector         = event.sector;
            entry.event_frequency      = event.frequency;
            entry.event_time_mode      = event.time_mode;
            entry.event_unit           = event.unit;
            entry.event_importance     = event.importance;
            entry.event_multiplier     = event.multiplier;
            entry.event_digits         = event.digits;
            entry.event_source_url     = event.source_url;
            entry.event_event_code     = event.event_code;
            entry.event_name           = event.name;
            // country data
            entry.country_name         = country.name;
            entry.country_code         = country.code;
            entry.country_currency     = country.currency;
            entry.country_currency_symbol = country.currency_symbol;
            entry.country_url_name     = country.url_name;
            // Add to your in-memory calendar
            history.Add(entry);
         }
      }
      // Sort to keep chronological order
      history.Sort();     
      // Mark the last update time
      s_lastUpdate = fromTime;
   }
}

//+------------------------------------------------------------------+
//| Exit time boolean function                                       |
//+------------------------------------------------------------------+
bool IsCloseTime(){
   datetime currentTime = TimeTradeServer();
   MqlDateTime timeStruct;
   TimeToStruct(currentTime,timeStruct);
   int currentHour =timeStruct.hour;
   return(currentHour>closeTime);
}

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

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

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

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

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

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

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



Заключение

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


Таблица файлов

Название файла Использование файла
CalendarHistory.mqh Вспомогательный включаемый файл для обработки данных о новостных календарных событиях.
News Breakout Backtest.mq5 Советник для хранения данных о новостных событиях и проведения бэк-тестов.
News Breakout.mq5 Советник для реальной торговли.

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
hrawoodward
hrawoodward | 16 мая 2025 в 06:47

Привет, это замечательно, спасибо! Я немного запутался с вводом нескольких валют. Я пробовал:

"USD"; "GBP"

"USD"; "GBP".

"USD" "GBP";

Только последний вариант не выдает ошибку, но я не уверен, что он работает правильно. Может быть, он принимает только USD. Можете ли вы посоветовать?

Zhuo Kai Chen
Zhuo Kai Chen | 16 мая 2025 в 10:28
hrawoodward #:

Привет, это замечательно, спасибо! Я немного запутался с вводом нескольких валют. Я пробовал:

"USD"; "GBP"

"USD"; "GBP".

"USD" "GBP";

Только последний вариант не выдает ошибку, но я не уверен, что он работает правильно. Может быть, он принимает только USD. Можете ли вы посоветовать?

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

настройки

результат

Stanislav Korotky
Stanislav Korotky | 16 мая 2025 в 16:22
Похоже, что эта реализация не учитывает переключение часовых поясов (DST) на сервере брокера и, следовательно, дает неточные результаты при бэктестинге и оптимизации.
Zhuo Kai Chen
Zhuo Kai Chen | 17 мая 2025 в 03:17
Stanislav Korotky #:
Похоже, что эта реализация не учитывает переключение часовых поясов (DST) на сервере брокера и, следовательно, дает неточные результаты при бэктестинге и оптимизации.

Спасибо, что напомнили! Я забыл учесть это в статье, так как использовал для демонстрации брокера, у которого нет DST.

https://www.mql5.com/ru/book/advanced/calendar

Из этого источника мы знаем, что данные календаря предоставляются со стороны MQL5, и они автоматически подстраиваются под текущий часовой пояс брокера Timetradeserver(), а это значит, что для брокеров с DST нужно будет скорректировать мой код и учесть это.

Stanislav Korotky
Stanislav Korotky | 17 мая 2025 в 13:05
Zhuo Kai Chen #:

Из этого источника мы знаем, что данные календаря предоставляются со стороны MQL5, и они автоматически подстраиваются под текущий часовой пояс брокера Timetradeserver(), а это значит, что для брокеров с DST нужно будет скорректировать мой код и учесть это.

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

Разработка инструментария для анализа движения цен (Часть 2): Скрипт аналитических комментариев Разработка инструментария для анализа движения цен (Часть 2): Скрипт аналитических комментариев
В продолжение нашей работы по упрощению взаимодействия с поведением цены мы рады представить еще один инструмент, который может значительно улучшить ваш анализ рынка и помочь вам принимать обоснованные решения. Этот инструмент отображает ключевые технические индикаторы, такие как цены предыдущего дня, значимые уровни поддержки и сопротивления, а также торговый объем, автоматически генерируя визуальные подсказки на графике.
Нейросети в трейдинге: Вероятностное прогнозирование временных рядов (K2VAE) Нейросети в трейдинге: Вероятностное прогнозирование временных рядов (K2VAE)
Предлагаем ознакомиться с оригинальной реализацией фреймворка K²VAE — гибкой модели, способной линейно аппроксимировать сложную динамику в латентном пространстве. В статье показано, как реализовать ключевые компоненты на языке MQL5, включая параметризованные матрицы и их управление вне стандартных нейросетевых слоёв. Материал будет полезен тем, кто ищет практический подход к созданию интерпретируемых моделей временных рядов.
Создание торговой панели администратора на MQL5 (Часть VI): Панель управления торговлей (II) Создание торговой панели администратора на MQL5 (Часть VI): Панель управления торговлей (II)
В этой статье мы улучшим панель управления торговлей нашей многофункциональной панели администратора. Мы представим мощную вспомогательную функцию, которая упрощает код, улучшая его читаемость, удобство обслуживания и эффективность. Мы также продемонстрируем, как легко интегрировать дополнительные кнопки и улучшить интерфейс для решения более широкого спектра торговых задач. Независимо от того, управляете ли вы позициями, корректируете ордера или упрощаете взаимодействие с пользователем, это руководство поможет вам разработать надежную и удобную панель управления торговлей.
Применение модели машинного обучения CatBoost в качестве фильтра для трендовых стратегий Применение модели машинного обучения CatBoost в качестве фильтра для трендовых стратегий
CatBoost – это эффективная модель машинного обучения на основе деревьев, которая специализируется на принятии решений на основе статических признаков. Другие модели на основе деревьев, такие как XGBoost и Random Forest, обладают схожими характеристиками в плане надежности, интерпретируемости и способности работать со сложными паттернами. Эти модели имеют широкий спектр применения: от анализа признаков до управления рисками. В данной статье мы пройдемся по процедуре использования обученной модели CatBoost в качестве фильтра для классической трендовой стратегии на основе пересечения скользящих средних. Цель данной статьи заключается в том, чтобы дать представление о процессе разработки стратегии, а также предоставить решения задач, с которыми можно при этом столкнуться. Я представлю свой рабочий процесс по выборке данных из MetaTrader 5, обучению модели машинного обучения на языке Python и обратной интеграции в советники MetaTrader 5. К концу данной статьи мы проверим стратегию посредством статистического тестирования и обсудим планы на будущее, основанные на текущем подходе.