English Русский 中文 Deutsch 日本語
preview
Operando con el Calendario Económico MQL5 (Parte 7): Preparación para la prueba de estrategias con análisis de eventos noticiosos basado en recursos

Operando con el Calendario Económico MQL5 (Parte 7): Preparación para la prueba de estrategias con análisis de eventos noticiosos basado en recursos

MetaTrader 5Probador |
23 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En este artículo, continuamos con la serie Calendario Económico MQL5 preparando el sistema de trading para la prueba de estrategias fuera del entorno en tiempo real, utilizando datos de eventos económicos integrados para lograr un backtesting fiable. Partiendo de la automatización de las entradas de operaciones de la Parte 6, con análisis de noticias y temporizadores de cuenta atrás, ahora nos centramos en cargar eventos de noticias desde un archivo de recursos y aplicar filtros definidos por el usuario para simular condiciones reales en el Probador de estrategias. Estructuramos el artículo con los siguientes temas:

  1. Importancia de la integración de datos estáticos
  2. Implementación en MQL5
  3. Pruebas
  4. Conclusión

¡Manos a la obra!


Importancia de la integración de datos estáticos

La integración de datos estáticos es fundamental para quienes desean desarrollar y probar estrategias sólidas, especialmente en entornos como MQL5, donde los datos históricos de acontecimientos económicos no se conservan durante largos periodos de tiempo. A diferencia del trading en vivo, en el que la plataforma puede obtener noticias en tiempo real, el Probador de estrategias no tiene acceso a este tipo de actualizaciones dinámicas. No almacena archivos exhaustivos de acontecimientos pasados, lo que nos deja sin una solución nativa para realizar pruebas retrospectivas de estrategias basadas en noticias. Al descargar estos datos de fuentes externas y organizarlos nosotros mismos —ya sea en forma de archivos, bases de datos o recursos integrados—, obtenemos el control sobre un conjunto de datos coherente que puede reutilizarse en múltiples pruebas, lo que garantiza que nuestras estrategias se enfrenten a las mismas condiciones en cada ocasión.

Más allá de superar las limitaciones de la plataforma, la integración de datos estáticos ofrece una flexibilidad que los flujos de datos en tiempo real no pueden proporcionar. El Calendario económico, como ya hemos visto en versiones anteriores, suele incluir datos fundamentales como las fechas y horas de los eventos, las divisas y los niveles de impacto, pero estos datos no siempre se presentan en un formato adecuado para el análisis algorítmico en horizontes temporales largos. Al estructurar esta información manualmente, podemos adaptarla a nuestras necesidades —por ejemplo, filtrando por divisas específicas o eventos de alto impacto—, lo que nos permite obtener una comprensión más profunda de cómo las noticias influyen en el comportamiento del mercado sin depender de su disponibilidad en tiempo real.

Además, este enfoque mejorará la eficiencia y la independencia. Recopilar y almacenar los datos estáticos por adelantado nos permite no depender de la conexión a Internet ni de servicios de terceros durante las pruebas, lo que reduce las variables que podrían sesgar los resultados. También nos permite simular escenarios poco comunes o específicos, como anuncios económicos importantes, mediante la selección de conjuntos de datos que abarcan años o se centran en momentos clave, algo que los sistemas en tiempo real o el almacenamiento limitado de la plataforma no pueden replicar fácilmente. En definitiva, la integración de datos estáticos cierra la brecha entre la información obtenida en tiempo real durante las operaciones y la precisión de las pruebas retrospectivas, sentando una base sólida para el desarrollo de estrategias.

El almacenamiento de datos será un factor clave, y MQL5 ofrece una amplia variedad de opciones, desde formatos de texto (txt), valores separados por comas (CSV), ANSI (American National Standards Institute), binario (bin) y Unicode, hasta las estructuras de bases de datos que se indican a continuación.

ALGUNOS FORMATOS DE DATOS DE ARCHIVOS MQL5

Utilizaremos el formato CSV, que no es el más sencillo, pero sí el más conveniente. De esta forma, tendremos los datos a mano y no tendremos que esperar horas para realizar pruebas retrospectivas de nuestra estrategia, lo que nos ahorrará mucho tiempo y energía. Vamos.


Implementación en MQL5

Para empezar, necesitaremos estructurar la recopilación y organización de datos de manera que refleje nuestra estructura anterior. Por lo tanto, necesitaremos algunas entradas que el usuario pueda personalizar, tal como lo hicimos anteriormente, como se muestra a continuación. 

//+------------------------------------------------------------------+
//|                                    MQL5 NEWS CALENDAR PART 7.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://youtube.com/@ForexAlgo-Trader?"
#property version   "1.00"
#property strict

//---- Input parameter for start date of event filtering
input datetime StartDate = D'2025.03.01'; // Download Start Date
//---- Input parameter for end date of event filtering
input datetime EndDate = D'2025.03.21'; // Download End Date
//---- Input parameter to enable/disable time filtering
input bool ApplyTimeFilter = true;
//---- Input parameter for hours before event to consider
input int HoursBefore = 4;
//---- Input parameter for minutes before event to consider
input int MinutesBefore = 10;
//---- Input parameter for hours after event to consider
input int HoursAfter = 1;
//---- Input parameter for minutes after event to consider
input int MinutesAfter = 5;
//---- Input parameter to enable/disable currency filtering
input bool ApplyCurrencyFilter = true;
//---- Input parameter defining currencies to filter (comma-separated)
input string CurrencyFilter = "USD,EUR,GBP,JPY,AUD,NZD,CAD,CHF"; // All 8 major currencies
//---- Input parameter to enable/disable impact filtering
input bool ApplyImpactFilter = true;

//---- Enumeration for event importance filtering options
enum ENUM_IMPORTANCE {
   IMP_NONE = 0,                  // None
   IMP_LOW,                       // Low
   IMP_MEDIUM,                    // Medium
   IMP_HIGH,                      // High
   IMP_NONE_LOW,                  // None,Low
   IMP_NONE_MEDIUM,               // None,Medium
   IMP_NONE_HIGH,                 // None,High
   IMP_LOW_MEDIUM,                // Low,Medium
   IMP_LOW_HIGH,                  // Low,High
   IMP_MEDIUM_HIGH,               // Medium,High
   IMP_NONE_LOW_MEDIUM,           // None,Low,Medium
   IMP_NONE_LOW_HIGH,             // None,Low,High
   IMP_NONE_MEDIUM_HIGH,          // None,Medium,High
   IMP_LOW_MEDIUM_HIGH,           // Low,Medium,High
   IMP_ALL                        // None,Low,Medium,High (default)
};
//---- Input parameter for selecting importance filter
input ENUM_IMPORTANCE ImportanceFilter = IMP_ALL; // Impact Levels (Default to all)

