English Русский 中文 Deutsch 日本語 Português
preview
Desarrollo de un asesor experto para el análisis de eventos de noticias basados en el calendario en MQL5

Desarrollo de un asesor experto para el análisis de eventos de noticias basados en el calendario en MQL5

MetaTrader 5Ejemplos |
200 6
Zhuo Kai Chen
Zhuo Kai Chen

Introducción

La volatilidad tiende a alcanzar su punto máximo alrededor de eventos noticiosos de alto impacto, lo que crea oportunidades de ruptura significativas. En este artículo, describiremos el proceso de implementación de una estrategia de ruptura basada en calendario en MQL5. Cubriremos todo, desde la creación de una clase para interpretar y almacenar datos del calendario, el desarrollo de backtests realistas utilizando estos datos y, finalmente, la implementación del código de ejecución para operaciones en vivo.


Motivación

Si bien la comunidad MQL5 ofrece numerosos artículos y bases de código sobre el manejo de calendarios de MetaTrader 5 en backtesting, estos recursos pueden ser demasiado complejos para principiantes que buscan desarrollar una estrategia de ruptura simple. Este artículo busca simplificar el proceso de creación de una estrategia utilizando el calendario de noticias en MQL5 y proporcionar una guía completa para los traders.

La motivación para crear una estrategia de trading con noticias de calendario radica en aprovechar el momento predecible de los eventos noticiosos programados (como informes económicos, publicaciones de ganancias o anuncios geopolíticos) que a menudo desencadenan una volatilidad significativa del mercado y movimientos de precios. Al anticipar estos eventos, los operadores buscan aprovechar las oportunidades de ruptura cuando los precios se mueven decisivamente más allá de los niveles de soporte o resistencia establecidos después de las noticias. Esta estrategia busca maximizar las ganancias a partir del aumento de liquidez y el impulso en torno a los comunicados de prensa, al tiempo que emplea una gestión de riesgos disciplinada para navegar la mayor incertidumbre. En última instancia, proporciona un enfoque estructurado para explotar los patrones y reacciones que suelen ocurrir en los mercados en torno a eventos clave del calendario.


Prueba retrospectiva de noticias del calendario

MQL5 tiene operaciones predeterminadas para manejar los datos del Calendario de noticias proporcionados por los corredores. Sin embargo, estos datos no se pueden obtener en el probador de estrategias utilizando el código existente. Por lo tanto, podemos crear un archivo de inclusión de historial de calendario (include) adaptado de Rene Balke, que procese los datos del historial de noticias y los almacene en un archivo binario que utilizaremos más adelante.

  • La clase CCalendarEntry representa un único evento del calendario económico con diversas propiedades relacionadas con el país, los detalles del evento y sus valores asociados (por ejemplo, previsiones, valores reales, valores anteriores, etc.).
  • El método Compare() compara dos eventos del calendario en función de su hora e importancia, y devuelve un valor que indica qué evento se considera más importante. 
  • El método ToString() convierte los datos del evento en un formato de cadena legible para los humanos, incluyendo la importancia del evento y otras propiedades relevantes.

//+------------------------------------------------------------------+
//| 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;
     }
   
};

  • La clase CCalendarHistory gestiona una colección de objetos de la clase CCalendarEnrtry, ampliando CArrayObj para ofrecer una funcionalidad similar a la de una matriz, y proporciona métodos para acceder y manipular los datos de los eventos del calendario. 
  • El método operator[] se sobrescribe para devolver un objeto CCalendarEnrtry en un índice específico de la colección, lo que permite el acceso de tipo matriz a las entradas del calendario. 
  • El método At() devuelve un puntero a un CCalendarEnrtry en un índice especificado. Garantiza que el índice sea válido antes de acceder a la matriz.
  • El método LoadCalendarEntriesFromFile() carga entradas del calendario desde un archivo binario, lee los datos relevantes (por ejemplo, información del país, detalles del evento) y rellena objetos CCalendarEnrtry.

//+------------------------------------------------------------------+
//| 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;
}