Aquí, configuramos los parámetros de entrada fundamentales y una enumeración para personalizar la forma en que nuestro sistema de trading gestiona los eventos económicos para la prueba de estrategias. Definimos "StartDate" y "EndDate" como variables datetime, con valores del 1 de marzo de 2025 y el 21 de marzo de 2025, respectivamente, para especificar el intervalo de tiempo para la descarga y el análisis de los datos de los eventos. Para controlar el filtrado temporal en torno a estos eventos, incluimos "ApplyTimeFilter" como un booleano cuyo valor predeterminado es "true", junto con "HoursBefore" (4 horas), "MinutesBefore" (10 minutos), "HoursAfter" (1 hora) y "MinutesAfter" (5 minutos), que determinan la ventana de tiempo para considerar los eventos respecto a una barra concreta.

Para realizar análisis específicos por divisa, introducimos "ApplyCurrencyFilter" (activado por defecto) y "CurrencyFilter", una cadena de caracteres que enumera las ocho divisas principales —"USD, EUR, GBP, JPY, AUD, NZD, CAD, CHF"— para centrarnos en los mercados relevantes. También permitimos el filtrado basado en el impacto con "ApplyImpactFilter" establecido en "true", gracias a la enumeración "ENUM_IMPORTANCE", que ofrece opciones flexibles como "IMP_NONE", "IMP_LOW", "IMP_MEDIUM", "IMP_HIGH" y combinaciones hasta "IMP_ALL", con "ImportanceFilter" establecido por defecto en "IMP_ALL" para incluir todos los niveles de impacto. El resultado es el siguiente.

VERSIÓN DE ENTRADAS

Una vez introducidos los datos, lo siguiente que debemos hacer es declarar una estructura con ocho campos de entrada que imite la estructura habitual y predeterminada del Calendario Económico MQL5, tal y como se muestra a continuación.

FORMATO PREDETERMINADO DEL CALENDARIO MQL5

Conseguimos el formato siguiendo la siguiente lógica.

//---- Structure to hold economic event data
struct EconomicEvent {
   string eventDate;      //---- Date of the event
   string eventTime;      //---- Time of the event
   string currency;       //---- Currency affected by the event
   string event;          //---- Event description
   string importance;     //---- Importance level of the event
   double actual;         //---- Actual value of the event
   double forecast;       //---- Forecasted value of the event
   double previous;       //---- Previous value of the event
};

//---- Array to store all economic events
EconomicEvent allEvents[];
//---- Array for currency filter values
string curr_filter[];
//---- Array for importance filter values
string imp_filter[];

En primer lugar, definimos la estructura "EconomicEvent" (struct) para encapsular los detalles clave del evento, incluyendo "eventDate" y "eventTime" como cadenas de caracteres para indicar la fecha y la hora del evento, "currency" para identificar el mercado afectado, "event" para la descripción y "importance" para indicar su nivel de impacto, junto con "actual", "forecast" y "previous" como valores de tipo double para almacenar los datos numéricos del evento.

Para almacenar y gestionar estos eventos, creamos tres matrices: "allEvents", una array de estructuras "EconomicEvent" destinada a contener todos los eventos cargados; "curr_filter", una array de cadenas para almacenar las divisas especificadas en el campo de entrada "CurrencyFilter"; y "imp_filter", una array de cadenas para gestionar los niveles de impacto seleccionados a través de "ImportanceFilter". Esto reproduce la estructura predeterminada, con la única diferencia de que desplazamos la sección "Period" para que incluya las fechas de los eventos al principio de la estructura. A continuación, debemos obtener los filtros a partir de los datos introducidos por el usuario, interpretarlos de forma que el ordenador los entienda e inicializarlos. Para mantener el código modularizado, utilizaremos funciones.

//---- Function to initialize currency and impact filters
void InitializeFilters() {
   //---- Currency Filter Section
   //---- Check if currency filter is enabled and has content
   if (ApplyCurrencyFilter && StringLen(CurrencyFilter) > 0) {
      //---- Split the currency filter string into array
      int count = StringSplit(CurrencyFilter, ',', curr_filter);
      //---- Loop through each currency filter entry
      for (int i = 0; i < ArraySize(curr_filter); i++) {
         //---- Temporary variable for trimming
         string temp = curr_filter[i];
         //---- Remove leading whitespace
         StringTrimLeft(temp);
         //---- Remove trailing whitespace
         StringTrimRight(temp);
         //---- Assign trimmed value back to array
         curr_filter[i] = temp;
         //---- Print currency filter for debugging
         Print("Currency filter [", i, "]: '", curr_filter[i], "'");
      }
   } else if (ApplyCurrencyFilter) {
      //---- Warn if currency filter is enabled but empty
      Print("Warning: CurrencyFilter is empty, no currency filtering applied");
      //---- Resize array to zero if no filter applied
      ArrayResize(curr_filter, 0);
   }
}

Aquí configuramos la parte de filtrado por divisa de la función "InitializeFilters" de nuestro sistema para preparar un análisis eficaz de los eventos durante las pruebas de estrategias. Empezamos comprobando si la variable "ApplyCurrencyFilter" es verdadera y si la cadena "CurrencyFilter" tiene contenido mediante la función StringLen; si es así, dividimos "CurrencyFilter", separado por comas (como "USD, EUR, GBP"), en la array "curr_filter" utilizando la función StringSplit, capturando el número de elementos en "count".

A continuación, recorremos cada elemento de "curr_filter" con un bucle "for", asignándolo a una cadena temporal "temp" string, limpiándolo al eliminar los espacios en blanco iniciales y finales con las funciones StringTrimLeft y StringTrimRight, y luego actualizando "curr_filter" con el valor recortado y mostrándolo mediante la función Print con fines de depuración (por ejemplo, "Currency filter [0]: 'USD'"). Sin embargo, si "ApplyCurrencyFilter" está activado pero "CurrencyFilter" está vacío, utilizamos la función "Print" para emitir una advertencia —"Warning: CurrencyFilter is empty, no currency filtering applied"— y redimensionamos el array a cero mediante la función ArrayResize para desactivar el filtrado. Esta inicialización cuidadosa garantizará que el filtro de moneda se derive de forma fiable de las entradas del usuario, lo que permitirá una gestión precisa de los eventos en el Probador de estrategias. Para el filtro de impacto, aplicamos una lógica de selección similar.

//---- Impact Filter Section (using enum)
//---- Check if impact filter is enabled
if (ApplyImpactFilter) {
   //---- Switch based on selected importance filter
   switch (ImportanceFilter) {
      case IMP_NONE:
         //---- Resize array for single importance level
         ArrayResize(imp_filter, 1);
         //---- Set importance to "None"
         imp_filter[0] = "None";
         break;
      case IMP_LOW:
         //---- Resize array for single importance level
         ArrayResize(imp_filter, 1);
         //---- Set importance to "Low"
         imp_filter[0] = "Low";
         break;
      case IMP_MEDIUM:
         //---- Resize array for single importance level
         ArrayResize(imp_filter, 1);
         //---- Set importance to "Medium"
         imp_filter[0] = "Medium";
         break;
      case IMP_HIGH:
         //---- Resize array for single importance level
         ArrayResize(imp_filter, 1);
         //---- Set importance to "High"
         imp_filter[0] = "High";
         break;
      case IMP_NONE_LOW:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Low";
         break;
      case IMP_NONE_MEDIUM:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Medium";
         break;
      case IMP_NONE_HIGH:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "High";
         break;
      case IMP_LOW_MEDIUM:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "Low";
         //---- Set second importance level
         imp_filter[1] = "Medium";
         break;
      case IMP_LOW_HIGH:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "Low";
         //---- Set second importance level
         imp_filter[1] = "High";
         break;
      case IMP_MEDIUM_HIGH:
         //---- Resize array for two importance levels
         ArrayResize(imp_filter, 2);
         //---- Set first importance level
         imp_filter[0] = "Medium";
         //---- Set second importance level
         imp_filter[1] = "High";
         break;
      case IMP_NONE_LOW_MEDIUM:
         //---- Resize array for three importance levels
         ArrayResize(imp_filter, 3);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Low";
         //---- Set third importance level
         imp_filter[2] = "Medium";
         break;
      case IMP_NONE_LOW_HIGH:
         //---- Resize array for three importance levels
         ArrayResize(imp_filter, 3);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Low";
         //---- Set third importance level
         imp_filter[2] = "High";
         break;
      case IMP_NONE_MEDIUM_HIGH:
         //---- Resize array for three importance levels
         ArrayResize(imp_filter, 3);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Medium";
         //---- Set third importance level
         imp_filter[2] = "High";
         break;
      case IMP_LOW_MEDIUM_HIGH:
         //---- Resize array for three importance levels
         ArrayResize(imp_filter, 3);
         //---- Set first importance level
         imp_filter[0] = "Low";
         //---- Set second importance level
         imp_filter[1] = "Medium";
         //---- Set third importance level
         imp_filter[2] = "High";
         break;
      case IMP_ALL:
         //---- Resize array for all importance levels
         ArrayResize(imp_filter, 4);
         //---- Set first importance level
         imp_filter[0] = "None";
         //---- Set second importance level
         imp_filter[1] = "Low";
         //---- Set third importance level
         imp_filter[2] = "Medium";
         //---- Set fourth importance level
         imp_filter[3] = "High";
         break;
   }
   //---- Loop through impact filter array to print values
   for (int i = 0; i < ArraySize(imp_filter); i++) {
      //---- Print each impact filter value
      Print("Impact filter [", i, "]: '", imp_filter[i], "'");
   }
} else {
   //---- Notify if impact filter is disabled
   Print("Impact filter disabled");
   //---- Resize impact filter array to zero
   ArrayResize(imp_filter, 0);
}

Para el proceso de filtrado de impacto, comenzamos comprobando si la variable "ApplyImpactFilter" es verdadera; de ser así, utilizamos una instrucción switch basada en la enumeración "ImportanceFilter" para determinar qué niveles de impacto incluir en la array "imp_filter". Para opciones de un solo nivel como "IMP_NONE", "IMP_LOW", "IMP_MEDIUM" o "IMP_HIGH", redimensionamos "imp_filter" a 1 utilizando la función ArrayResize y le asignamos la cadena correspondiente (por ejemplo, "imp_filter[0] = “None”"); para opciones de dos niveles como "IMP_NONE_LOW" o "IMP_MEDIUM_HIGH", lo redimensionamos a 2 y establecemos dos valores (por ejemplo, "imp_filter[0] = “None”, imp_filter[1] = “Low”"); para opciones de tres niveles como "IMP_LOW_MEDIUM_HIGH", cambiamos el tamaño a 3; y para "IMP_ALL", cambiamos el tamaño a 4, cubriendo "None", "Low", "Medium" y "High".

Una vez definida la array, recorremos "imp_filter" utilizando la función ArraySize para determinar su tamaño, y mostramos cada valor con la función Print para facilitar la depuración (por ejemplo, "Impact filter [0]: 'None'"). Si "ApplyImpactFilter" es falso, avisamos al usuario mediante la función "Print" —"Impact filter disabled"— y redimensionamos "imp_filter" a cero.

Con esto, ahora tenemos que llamar a la función en el controlador de eventos OnInit.

int OnInit() {
   //---- Initialize filters
   InitializeFilters();

   //---- Return successful initialization
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
   //---- Print termination reason
   Print("EA terminated, reason: ", reason);
}

Llamamos a la función en el controlador de eventos OnInit y también mostramos el motivo de la finalización del programa en el controlador de eventos OnDeinit. Este es el resultado.

INICIALIZACIÓN DE LOS FILTROS

En la imagen podemos ver que hemos inicializado y parseado correctamente las entradas del filtro y las hemos almacenado. Ahora solo tenemos que obtener los datos en ejecución real y almacenarlos. En este caso, la lógica es que primero debemos ejecutar el programa una vez en modo real, para que pueda descargar los datos de la base de datos del Calendario Económico MQL5, y luego cargar y utilizar esos datos en modo de prueba. Aquí está la lógica de inicialización.