A continuación, crearemos el asesor experto responsable de obtener los resultados de las pruebas retrospectivas.

Para este asesor experto, lo ejecutaremos en un marco temporal de 5 minutos. Para cada barra cerrada, comprobamos si hay alguna noticia de gran repercusión en los próximos 5 minutos. Si es así, colocamos órdenes de compra y venta con stop dentro de una desviación determinada del precio de oferta actual, con un stop loss opcional. Además, saldremos de todas las posiciones abiertas una vez que se alcance un tiempo específico.

El desarrollo de una estrategia de negociación basada en noticias de última hora —en la que se colocan órdenes stop de compra/venta en niveles clave justo antes de que se produzcan acontecimientos de gran repercusión— puede estar motivado por varias consideraciones estratégicas y tácticas:

  • Movimiento explosivo de los precios: Las noticias de gran impacto suelen provocar importantes oscilaciones en los precios. Colocar stops cerca de niveles clave puede ayudar a los operadores a entrar tan pronto como se produzca una ruptura, capturando grandes movimientos de precios.
  • Riesgo-recompensa mejorado: Los movimientos rápidos y volátiles pueden presentar configuraciones de riesgo-recompensa favorables si los stops del operador se activan en la dirección de la ruptura.
  • Previsibilidad de los comunicados de prensa: Dado que el momento en que se publicarán las noticias de gran impacto se conoce con antelación, los operadores pueden planificar sus entradas y salidas con mayor precisión, lo que reduce la incertidumbre en torno al momento adecuado para operar en el mercado.
  • Anticipación del aumento de liquidez: Las noticias suelen atraer una mayor participación en el mercado, lo que puede dar lugar a rupturas más fiables cuando se superan niveles clave.
  • Ejecución planificada previamente: Establecer stops en niveles técnicos clave antes de la publicación de las noticias ayuda a eliminar la toma de decisiones emocionales en el momento de la conmoción del mercado, lo que conduce a una ejecución más disciplinada.
  • Posibilidad de automatización: Realizar órdenes por adelantado permite la ejecución automatizada tan pronto como se publican las noticias, lo que garantiza una respuesta rápida a los movimientos del mercado sin necesidad de intervención manual.

Al combinar estas motivaciones, nuestro objetivo es desarrollar un enfoque sistemático que aproveche la previsibilidad y la volatilidad de los acontecimientos noticiosos de gran repercusión, al tiempo que se mantiene una gestión disciplinada del riesgo y unas reglas de ejecución claras.

El asesor experto comienza incluyendo los archivos auxiliares necesarios y creando objetos para las clases relacionadas. También declara las variables globales relevantes que se utilizarán más adelante.

#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;

La función inicializadora OnInit() garantiza lo siguiente:

  • Si saveFile es verdadero, las entradas del calendario se guardan en un archivo llamado «CalendarHistory.bin».
  • A continuación, los eventos del calendario se cargan desde este archivo. Pero no se puede guardar y cargar al mismo tiempo porque el método de guardado cierra el archivo al final.
  • La cadena de entrada de la variable Currencies se divide en una matriz de monedas individuales y la matriz se ordena. Por lo tanto, si desea eventos relacionados con las divisas USD y EUR, simplemente introduzca «USD»; «EUR».
  • El número mágico se asigna al objeto CTrade para identificar las operaciones iniciadas por este EA.

//+------------------------------------------------------------------+
//| 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);
}

Estas son las funciones necesarias para realizar las tareas de ejecución.

  • OnTradeTransaction: Supervisa las transacciones comerciales entrantes y actualiza la posición de compra o venta con el ticket de la orden cuando se añade una orden de compra o venta con el número mágico especificado.
  • executeBuy: Coloca una orden de compra stop al precio indicado con un stop loss calculado y registra la orden resultante en buypos.
  • executeSell: Coloca una orden de venta stop al precio indicado con un stop loss calculado y registra la orden resultante en sellpos.
  • IsCloseTime: Comprueba la hora actual del servidor para determinar si ha pasado la hora de cierre predefinida.
//+------------------------------------------------------------------+
//| 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);
}

Por último, solo tenemos que implementar la lógica de ejecución en la función 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;
      }     

   }
}

Esto garantiza que comprobemos el estado de las barras recién cerradas. Si la barra actual es igual a la última barra guardada, eso implica que aún no es una nueva barra, por lo que volveríamos sin ejecutar el resto de la lógica de negociación.

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

La lógica del bucle while garantiza una comprobación retrospectiva eficiente al comenzar desde el principio de los datos. Si la hora actual es posterior a un evento determinado, incrementamos el índice de la variable global, evitando así tener que volver a empezar el bucle desde el principio. Esto ayuda a reducir el tiempo de cálculo y el uso de memoria durante nuestras pruebas retrospectivas, lo que acelera considerablemente el proceso y ahorra mucho tiempo, especialmente en períodos de prueba prolongados.

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++;
      }  

Esta parte verifica si la hora actual ha pasado la hora de cierre. Si es así, recorre la cartera, verifica si hay posiciones abiertas con el número mágico del EA y las cierra. Una vez cerrado, el ticket de posición se pone a cero.

    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;
      }     

Ahora compile el programa y diríjase a la terminal MetaTrader 5. Abra cualquier gráfico y arrastre este EA al gráfico de la siguiente manera:

Dragging EA

Asegúrate de que la configuración saveFile esté establecida en «true».

Establecer como verdadero

Los datos de noticias proporcionados por su bróker son los mismos en todos los gráficos, por lo que los símbolos no importan. El EA se inicializará inmediatamente después de adjuntarlo a su gráfico, lo que significa que el archivo se guardará en ese momento. Después de unos segundos, puedes eliminar el EA. El archivo se guardará en la ruta de archivos comunes de su ordenador, y puede navegar hasta allí para comprobar que el archivo binario se ha guardado.

Ahora puedes probar la estrategia en el probador de estrategias.

Parámetros

Notas importantes sobre los parámetros de su backtest:

  • Establezca la variable saveFile en «false» para evitar que el archivo de datos binarios se cierre durante la inicialización.

  • Seleccione una desviación razonable y un stop loss. Una desviación demasiado amplia no captará la volatilidad del evento noticioso, mientras que una desviación o un stop loss demasiado estrechos serán propensos a deslizamientos durante los picos de noticias.

  • Elija una hora de cierre razonable. Recomiendo seleccionar una hora cercana al cierre del mercado o al final de la jornada bursátil, para poder captar todo el movimiento impulsado por las noticias. La hora de entrada correspondiente dependerá de la hora del servidor de su corredor.

Aquí está mi backtest sobre SPIUSDc desde el 1 de enero de 2019 hasta el 1 de diciembre de 2024 en un intervalo de tiempo de 5 minutos.

Configuración

Curva de capital

Resultado

Resultados clave:

  • Factor de beneficio: 1.26
  • Índice de Sharpe: 2.66
  • Número de operaciones: 1604

Por último, sugiero que los lectores utilicen garrapatas reales si están disponibles. Seleccione pruebas de estrés en alta latencia para tener en cuenta el deslizamiento y los altos diferenciales durante eventos noticiosos de alto impacto. Por último, pero no por ello menos importante, realice operaciones de demostración en condiciones reales para validar que los resultados de su prueba retrospectiva sean confiables. Algunos corredores tienen un deslizamiento significativo, mientras que otros pueden tener un deslizamiento mínimo. Al operar con una estrategia que se basa en una alta volatilidad, siempre tome medidas adicionales para garantizar que la estrategia sea rentable en operaciones en vivo.


Implementación de trading en vivo

Para operar en vivo, utilizaremos un código de asesor experto independiente. Ahora podemos utilizar las operaciones de calendario de MQL5, implementando la misma lógica que antes. La única diferencia es que creamos una función para actualizar los próximos eventos de noticias y los almacenamos en la matriz del objeto de calendario que creamos, actualizándolo cada hora.