//---- Check if not running in tester mode
if (!MQLInfoInteger(MQL_TESTER)) {
   //---- Validate date range
   if (StartDate >= EndDate) {
      //---- Print error for invalid date range
      Print("Error: StartDate (", TimeToString(StartDate), ") must be earlier than EndDate (", TimeToString(EndDate), ")");
      //---- Return initialization failure
      return(INIT_PARAMETERS_INCORRECT);
   }

   //---- Array to hold calendar values
   MqlCalendarValue values[];
   //---- Fetch calendar data for date range
   if (!CalendarValueHistory(values, StartDate, EndDate)) {
      //---- Print error if calendar data fetch fails
      Print("Error fetching calendar data: ", GetLastError());
      //---- Return initialization failure
      return(INIT_FAILED);
   }

   //---- Array to hold economic events
   EconomicEvent events[];
   //---- Counter for events
   int eventCount = 0;

   //---- Loop through calendar values
   for (int i = 0; i < ArraySize(values); i++) {
      //---- Structure for event details
      MqlCalendarEvent eventDetails;
      //---- Fetch event details by ID
      if (!CalendarEventById(values[i].event_id, eventDetails)) continue;

      //---- Structure for country details
      MqlCalendarCountry countryDetails;
      //---- Fetch country details by ID
      if (!CalendarCountryById(eventDetails.country_id, countryDetails)) continue;

      //---- Structure for value details
      MqlCalendarValue value;
      //---- Fetch value details by ID
      if (!CalendarValueById(values[i].id, value)) continue;

      //---- Resize events array for new event
      ArrayResize(events, eventCount + 1);
      //---- Convert event time to string
      string dateTimeStr = TimeToString(values[i].time, TIME_DATE | TIME_MINUTES);
      //---- Extract date from datetime string
      events[eventCount].eventDate = StringSubstr(dateTimeStr, 0, 10);
      //---- Extract time from datetime string
      events[eventCount].eventTime = StringSubstr(dateTimeStr, 11, 5);
      //---- Assign currency from country details
      events[eventCount].currency = countryDetails.currency;
      //---- Assign event name
      events[eventCount].event = eventDetails.name;
      //---- Map importance level from enum to string
      events[eventCount].importance = (eventDetails.importance == 0) ? "None" :    // CALENDAR_IMPORTANCE_NONE
                                      (eventDetails.importance == 1) ? "Low" :     // CALENDAR_IMPORTANCE_LOW
                                      (eventDetails.importance == 2) ? "Medium" :  // CALENDAR_IMPORTANCE_MODERATE
                                      "High";                                      // CALENDAR_IMPORTANCE_HIGH
      //---- Assign actual value
      events[eventCount].actual = value.GetActualValue();
      //---- Assign forecast value
      events[eventCount].forecast = value.GetForecastValue();
      //---- Assign previous value
      events[eventCount].previous = value.GetPreviousValue();
      //---- Increment event count
      eventCount++;
   }

}

Aquí gestionamos la obtención de datos en tiempo real dentro de la función OnInit de nuestro programa, asegurándonos de que se recopilen datos sobre eventos económicos para su uso posterior en las pruebas de estrategias. Empezamos comprobando si el sistema no se encuentra en modo de prueba mediante la función MQLInfoInteger con MQL_TESTER; si es cierto, verificamos que "StartDate" sea anterior a "EndDate", mostrando un mensaje de error y devolviendo INIT_PARAMETERS_INCORRECT si no es válido. A continuación, declaramos una array MqlCalendarValue denominada "values" y recuperamos los datos del calendario comprendidos entre "StartDate" y "EndDate" mediante la función CalendarValueHistory, mostrando cualquier error con GetLastError y devolviendo "INIT_FAILED" si se produce un error.

A continuación, inicializamos una array "EconomicEvent" denominada "events" y una variable entera "eventCount" para llevar un recuento de los eventos, recorriendo "values" con la función ArraySize. En cada iteración, recuperamos los detalles del evento en una estructura MqlCalendarEvent denominada "eventDetails" mediante la función CalendarEventById, los detalles del país en una estructura MqlCalendarCountry denominada "countryDetails" mediante CalendarCountryById, y los detalles del valor en una estructura "MqlCalendarValue" denominada "value" mediante "CalendarValueById", omitiendo el proceso si falla alguna de las recuperaciones. Redimensionamos los "eventos" con la función ArrayResize, convertimos la hora del evento en una cadena "dateTimeStr" utilizando la función TimeToString y extraemos "eventDate" y "eventTime" con la función StringSubstr, asignando "currency" a partir de "countryDetails", "event" de "eventDetails.name" y asignando "importance" de valores numéricos a cadenas ("None", "Low", "Medium", "High"). Por último, establecemos los valores "actual", "forecast" y "previous" mediante los métodos "value" e incrementamos "eventCount", generando así un conjunto completo de eventos para la gestión en tiempo real. Ahora necesitamos una función que se encargue de almacenar esta información en un archivo de datos.

//---- Function to write events to a CSV file
void WriteToCSV(string fileName, EconomicEvent &events[]) {
   //---- Open file for writing in CSV format
   int handle = FileOpen(fileName, FILE_WRITE | FILE_CSV, ',');
   //---- Check if file opening failed
   if (handle == INVALID_HANDLE) {
      //---- Print error message with last error code
      Print("Error creating file: ", GetLastError());
      //---- Exit function on failure
      return;
   }

   //---- Write CSV header row
   FileWrite(handle, "Date", "Time", "Currency", "Event", "Importance", "Actual", "Forecast", "Previous");
   //---- Loop through all events to write to file
   for (int i = 0; i < ArraySize(events); i++) {
      //---- Write event data to CSV file
      FileWrite(handle, events[i].eventDate, events[i].eventTime, events[i].currency, events[i].event,
                events[i].importance, DoubleToString(events[i].actual, 2), DoubleToString(events[i].forecast, 2),
                DoubleToString(events[i].previous, 2));
      //---- Print event details for debugging
      Print("Writing event ", i, ": ", events[i].eventDate, ", ", events[i].eventTime, ", ", events[i].currency, ", ",
            events[i].event, ", ", events[i].importance, ", ", DoubleToString(events[i].actual, 2), ", ",
            DoubleToString(events[i].forecast, 2), ", ", DoubleToString(events[i].previous, 2));
   }

   //---- Flush data to file
   FileFlush(handle);
   //---- Close the file handle
   FileClose(handle);
   //---- Print confirmation of data written
   Print("Data written to ", fileName, " with ", ArraySize(events), " events.");

   //---- Verify written file by reading it back
   int verifyHandle = FileOpen(fileName, FILE_READ | FILE_TXT);
   //---- Check if verification file opening succeeded
   if (verifyHandle != INVALID_HANDLE) {
      //---- Read entire file content
      string content = FileReadString(verifyHandle, (int)FileSize(verifyHandle));
      //---- Print file content for verification
      Print("File content after writing (size: ", FileSize(verifyHandle), " bytes):\n", content);
      //---- Close verification file handle
      FileClose(verifyHandle);
   }
}