La lógica de este código actualiza un historial de calendario obteniendo nuevos datos de eventos de una API de calendario si ha pasado más de una hora desde la última actualización, luego procesa y almacena los detalles del evento, incluidos los datos de país, valor y pronóstico, en un nuevo objeto CCalendarEntry para cada evento.

//+------------------------------------------------------------------+
//| 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;
   }
}

El resto del código es casi exactamente el mismo que el del EA de backtest. Puedes simplemente integrar el código en el EA de ejecución de esta manera y listo.

#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);
}

Para el desarrollo de una estrategia futura, puede aprovechar las bases descritas en este artículo y explorar ideas centradas en el trading basado en noticias. Por ejemplo:

Operaciones con rupturas en niveles clave
En lugar de depender de desviaciones fijas, este enfoque se centra en los movimientos de precios provocados por noticias importantes que rompen niveles clave de soporte o resistencia. Por ejemplo, cuando importantes informes económicos o anuncios corporativos provocan una ruptura, puedes realizar operaciones en la dirección del movimiento. Para implementar esto, monitoree el calendario de noticias e identifique los niveles de precios críticos con antelación.

Desvanecimiento de las noticias
Esta estrategia implica operar contra la reacción inicial del mercado a un evento noticioso, asumiendo que el primer movimiento es una reacción exagerada. Después de un aumento brusco de precios luego de las noticias, esperas a que el mercado se corrija y luego ingresas a una operación en la dirección opuesta.

Filtrar eventos de noticias
Si su estrategia funciona mejor en mercados de baja volatilidad, puede evitar operar durante eventos noticiosos de alto impacto. Al consultar el calendario de noticias con antelación, puede programar su sistema de negociación para cerrar posiciones abiertas o pausar nuevas operaciones hasta después del evento, lo que garantiza condiciones de mercado más estables.

Scalping de noticias
Esta idea se centra en capturar pequeñas ganancias de los movimientos de precios a corto plazo causados por eventos noticiosos. Implica entradas y salidas rápidas, stop-loss ajustados y una rápida obtención de beneficios. Esta estrategia es especialmente eficaz durante acontecimientos volátiles que provocan rápidas fluctuaciones de precios.

Operaciones con el calendario económico
Este enfoque gira en torno a eventos programados en el calendario económico, como decisiones sobre tasas de interés, anuncios del PIB o informes de empleo. Al analizar cómo han reaccionado históricamente los mercados a noticias similares y tener en cuenta las expectativas actuales, puede anticipar posibles movimientos de precios y prepararse en consecuencia.

Cada una de estas estrategias se basa en la recopilación y el análisis de datos noticiosos relevantes con antelación, lo que permite aprovechar al máximo la volatilidad del mercado causada por eventos importantes.



Conclusión

En este artículo, creamos un archivo de inclusión auxiliar para interpretar, formatear y almacenar datos de eventos de noticias del calendario. A continuación, desarrollamos un asesor experto de backtest para obtener los datos de eventos noticiosos proporcionados por los corredores, implementar la lógica de la estrategia y probar los resultados de nuestra estrategia comercial utilizando estos datos. La estrategia mostró una rentabilidad prometedora, con más de 1600 muestras a lo largo de 5 años de datos de ticks. Finalmente, compartimos el código del asesor experto de ejecución en vivo para trading en vivo y describimos las aspiraciones futuras, alentando un mayor desarrollo de estrategias basadas en el marco presentado en este artículo.


Tabla de archivos

Nombre del archivo Uso de archivos
CalendarHistory.mqh El archivo auxiliar de inclusión para manejar datos de eventos de noticias del calendario.
News Breakout Backtest.mq5 El asesor experto para almacenar datos de eventos noticiosos y realizar pruebas retrospectivas.
News Breakout.mq5 El asesor experto para trading en vivo.

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/16752

Zhuo Kai Chen
Zhuo Kai Chen | 16 may 2025 en 10:28
hrawoodward #:

Hola, gracias. Estoy un poco confundido acerca de la introducción de varias monedas. Lo he intentado:

"USD"; "GBP"

USD"; "GBP".

"USD"; "GBP";