Aquí creamos la función "WriteToCSV" para exportar de forma sistemática los datos de eventos económicos a un archivo CSV. Comenzamos abriendo el archivo especificado por "fileName" mediante la función FileOpen en modo "FILE_WRITE | FILE_CSV" con delimitador de comas, y almacenamos el resultado en "handle"; si esto falla y "handle" es igual a "INVALID_HANDLE", utilizamos la función "Print" para mostrar un mensaje de error que incluye el código GetLastError y salimos de la función con "return". Una vez abierto el archivo, escribimos una fila de encabezado con la función FileWrite, definiendo las columnas como "Date", "Time", "Currency", "Event", "Importance", "Actual", "Forecast" y "Previous" para organizar los datos.

A continuación, recorremos la array "events", determinando su tamaño con la función ArraySize, y para cada evento llamamos a "FileWrite" para registrar sus propiedades —"eventDate", "eventTime", "currency", "event", "importance" y los valores numéricos "actual", "forecast" y "previous", convertidos a cadenas con la función DoubleToString (formateados con 2 decimales)—, al tiempo que registramos estos detalles con la función "Print" con fines de depuración.

Una vez completado el bucle, nos aseguramos de que todos los datos se hayan escrito en el archivo llamando a la función FileFlush sobre "handle"; a continuación, cerramos el archivo mediante la función FileClose y confirmamos que la operación se ha realizado correctamente con un mensaje.

Para verificar el resultado, volvemos a abrir el archivo en modo lectura utilizando "FILE_READ | FILE_TXT", y almacenamos este identificador en "verifyHandle"; si tiene éxito, leemos todo el contenido en "content" con la función FileReadString basándonos en el tamaño en bytes de FileSize, lo imprimimos para su inspección (por ejemplo, "File content after writing (size: X bytes):\n"content"") y cerramos. Este minucioso proceso garantiza que los datos del evento se guarden con precisión y puedan verificarse, lo que lo convierte en un recurso fiable para el backtesting en el Probador de estrategias. Ahora podemos utilizar la función para el proceso de guardado de datos.

//---- Define file path for CSV
string fileName = "Database\\EconomicCalendar.csv";

//---- Check if file exists and print appropriate message
if (!FileExists(fileName)) Print("Creating new file: ", fileName);
else Print("Overwriting existing file: ", fileName);

//---- Write events to CSV file
WriteToCSV(fileName, events);
//---- Print instructions for tester mode
Print("Live mode: Data written. To use in tester, manually add ", fileName, " as a resource and recompile.");

Para completar el manejo de datos en modo en vivo, hemos establecido "fileName" en "Database\EconomicCalendar.csv" y hemos utilizado la función personalizada "FileExists" para comprobar su estado. Luego llamamos a la función "WriteToCSV" con "fileName" y "events" para guardar los datos, y mostramos con "Print": "Live mode: Data written. To use in tester, add "fileName" as a resource and recompile.", para uso del probador. El fragmento de código de la función personalizada para comprobar la existencia del archivo es el siguiente.

//---- Function to check if a file exists
bool FileExists(string fileName) {
   //---- Open file in read mode to check existence
   int handle = FileOpen(fileName, FILE_READ | FILE_CSV);
   //---- Check if file opened successfully
   if (handle != INVALID_HANDLE) {
      //---- Close the file handle
      FileClose(handle);
      //---- Return true if file exists
      return true;
   }
   //---- Return false if file doesn't exist
   return false;
}

En la función "FileExists", que comprueba la existencia de un archivo para las pruebas de estrategias, abrimos "fileName" con la función FileOpen en modo "FILE_READ | FILE_CSV", y si "handle" no es "INVALID_HANDLE", lo cerramos con FileClose y devolvemos "true"; en caso contrario, devolvemos "false". Esto confirma el estado del archivo para el tratamiento de datos. Al ejecutarlo en modo de producción, este es el resultado.

OBTENCIÓN DE DATOS EN MODO EN VIVO

En la imagen podemos ver que los datos están guardados y que podemos acceder a ellos.

ACCESO A DATOS

Para utilizar los datos en modo de prueba, necesitamos guardarlos en el archivo ejecutable. Para ello, lo añadimos como recurso.

//---- Define resource file for economic calendar data
#resource "\\Files\\Database\\EconomicCalendar.csv" as string EconomicCalendarData

Aquí integramos el recurso de datos estáticos en nuestro programa para respaldar las pruebas de estrategia. Mediante la directiva #resource, incorporamos el archivo ubicado en "\Files\Database\EconomicCalendar.csv" y lo asignamos a la variable de cadena "EconomicCalendarData". De esa forma, el archivo se encuentra en el directorio "executive", por lo que no hay que preocuparse aunque se borre. Ahora podemos tener una función para cargar el contenido del archivo.