Sólo el último no produce error, pero no estoy seguro de que funcione correctamente. Tal vez sólo recoge el USD. ¿Me puede aconsejar?

Hola, si te fijas en el código de la función de inicialización, dividirá los dos puntos y almacenará las diferentes monedas en el atributo del objeto curr. La primera debería funcionar, aunque no es necesario que añadas las comillas. El proceso de almacenamiento almacenará todos los eventos en el archivo binario independientemente de sus atributos. Sólo en la lógica comercial filtraremos por los atributos. Esto es lo que he ejecutado ahora:

ajustes

resultado

Stanislav Korotky
Stanislav Korotky | 16 may 2025 en 16:22
Parece que esta implementación no tiene en cuenta los cambios de zona horaria (DST) en el servidor del corredor y, por lo tanto, produce resultados inexactos durante las pruebas retrospectivas y las optimizaciones.
Zhuo Kai Chen
Zhuo Kai Chen | 17 may 2025 en 03:17
Stanislav Korotky #:
Parece que esta implementación no tiene en cuenta los cambios de zona horaria (DST) en el servidor del broker y por lo tanto produce resultados inexactos durante el backtesting y las optimizaciones.

Gracias por recordármelo. Olvidé tenerlo en cuenta en el artículo porque utilicé un broker que no tiene DST para la demostración.

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

De esta fuente sabemos que los datos del calendario se proporcionan desde el lado de MQL5, y se ajustan automáticamente a la zona horaria actual de Timetradeserver() del broker, lo que significa que para brokers con DST necesitaría ajustar mi código y tenerlo en cuenta.

Stanislav Korotky
Stanislav Korotky | 17 may 2025 en 13:05
Zhuo Kai Chen #:

De esta fuente sabemos que los datos del calendario se proporciona desde el lado MQL5, y se ajusta automáticamente a la actual zona horaria Timetradeserver() del corredor, lo que significa que para los corredores con DST tendría que ajustar mi código y tenerlo en cuenta.

Dado que la implementación publicada en el libro es un poco anticuada, la historia real (actualizada) se puede encontrar en el blog y en la base de código (indicador) y en la base de código (script).

TL_TL_TL
TL_TL_TL | 27 sept 2025 en 19:41
Hermano, este archivo CalendarHistory.mqh no compila, 4 errores, 94, 106, 114, 122 líneas.
Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt (Final) Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt (Final)
Continuamos nuestra exploración del framework de aprendizaje multitarea basado en ResNeXt, que destaca por su modularidad, su alta eficiencia desde el punto de vista computacional y su capacidad de identificar patrones consistentes en los datos. El uso de un único codificador y de "cabezas" especializadas reduce el riesgo de sobreentrenamiento del modelo y mejora la calidad de las predicciones.
Implementación del algoritmo criptográfico SHA-256 desde cero en MQL5 Implementación del algoritmo criptográfico SHA-256 desde cero en MQL5
La creación de integraciones de intercambio de criptomonedas sin DLL ha sido durante mucho tiempo un reto, pero esta solución proporciona un marco completo para la conectividad directa con el mercado.
Analizamos el código binario de los precios en bolsa (Parte II): Convirtiendo a BIP39 y escribiendo un modelo GPT Analizamos el código binario de los precios en bolsa (Parte II): Convirtiendo a BIP39 y escribiendo un modelo GPT
Seguimos intentando descifrar los movimientos de los precios.... ¿Qué tal un análisis lingüístico del "diccionario de mercado" que obtendríamos convirtiendo el código binario de precios en BIP39? En el presente artículo, nos adentramos en un enfoque innovador del análisis de los datos bursátiles y exploramos cómo pueden aplicarse las modernas técnicas de procesamiento del lenguaje natural al lenguaje del mercado.
Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt
El marco de aprendizaje multitarea basado en ResNeXt optimiza el análisis de datos financieros considerando su alta dimensionalidad, la no linealidad y las dependencias temporales. El uso de la convolución grupal y cabezas especializadas permite al modelo extraer eficazmente características clave de los datos de origen.