//---- Function to load events from resource file
bool LoadEventsFromResource() {
   //---- Get data from resource
   string fileData = EconomicCalendarData;
   //---- Print raw resource content for debugging
   Print("Raw resource content (size: ", StringLen(fileData), " bytes):\n", fileData);

   //---- Array to hold lines from resource
   string lines[];
   //---- Split resource data into lines
   int lineCount = StringSplit(fileData, '\n', lines);
   //---- Check if resource has valid data
   if (lineCount <= 1) {
      //---- Print error if no data lines found
      Print("Error: No data lines found in resource! Raw data: ", fileData);
      //---- Return false on failure
      return false;
   }

   //---- Reset events array
   ArrayResize(allEvents, 0);
   //---- Index for event array
   int eventIndex = 0;

   //---- Loop through each line (skip header at i=0)
   for (int i = 1; i < lineCount; i++) {
      //---- Check for empty lines
      if (StringLen(lines[i]) == 0) {
         //---- Print message for skipped empty line
         Print("Skipping empty line ", i);
         //---- Skip to next iteration
         continue;
      }

      //---- Array to hold fields from each line
      string fields[];
      //---- Split line into fields
      int fieldCount = StringSplit(lines[i], ',', fields);
      //---- Print line details for debugging
      Print("Line ", i, ": ", lines[i], " (field count: ", fieldCount, ")");

      //---- Check if line has minimum required fields
      if (fieldCount < 8) {
         //---- Print error for malformed line
         Print("Malformed line ", i, ": ", lines[i], " (field count: ", fieldCount, ")");
         //---- Skip to next iteration
         continue;
      }

      //---- Extract date from field
      string dateStr = fields[0];
      //---- Extract time from field
      string timeStr = fields[1];
      //---- Extract currency from field
      string currency = fields[2];
      //---- Extract event description (handle commas in event name)
      string event = fields[3];
      //---- Combine multiple fields if event name contains commas
      for (int j = 4; j < fieldCount - 4; j++) {
         event += "," + fields[j];
      }
      //---- Extract importance from field
      string importance = fields[fieldCount - 4];
      //---- Extract actual value from field
      string actualStr = fields[fieldCount - 3];
      //---- Extract forecast value from field
      string forecastStr = fields[fieldCount - 2];
      //---- Extract previous value from field
      string previousStr = fields[fieldCount - 1];

      //---- Convert date and time to datetime format
      datetime eventDateTime = StringToTime(dateStr + " " + timeStr);
      //---- Check if datetime conversion failed
      if (eventDateTime == 0) {
         //---- Print error for invalid datetime
         Print("Error: Invalid datetime conversion for line ", i, ": ", dateStr, " ", timeStr);
         //---- Skip to next iteration
         continue;
      }

      //---- Resize events array for new event
      ArrayResize(allEvents, eventIndex + 1);
      //---- Assign event date
      allEvents[eventIndex].eventDate = dateStr;
      //---- Assign event time
      allEvents[eventIndex].eventTime = timeStr;
      //---- Assign event currency
      allEvents[eventIndex].currency = currency;
      //---- Assign event description
      allEvents[eventIndex].event = event;
      //---- Assign event importance
      allEvents[eventIndex].importance = importance;
      //---- Convert and assign actual value
      allEvents[eventIndex].actual = StringToDouble(actualStr);
      //---- Convert and assign forecast value
      allEvents[eventIndex].forecast = StringToDouble(forecastStr);
      //---- Convert and assign previous value
      allEvents[eventIndex].previous = StringToDouble(previousStr);
      //---- Print loaded event details
      Print("Loaded event ", eventIndex, ": ", dateStr, " ", timeStr, ", ", currency, ", ", event);
      //---- Increment event index
      eventIndex++;
   }

   //---- Print total events loaded
   Print("Loaded ", eventIndex, " events from resource into array.");
   //---- Return success if events were loaded
   return eventIndex > 0;
}

Definimos la función "LoadEventsFromResource" para cargar los datos de eventos económicos desde el recurso integrado para la prueba de estrategias. Asignamos el recurso "EconomicCalendarData" a "fileData" e imprimimos su contenido sin gestionar con la función "Print", incluyendo su tamaño mediante la función StringLen, con fines de depuración. Dividimos "fileData" en la array "lines" utilizando la función StringSplit con un delimitador de salto de línea, almacenamos el recuento en "lineCount" y, si "lineCount" es igual o inferior a 1, mostramos un mensaje de error y devolvemos "false". Restablecemos la array "allEvents" a cero con la función ArrayResize e inicializamos "eventIndex" en 0; a continuación, recorremos "lines" empezando por el índice 1 (omitiendo el encabezado). Para cada línea, comprobamos si está vacía con StringLen; si es así, mostramos un mensaje de omisión y continuamos; de lo contrario, la dividimos en "campos" utilizando comas.

Si "fieldCount" es menor que 8, mostramos un mensaje de error y pasamos al siguiente caso; de lo contrario, extraemos "dateStr", "timeStr" y "currency", y creamos "event" concatenando los campos (tratando las comas) en un bucle; a continuación, obtenemos "importance", "actualStr", "forecastStr" y "previousStr". Convertimos "dateStr" y "timeStr" en "eventDateTime" con la función StringToTime; si falla, se omite y se genera un error; a continuación, redimensionamos "allEvents" con "ArrayResize", asignamos todos los valores —convirtiendo los números con StringToDouble—, imprimimos el evento e incrementamos "eventIndex". Finalmente, imprimimos el "eventIndex" total y devolvemos verdadero si se cargaron eventos, lo que garantiza que los datos estén listos para el Probador de estrategias. Ahora podemos llamar a esta función durante la inicialización en el modo de prueba.

else {
   //---- Check if resource data is empty in tester mode
   if (StringLen(EconomicCalendarData) == 0) {
      //---- Print error for empty resource
      Print("Error: Resource EconomicCalendarData is empty. Please run in live mode, add the file as a resource, and recompile.");
      //---- Return initialization failure
      return(INIT_FAILED);
   }
   //---- Print message for tester mode
   Print("Running in Strategy Tester, using embedded resource: Database\\EconomicCalendar.csv");

   //---- Load events from resource
   if (!LoadEventsFromResource()) {
      //---- Print error if loading fails
      Print("Failed to load events from resource.");
      //---- Return initialization failure
      return(INIT_FAILED);
   }
}

Aquí, si "EconomicCalendarData" está vacío según StringLen, mostramos un mensaje de error y devolvemos "INIT_FAILED"; en caso contrario, mostramos un mensaje de modo de prueba con la función "Print" y llamamos a "LoadEventsFromResource", devolviendo "INIT_FAILED" junto con un error si falla. Esto garantizará que los datos de nuestro evento se carguen correctamente para el backtesting. Este es el resultado.

DATOS CARGADOS EN MODO DE PRUEBA

A partir de la imagen, podemos confirmar que los datos se han cargado correctamente. La manipulación incorrecta de los datos y la omisión de líneas vacías también se gestionan correctamente. Ahora podemos pasar al controlador de eventos OnTick y simular la gestión de datos como si estuviéramos en modo real. Para ello, queremos gestionar los datos por barra y no en cada tick.

//---- Variable to track last bar time
datetime lastBarTime = 0;

//---- Tick event handler
void OnTick() {
   //---- Get current bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   //---- Check if bar time has changed
   if (currentBarTime != lastBarTime) {
      //---- Update last bar time
      lastBarTime = currentBarTime;

      //----
   }
}

Definimos "lastBarTime" como una variable "datetime" inicializada en 0 para registrar la hora de la barra anterior. En la función OnTick, recuperamos la hora de la barra actual con la función iTime utilizando _Symbol, _Period y el índice de barra 0, y la almacenamos en "currentBarTime"; si "currentBarTime" difiere de "lastBarTime", actualizamos "lastBarTime" a "currentBarTime", asegurándonos de que el sistema reaccione ante las nuevas barras para la gestión de eventos. A continuación, podemos definir una función para gestionar los datos de simulación en tiempo real siguiendo un formato similar al de la versión anterior, tal y como se muestra a continuación.

//---- Function to filter and print economic events
void FilterAndPrintEvents(datetime barTime) {
   //---- Get total number of events
   int totalEvents = ArraySize(allEvents);
   //---- Print total events considered
   Print("Total considered data size: ", totalEvents, " events");

   //---- Check if there are events to filter
   if (totalEvents == 0) {
      //---- Print message if no events loaded
      Print("No events loaded to filter.");
      //---- Exit function
      return;
   }

   //---- Array to store filtered events
   EconomicEvent filteredEvents[];
   //---- Counter for filtered events
   int filteredCount = 0;

   //---- Variables for time range
   datetime timeBefore, timeAfter;
   //---- Apply time filter if enabled
   if (ApplyTimeFilter) {
      //---- Structure for bar time
      MqlDateTime barStruct;
      //---- Convert bar time to structure
      TimeToStruct(barTime, barStruct);

      //---- Calculate time before event
      MqlDateTime timeBeforeStruct = barStruct;
      //---- Subtract hours before
      timeBeforeStruct.hour -= HoursBefore;
      //---- Subtract minutes before
      timeBeforeStruct.min -= MinutesBefore;
      //---- Adjust for negative minutes
      if (timeBeforeStruct.min < 0) {
         timeBeforeStruct.min += 60;
         timeBeforeStruct.hour -= 1;
      }
      //---- Adjust for negative hours
      if (timeBeforeStruct.hour < 0) {
         timeBeforeStruct.hour += 24;
         timeBeforeStruct.day -= 1;
      }
      //---- Convert structure to datetime
      timeBefore = StructToTime(timeBeforeStruct);

      //---- Calculate time after event
      MqlDateTime timeAfterStruct = barStruct;
      //---- Add hours after
      timeAfterStruct.hour += HoursAfter;
      //---- Add minutes after
      timeAfterStruct.min += MinutesAfter;
      //---- Adjust for minutes overflow
      if (timeAfterStruct.min >= 60) {
         timeAfterStruct.min -= 60;
         timeAfterStruct.hour += 1;
      }
      //---- Adjust for hours overflow
      if (timeAfterStruct.hour >= 24) {
         timeAfterStruct.hour -= 24;
         timeAfterStruct.day += 1;
      }
      //---- Convert structure to datetime
      timeAfter = StructToTime(timeAfterStruct);

      //---- Print time range for debugging
      Print("Bar time: ", TimeToString(barTime), ", Time range: ", TimeToString(timeBefore), " to ", TimeToString(timeAfter));
   } else {
      //---- Print message if no time filter applied
      Print("Bar time: ", TimeToString(barTime), ", No time filter applied, using StartDate to EndDate only.");
      //---- Set time range to date inputs
      timeBefore = StartDate;
      timeAfter = EndDate;
   }

   //---- Loop through all events for filtering
   for (int i = 0; i < totalEvents; i++) {
      //---- Convert event date and time to datetime
      datetime eventDateTime = StringToTime(allEvents[i].eventDate + " " + allEvents[i].eventTime);
      //---- Check if event is within date range
      bool inDateRange = (eventDateTime >= StartDate && eventDateTime <= EndDate);
      //---- Skip if not in date range
      if (!inDateRange) continue;

      //---- Time Filter Check
      //---- Check if event is within time range if filter applied
      bool timeMatch = !ApplyTimeFilter || (eventDateTime >= timeBefore && eventDateTime <= timeAfter);
      //---- Skip if time doesn't match
      if (!timeMatch) continue;
      //---- Print event details if time passes
      Print("Event ", i, ": Time passes (", allEvents[i].eventDate, " ", allEvents[i].eventTime, ") - ",
            "Currency: ", allEvents[i].currency, ", Event: ", allEvents[i].event, ", Importance: ", allEvents[i].importance,
            ", Actual: ", DoubleToString(allEvents[i].actual, 2), ", Forecast: ", DoubleToString(allEvents[i].forecast, 2),
            ", Previous: ", DoubleToString(allEvents[i].previous, 2));

      //---- Currency Filter Check
      //---- Default to match if filter disabled
      bool currencyMatch = !ApplyCurrencyFilter;
      //---- Apply currency filter if enabled
      if (ApplyCurrencyFilter && ArraySize(curr_filter) > 0) {
         //---- Initially set to no match
         currencyMatch = false;
         //---- Check each currency in filter
         for (int j = 0; j < ArraySize(curr_filter); j++) {
            //---- Check if event currency matches filter
            if (allEvents[i].currency == curr_filter[j]) {
               //---- Set match to true if found
               currencyMatch = true;
               //---- Exit loop on match
               break;
            }
         }
         //---- Skip if currency doesn't match
         if (!currencyMatch) continue;
      }
      //---- Print event details if currency passes
      Print("Event ", i, ": Currency passes (", allEvents[i].currency, ") - ",
            "Date: ", allEvents[i].eventDate, " ", allEvents[i].eventTime,
            ", Event: ", allEvents[i].event, ", Importance: ", allEvents[i].importance,
            ", Actual: ", DoubleToString(allEvents[i].actual, 2), ", Forecast: ", DoubleToString(allEvents[i].forecast, 2),
            ", Previous: ", DoubleToString(allEvents[i].previous, 2));

      //---- Impact Filter Check
      //---- Default to match if filter disabled
      bool impactMatch = !ApplyImpactFilter;
      //---- Apply impact filter if enabled
      if (ApplyImpactFilter && ArraySize(imp_filter) > 0) {
         //---- Initially set to no match
         impactMatch = false;
         //---- Check each importance in filter
         for (int k = 0; k < ArraySize(imp_filter); k++) {
            //---- Check if event importance matches filter
            if (allEvents[i].importance == imp_filter[k]) {
               //---- Set match to true if found
               impactMatch = true;
               //---- Exit loop on match
               break;
            }
         }
         //---- Skip if importance doesn't match
         if (!impactMatch) continue;
      }
      //---- Print event details if impact passes
      Print("Event ", i, ": Impact passes (", allEvents[i].importance, ") - ",
            "Date: ", allEvents[i].eventDate, " ", allEvents[i].eventTime,
            ", Currency: ", allEvents[i].currency, ", Event: ", allEvents[i].event,
            ", Actual: ", DoubleToString(allEvents[i].actual, 2), ", Forecast: ", DoubleToString(allEvents[i].forecast, 2),
            ", Previous: ", DoubleToString(allEvents[i].previous, 2));

      //---- Add event to filtered array
      ArrayResize(filteredEvents, filteredCount + 1);
      //---- Assign event to filtered array
      filteredEvents[filteredCount] = allEvents[i];
      //---- Increment filtered count
      filteredCount++;
   }

   //---- Print summary of filtered events
   Print("After ", (ApplyTimeFilter ? "time filter" : "date range filter"),
         ApplyCurrencyFilter ? " and currency filter" : "",
         ApplyImpactFilter ? " and impact filter" : "",
         ": ", filteredCount, " events remaining.");

   //---- Check if there are filtered events to print
   if (filteredCount > 0) {
      //---- Print header for filtered events
      Print("Filtered Events at Bar Time: ", TimeToString(barTime));
      //---- Print filtered events array
      ArrayPrint(filteredEvents, 2, " | ");
   } else {
      //---- Print message if no events found
      Print("No events found within the specified range.");
   }
}

Aquí, construimos la función "FilterAndPrintEvents" para filtrar y mostrar eventos económicos relevantes para una barra determinada. Comenzamos calculando "totalEvents" con la función ArraySize en "allEvents" y lo imprimimos; si es cero, salimos con "return". Inicializamos "filteredEvents" como una array "EconomicEvent" y "filteredCount" en 0, luego definimos "timeBefore" y "timeAfter" para el filtrado por tiempo. Si "ApplyTimeFilter" es verdadero, convertimos "barTime" a "barStruct" con la función TimeToStruct, ajustamos "timeBeforeStruct" restando "HoursBefore" y "MinutesBefore" (corrigiendo los valores negativos), y "timeAfterStruct" sumando "HoursAfter" y "MinutesAfter" (corrigiendo los desbordamientos), convertimos ambos a "datetime" con la función StructToTime e imprimimos el intervalo; de lo contrario, los establecemos como "StartDate" y "EndDate" e imprimimos un mensaje de ausencia de filtro.

Recorremos "allEvents" con "totalEvents", convirtiendo cada "eventDate" y "eventTime" en "eventDateTime" con StringToTime, comprobando si se encuentra dentro de "StartDate" y "EndDate" para "inDateRange", y omitiendo los que no cumplan este criterio. Para el filtrado por fecha, comprobamos "timeMatch" con "ApplyTimeFilter" y el intervalo, y mostramos los detalles si la comprobación es satisfactoria; para la divisa, establecemos "currencyMatch" basándonos en "ApplyCurrencyFilter" y "curr_filter" mediante la función ArraySize y un bucle, y mostramos el resultado si coincide; y para el impacto, establecemos "impactMatch" con "ApplyImpactFilter" y "imp_filter", y mostramos el resultado si coincide. Los eventos coincidentes se añaden a "filteredEvents" mediante la función ArrayResize, incrementando "filteredCount".

Por último, mostramos un resumen y, si "filteredCount" es positivo, mostramos la lista filtrada con ArrayPrint; en caso contrario, mostramos un mensaje indicando que no hay eventos, lo que garantiza un análisis exhaustivo de los eventos para las pruebas. A continuación, llamamos a la función en el controlador de eventos "tick".

void OnTick() {
   //---- Get current bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   //---- Check if bar time has changed
   if (currentBarTime != lastBarTime) {
      //---- Update last bar time
      lastBarTime = currentBarTime;
      //---- Filter and print events for current bar
      FilterAndPrintEvents(currentBarTime);
   }
}

Al ejecutar el programa, obtenemos el siguiente resultado.

ANÁLISIS FINAL

En la imagen podemos ver que el filtrado está activado y funciona según lo previsto. Lo único que queda por hacer es probar nuestra lógica, y eso se aborda en la siguiente sección.


Pruebas

Para realizar una prueba exhaustiva, lo hemos plasmado todo en un vídeo, que puedes ver a continuación.



Conclusión

En conclusión, hemos mejorado nuestra serie "Calendario Económico MQL5" preparando el sistema para la prueba de estrategias, utilizando datos estáticos de un archivo guardado para permitir una validación retrospectiva fiable. Esto conecta el análisis de eventos en vivo con el Probador de Estrategias mediante filtros flexibles, superando las limitaciones de datos para una validación precisa de la estrategia. A continuación, exploraremos cómo optimizar la ejecución de las operaciones a partir de estos resultados y su integración en el panel de control. ¡Manténganse atentos!

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

Archivos adjuntos |
Negociamos con opciones sin opciones (Parte 1): Teoría básica y emulación a través de activos subyacentes Negociamos con opciones sin opciones (Parte 1): Teoría básica y emulación a través de activos subyacentes
El artículo describe una variante de emulación de opciones a través de un activo subyacente, implementada en el lenguaje de programación MQL5. Asimismo, se comparan las ventajas y desventajas del enfoque elegido con opciones bursátiles reales utilizando el ejemplo del mercado de futuros FORTS de la bolsa de Moscú MOEX y la bolsa de criptomonedas Bybit.
Redes neuronales en el trading: Extracción eficiente de características para una clasificación precisa (Final) Redes neuronales en el trading: Extracción eficiente de características para una clasificación precisa (Final)
El framework Mantis transforma series temporales complejas en tokens informativos y sirve como una base sólida para un agente comercial inteligente en tiempo real.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Integración de un modelo de IA en una estrategia de trading MQL5 ya existente Integración de un modelo de IA en una estrategia de trading MQL5 ya existente
Este tema se centra en la incorporación de un modelo de IA entrenado (como un modelo basado en redes LSTM o un modelo predictivo basado en aprendizaje automático) en una estrategia de trading MQL5 existente.