English Русский 中文 Deutsch 日本語
preview
Dominando los registros (Parte 4): Guardar registros en archivos

Dominando los registros (Parte 4): Guardar registros en archivos

MetaTrader 5Ejemplos |
131 1
joaopedrodev
joaopedrodev

Introducción

En el primer artículo de esta serie, Dominando los registros (Parte 1): Conceptos fundamentales y primeros pasos en MQL5, nos embarcamos en la creación de una biblioteca de registros personalizada para el desarrollo de un Asesor Experto (Expert Advisor, EA). En él, exploramos la motivación detrás de la creación de una herramienta tan esencial: superar las limitaciones de los registros nativos de MetaTrader 5 y aportar una solución robusta, personalizable y potente al universo MQL5.

Para resumir los puntos principales tratados, sentamos las bases de nuestra biblioteca estableciendo los siguientes requisitos fundamentales:

  1. Estructura robusta que utiliza el patrón Singleton, lo que garantiza la coherencia entre los componentes del código.
  2. Persistencia avanzada para almacenar registros en bases de datos, proporcionando un historial rastreable para auditorías y análisis en profundidad.
  3. Flexibilidad en las salidas, lo que permite almacenar o mostrar los registros de forma cómoda, ya sea en la consola, en archivos, en el terminal o en una base de datos.
  4. Clasificación por niveles de registro, diferenciando los mensajes informativos de las alertas críticas y los errores.
  5. Personalización del formato de salida, para satisfacer las necesidades únicas de cada desarrollador o proyecto.

Con esta base bien establecida, quedó claro que el marco de registro que estamos desarrollando será mucho más que un simple registro de eventos; será una herramienta estratégica para comprender, monitorear y optimizar el comportamiento de los EA en tiempo real.

Hasta ahora, hemos explorado los conceptos básicos de los registros, hemos aprendido a darles formato y hemos comprendido cómo los controladores controlan el destino de los mensajes. Pero, ¿dónde almacenamos estos registros para futuras consultas? Ahora, en este cuarto artículo, analizaremos más detenidamente el proceso de guardar registros en archivos. ¡Empecemos!


¿Por qué guardar registros en archivos?

Guardar registros en archivos es una práctica esencial para cualquier sistema que valore la solidez y el mantenimiento eficiente. Imagina la siguiente situación: tu Asesor Experto lleva días funcionando y, de repente, se produce un error inesperado. ¿Cómo puedes entender lo que pasó? Sin registros permanentes, sería como intentar resolver un rompecabezas sin tener todas las piezas.

Los archivos de registro no son solo una forma de almacenar mensajes. Representan la memoria del sistema. Estas son las principales razones para adoptarlas:

  1. Persistencia e historia

    Los registros guardados en archivos permanecen disponibles incluso después de que el programa haya finalizado. Esto permite realizar consultas históricas para comprobar el rendimiento, comprender los comportamientos pasados e identificar patrones a lo largo del tiempo.

  2. Auditoría y transparencia

    En proyectos críticos, como en el mercado financiero, mantener un historial detallado es esencial para las auditorías o las justificaciones de las decisiones automatizadas. Un registro bien guardado puede ser su mejor defensa en caso de preguntas.

  3. Diagnóstico y depuración

    Con los archivos de registro, puede rastrear errores específicos, monitorear eventos críticos y analizar cada paso de la ejecución del sistema.

  4. Flexibilidad de acceso

    A diferencia de los registros que se muestran en la consola o terminal, se puede acceder a los archivos de forma remota o compartirlos con los equipos, lo que genera una vista compartida y detallada de los eventos importantes.

  5. Automatización e integración

    Los archivos pueden ser leídos y analizados por herramientas automatizadas, enviando alertas ante problemas críticos o creando informes detallados sobre el rendimiento.

Al guardar los registros en archivos, transformas un recurso simple en una herramienta para la gestión, el seguimiento y la mejora. No es necesario que entre en muchos detalles aquí para justificar la importancia de guardar estos datos en un archivo; pasemos al tema principal en el siguiente apartado: comprender cómo implementar este recurso de manera eficiente en nuestra biblioteca.

Antes de pasar directamente al código, es importante definir las funcionalidades que debe ofrecer el gestor de archivos. A continuación, detallo cada uno de los requisitos necesarios:

  • Personalización de directorios, nombres y tipos de archivo

    Permite a los usuarios configurar:

    • El directorio donde se almacenarán los registros.
    • El nombre de los archivos de registro, lo que garantiza un mayor control y organización.
    • El formato del archivo de salida, con soporte para .log, .txt y .json.
  • Configuración de codificación

    Admite diferentes tipos de codificación para archivos de registro, tales como:

    • UTF-8 (estándar recomendado).
    • UTF-7, página de códigos ANSI (ANSI Code Page, ACP) u otras, según sea necesario.
  • Informe de errores de la biblioteca

    La biblioteca debe incluir un sistema para identificar e informar sobre errores en su propia ejecución:

    • Los mensajes de error se muestran directamente en la consola del terminal.
    • Información clara para facilitar el diagnóstico y la resolución de problemas.


Trabajar con archivos en MQL5

En MQL5, trabajar con archivos requiere una comprensión básica de cómo el lenguaje maneja estas operaciones. Si desea profundizar realmente en las complejas operaciones de lectura, escritura y uso de indicadores, no puedo sino recomendarle que lea el artículo Principios de programación en MQL5: Archivos de Dmitry Fedoseev. Proporciona una visión general completa y detallada del tema, de una manera que transforma la complejidad en algo claro, sin perder profundidad.

Pero lo que buscamos aquí es algo un poco más directo y objetivo. No nos perderemos en los detalles minuciosos, porque mi misión es enseñarte lo esencial: abrir, manipular y cerrar archivos de una manera sencilla y práctica.

  1. Comprender los directorios de archivos en MQL5 En MQL5, todos los archivos manejados por las funciones estándar se almacenan automáticamente en la carpeta MQL5/Files, que se encuentra dentro del directorio de instalación del terminal. Esto significa que al trabajar con archivos en MQL5, solo necesita especificar la ruta relativa desde esta carpeta base, sin necesidad de incluir la ruta completa. Por ejemplo, al guardar en logs/expert.log, la ruta completa será:

    <terminal folder>/MQL5/Files/logs/expert.log

  1. Creación y apertura de archivos La función para abrir o crear archivos es FileOpen. Requiere como argumento obligatorio la ruta del archivo (después de MQL5/Files) y algunos indicadores que determinan cómo se gestionará el archivo. Las banderas que utilizaremos son:

    • FILE_READ: Permite abrir el archivo para su lectura.
    • FILE_WRITE: Permite abrir el archivo para escritura.
    • FILE_ANSI: Especifica que el contenido se escribirá utilizando cadenas en formato ANSI (cada carácter ocupa un byte).

    Una característica útil de MQL5 es que, al combinar FILE_READ y FILE_WRITE, crea automáticamente el archivo si no existe. Esto elimina la necesidad de realizar comprobaciones manuales de existencia.

  2. Cerrar el archivo: Por último, cuando termine de trabajar con el archivo, utilice la función FileClose() para cerrar el procesamiento del archivo.

Aquí tenéis un ejemplo práctico de cómo abrir (o crear) y cerrar un archivo en MQL5:

int OnInit()
  {
   //--- Open the file and store the handler
   int handle_file = FileOpen("logs\\expert.log", FILE_READ|FILE_WRITE|FILE_ANSI, '\t', CP_UTF8);
   
   //--- If opening fails, display an error in the terminal log
   if(handle_file == INVALID_HANDLE)
     {
      Print("[ERROR] Unable to open log file. Asegúrese de que el directorio existe y tiene permisos de escritura. (Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   
   //--- Close file
   FileClose(handle_file);
   
   return(INIT_SUCCEEDED);
  }

Ahora que hemos abierto el archivo, es hora de aprender a escribir en él.

  1. Posicionamiento del puntero de escritura: Antes de escribir, debemos definir dónde se insertarán los datos. Utilizamos la función FileSeek() para colocar el puntero de escritura al final del archivo. Esto evita sobrescribir el contenido existente.
  2. Escritura de datos: El método FileWrite() escribe cadenas en el archivo. No es necesario utilizar «\n» para saltar de línea. Al utilizar este método, la próxima vez que se escriban los datos, se escribirán automáticamente en otra línea, para garantizar una mejor organización.

A continuación se explica cómo hacerlo en la práctica:

int OnInit()
  {
   //--- Open the file and store the handler
   int handle_file = FileOpen("logs\\expert.log", FILE_READ|FILE_WRITE|FILE_ANSI, '\t', CP_UTF8);
   
   //--- If opening fails, display an error in the terminal log
   if(handle_file == INVALID_HANDLE)
     {
      Print("[ERROR] Unable to open log file. Asegúrese de que el directorio existe y tiene permisos de escritura. (Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   
   //--- Move the writing pointer
   FileSeek(handle_file, 0, SEEK_END);
   
   //--- Writes the content inside the file
   FileWrite(handle_file, "[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms");
   
   //--- Close file
   FileClose(handle_file);
   
   return(INIT_SUCCEEDED);
  }

Después de ejecutar el código, verás un archivo creado en la carpeta "Files". La ruta completa será algo así como:

<Terminal folder>/MQL5/Files/logs/expert.log

Si abres el archivo, verás exactamente lo que escribimos:

[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms

Ahora que hemos aprendido a manejar archivos de una forma muy sencilla en MQL5, vamos a añadir este trabajo a la clase de controlador responsable de guardar archivos, CLogifyHandlerFile.


Creación de las configuraciones de la clase CLogifyHandlerFile

Ahora, vamos a detallar cómo podemos configurar esta clase para manejar de manera eficiente la rotación de archivos que mencioné en la sección de requisitos. Pero, ¿qué significa exactamente "rotación de archivos"? Permítanme explicarles con más detalle. La rotación es una práctica esencial para evitar ese escenario caótico en el que un único archivo de registro crece indefinidamente, evitando la acumulación excesiva de datos en un solo archivo de registro, lo que puede dificultar el análisis posterior, convirtiendo los registros en una auténtica pesadilla lenta, difícil de manejar y casi imposible de descifrar.

Imagine este escenario: un Asesor Experto funcionando durante semanas o meses, registrando cada evento, error o notificación en el mismo archivo. Pronto, ese registro comienza a alcanzar tamaños considerables, lo que hace que la lectura e interpretación de la información sea bastante compleja. Aquí es donde entra en juego la rotación. Nos permite dividir esta información en fragmentos más pequeños y organizados, lo que facilita enormemente su lectura y análisis.

Las dos formas más comunes de hacerlo son:

  1. Por tamaño: Se establece un límite de tamaño, normalmente en megabytes (MB), para el archivo de registro. Cuando se alcanza este límite, se crea automáticamente un nuevo archivo y el ciclo vuelve a comenzar. Este enfoque resulta muy práctico cuando el objetivo es controlar el crecimiento logarítmico, sin necesidad de ceñirse a un calendario. Tan pronto como el archivo actual alcanza el límite de tamaño (en megabytes), ocurre el siguiente flujo: El archivo de registro actual se renombra, obteniendo un índice, como "log1.log". Los archivos existentes en el directorio también se renumeran, por ejemplo, "log1.log" pasa a ser "log2.log". Si el número de archivos alcanza el máximo permitido, se eliminan los archivos más antiguos. Este enfoque resulta útil para limitar tanto el espacio ocupado por los registros como el número de archivos guardados.
  2. Por fecha: En este caso, se crea un nuevo archivo de registro cada día. Cada uno tiene la fecha en que fue creado en su nombre, por ejemplo log_2025-01-19.log, lo que resuelve la mayor parte del problema de organizar los registros. Este enfoque es perfecto cuando necesitas echar un vistazo específico a un día en particular, sin perderte en un único archivo gigantesco. Este es el método que más utilizo para guardar los registros de mis Asesores Expertos; todo es más limpio, más directo y más fácil de navegar.

Además, también puede limitar el número de archivos de registro almacenados. Este control es muy importante para evitar la acumulación innecesaria de registros antiguos. Imagina que configuras el sistema para que conserve los 30 archivos más recientes; cuando aparezca el número 31, el sistema descarta automáticamente el más antiguo, lo que evita la acumulación de registros muy antiguos en el disco y se conservan los más recientes.

Otro detalle crucial es el uso de una caché. En lugar de escribir cada mensaje directamente en el archivo tan pronto como llega, los mensajes se almacenan temporalmente en la caché. Cuando la caché alcanza un límite establecido, vuelca todo el contenido del archivo de una sola vez. Esto se traduce en menos operaciones de lectura y escritura en el disco, un mejor rendimiento y una mayor vida útil para sus dispositivos de almacenamiento.

Ahora que entendemos el concepto de rotación de archivos, vamos a crear una estructura llamada MqlLogifyHandleFileConfig para almacenar todas las configuraciones para la clase CLogifyHandlerFile. Esta estructura será la responsable de contener los parámetros que definen cómo se gestionarán los registros.

La primera parte de la estructura consistirá en definir enumeraciones para los tipos de rotación y las extensiones de archivo que se utilizarán:

//+------------------------------------------------------------------+
//| ENUMS for log rotation and file extension                        |
//+------------------------------------------------------------------+
enum ENUM_LOG_ROTATION_MODE
  {
   LOG_ROTATION_MODE_NONE = 0,       // No rotation
   LOG_ROTATION_MODE_DATE,           // Rotate based on date
   LOG_ROTATION_MODE_SIZE,           // Rotate based on file size
  };
enum ENUM_LOG_FILE_EXTENSION
  {
   LOG_FILE_EXTENSION_TXT = 0,       // .txt file
   LOG_FILE_EXTENSION_LOG,           // .log file
   LOG_FILE_EXTENSION_JSON,          // .json file
  };

La propia estructura MqlLogifyHandleFileConfig contendrá los siguientes parámetros:

  • directory: Directorio donde se almacenarán los archivos de registro.
  • base_filename: Nombre del archivo base, sin la extensión.
  • file_extension: Tipo de extensión del archivo de registro (como .txt, .log o .json).
  • rotation_mode: Modo de rotación de archivos.
  • messages_per_flush: Número de mensajes de registro que se almacenarán en caché antes de escribirlos en el archivo.
  • codepage: Codificación utilizada para los archivos de registro (como UTF-8 o ANSI).
  • max_file_size_mb: Tamaño máximo de cada archivo de registro, si la rotación se basa en el tamaño.
  • max_file_count: Número máximo de archivos de registro que se conservarán antes de eliminar los más antiguos.

Además de los constructores y destructores, añadiré métodos auxiliares a la estructura para configurar cada uno de los modos de rotación, diseñados para que el proceso de configuración sea más práctico y, sobre todo, fiable. Estos métodos no solo sirven para dar elegancia, sino que garantizan que no se pase por alto ningún detalle crítico durante la configuración.

Por ejemplo, si el modo de rotación está configurado en por fecha ( LOG_ROTATION_MODE_DATE ), intentar configurar el atributo max_file_size_mb no tiene ningún sentido, ya que, al fin y al cabo, este parámetro solo es relevante en el modo por tamaño ( LOG_ROTATION_MODE_SIZE ). La función de estos métodos es evitar inconsistencias como esta, protegiendo el sistema contra configuraciones no válidas.

Si, por casualidad, no se especifica un parámetro esencial, el sistema toma medidas. Puede completar automáticamente un valor predeterminado, emitiendo una advertencia al desarrollador, lo que garantiza que el flujo sea robusto y sin lugar a sorpresas desagradables.

Los métodos auxiliares que implementaremos son:

  • CreateNoRotationConfig(): Configuración para la rotación sin archivos (todos los registros se envían al mismo archivo sin rotación).
  • CreateDateRotationConfig(): Configuración para la rotación basada en fechas.
  • CreateSizeRotationConfig(): Configuración para la rotación basada en el tamaño del archivo.
  • ValidateConfig(): Método que valida si todas las configuraciones son correctas y están listas para usar. (este es un método que utilizará automáticamente la clase y no el desarrollador que utilizará la biblioteca)

Aquí está la implementación completa de la estructura:

//+------------------------------------------------------------------+
//| Struct: MqlLogifyHandleFileConfig                                |
//+------------------------------------------------------------------+
struct MqlLogifyHandleFileConfig
  {
   string directory;                         // Directory for log files
   string base_filename;                     // Base file name
   ENUM_LOG_FILE_EXTENSION file_extension;   // File extension type
   ENUM_LOG_ROTATION_MODE rotation_mode;     // Rotation mode
   int messages_per_flush;                   // Messages before flushing
   uint codepage;                            // Encoding (e.g., UTF-8, ANSI)
   ulong max_file_size_mb;                   // Max file size in MB for rotation
   int max_file_count;                       // Max number of files before deletion
   
   //--- Default constructor
   MqlLogifyHandleFileConfig(void)
     {
      directory = "logs";                    // Default directory
      base_filename = "expert";              // Default base name
      file_extension = LOG_FILE_EXTENSION_LOG;// Default to .log extension
      rotation_mode = LOG_ROTATION_MODE_SIZE;// Default size-based rotation
      messages_per_flush = 100;              // Default flush threshold
      codepage = CP_UTF8;                    // Default UTF-8 encoding
      max_file_size_mb = 5;                  // Default max file size in MB
      max_file_count = 10;                   // Default max file count
     }

   //--- Destructor
   ~MqlLogifyHandleFileConfig(void)
     {
     }

   //--- Create configuration for no rotation
   void CreateNoRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_NONE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
     }

   //--- Create configuration for date-based rotation
   void CreateDateRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, int max_files=10, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_DATE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
      max_file_count = max_files;
     }

   //--- Create configuration for size-based rotation
   void CreateSizeRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, ulong max_size=5, int max_files=10, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_SIZE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
      max_file_size_mb = max_size;
      max_file_count = max_files;
     }
   
   //--- Validate configuration
   bool ValidateConfig(string &error_message)
     {
      //--- Saves the return value
      bool is_valid = true;
      
      //--- Check if the directory is not empty
      if(directory == "")
        {
         directory = "logs";
         error_message = "The directory cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the base filename is not empty
      if(base_filename == "")
        {
         base_filename = "expert";
         error_message = "The base filename cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the number of messages per flush is positive
      if(messages_per_flush <= 0)
        {
         messages_per_flush = 100;
         error_message = "The number of messages per flush must be greater than zero.";
         is_valid = false;
        }
      
      //--- Check if the codepage is valid (verify against expected values)
      if(codepage != CP_ACP
      && codepage != CP_MACCP
      && codepage != CP_OEMCP
      && codepage != CP_SYMBOL
      && codepage != CP_THREAD_ACP
      && codepage != CP_UTF7
      && codepage != CP_UTF8)
        {
         codepage = CP_UTF8;
         error_message = "The specified codepage is invalid.";
         is_valid = false;
        }
      
      //--- Validate limits for size-based rotation
      if(rotation_mode == LOG_ROTATION_MODE_SIZE)
        {
         if(max_file_size_mb <= 0)
           {
            max_file_size_mb = 5;
            error_message = "The maximum file size (in MB) must be greater than zero.";
            is_valid = false;
           }
         if(max_file_count <= 0)
           {
            max_file_count = 10;
            error_message = "The maximum number of files must be greater than zero.";
            is_valid = false;
           }
        }
      
      //--- Validate limits for date-based rotation
      if(rotation_mode == LOG_ROTATION_MODE_DATE)
        {
         if(max_file_count <= 0)
           {
            max_file_count = 10;
            error_message = "The maximum number of files for date-based rotation must be greater than zero.";
            is_valid = false;
           }
        }
   
      //--- No errors found
      error_message = "";
      return(is_valid);
     }
  };
//+------------------------------------------------------------------+

Un detalle interesante que quiero destacar aquí es cómo funciona la función ValidateConfig(). Al analizar esta función, se observa algo interesante: cuando detecta un error en algún valor de configuración, no es como si devolviera inmediatamente false, indicando que algo ha salido mal. Actúa primero, tomando medidas correctivas para resolver el problema automáticamente, todo ello antes de devolver un resultado definitivo.

En primer lugar, restablece el valor no válido, devolviéndolo a su valor predeterminado. Esto, en cierto modo, «corrige» temporalmente la configuración, sin permitir que el error impida que el proceso del programa siga su flujo. A continuación, para no dejar la situación sin explicar, la función asigna un mensaje detallado, indicando claramente dónde se produjo el error y qué hay que ajustar. Y, por último, la función marca la variable llamada is_valid como falsa, lo que indica que algo ha salido mal. Solo al final, después de tomar todas estas medidas, devuelve esta variable, con el estado final, que indicará si la configuración ha pasado y es válida o no.

Pero lo que hace que esto sea aún más interesante es cómo la función gestiona múltiples errores. Si hay más de un valor incorrecto al mismo tiempo, no se centra en corregir el primer error que aparece, dejando los demás para más tarde. Por el contrario, va tras todos ellos a la vez, corrigiendo todo al mismo tiempo. Al final, la función devuelve el mensaje explicando cuál fue el último error corregido, asegurándose de que no se omita nada.

Este tipo de enfoque es valioso y ayuda al trabajo del desarrollador. Durante el desarrollo de un sistema, es habitual que algunos valores se definan incorrectamente o por error. Lo bueno aquí es que la función tiene una capa adicional de seguridad, que corrige los errores automáticamente, sin esperar a que el programador los detecte uno por uno. Después de todo, los pequeños errores, si no se tratan, pueden provocar fallos más graves, como, por ejemplo, la imposibilidad de guardar los registros de los archivos de registro. Esta automatización en el manejo de errores que creé evita que pequeños fallos interrumpan el funcionamiento del sistema, lo que ayuda a que todo siga funcionando.


Implementación de la clase CLogifyHandlerFile

Tenemos la clase que ya se creó en el último artículo, solo haremos modificaciones para que sea funcional. Aquí detallaré cada ajuste realizado para asegurar que comprenda cómo funciona todo.

En el ámbito privado de la clase, añadimos variables y algunos métodos auxiliares importantes:

  1. Configuración: Creamos una variable m_config de tipo MqlLogifyHandleFileConfig para almacenar los ajustes relacionados con el sistema de registro.
  2. También implementé los métodos SetConfig() y GetConfig() para definir y acceder a la configuración de la clase.

Aquí está la estructura inicial de la clase, con las definiciones y métodos básicos:

//+------------------------------------------------------------------+
//| class : CLogifyHandlerFile                                       |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : CLogifyHandlerFile                                 |
//| Heritage    : CLogifyHandler                                     |
//| Description : Log handler, inserts data into file, supports      |
//| rotation modes.                                                  |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerFile : public CLogifyHandler
  {
private:
   //--- Config
   MqlLogifyHandleFileConfig m_config;
   
public:
   //--- Configuration management
   void              SetConfig(MqlLogifyHandleFileConfig &config);
   MqlLogifyHandleFileConfig GetConfig(void);
  };
//+------------------------------------------------------------------+
//| Set configuration                                                |
//+------------------------------------------------------------------+
void CLogifyHandlerFile::SetConfig(MqlLogifyHandleFileConfig &config)
  {
   m_config = config;
   
   //--- Validade
   string err_msg = "";
   if(!m_config.ValidateConfig(err_msg))
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg);
     }
  }
//+------------------------------------------------------------------+
//| Get configuration                                                |
//+------------------------------------------------------------------+
MqlLogifyHandleFileConfig CLogifyHandlerFile::GetConfig(void)
  {
   return(m_config);
  }
//+------------------------------------------------------------------+

Enumeraré los métodos auxiliares y explicaré con más detalle cómo funcionan. He implementado tres métodos útiles que se utilizarán en la gestión de archivos:

  1. LogFileExtensionToStr(): Convierte el valor de la enumeración ENUM_LOG_FILE_EXTENSION en una cadena que representa la extensión del archivo. La enumeración define los valores posibles para el tipo de archivo, como .log, .txt y .json.

    //+------------------------------------------------------------------+
    //| Convert log file extension enum to string                        |
    //+------------------------------------------------------------------+
    string CLogifyHandlerFile::LogFileExtensionToStr(ENUM_LOG_FILE_EXTENSION file_extension)
      {
       switch(file_extension)
         {
          case LOG_FILE_EXTENSION_LOG:
            return(".log");
          case LOG_FILE_EXTENSION_TXT:
            return(".txt");
          case LOG_FILE_EXTENSION_JSON:
            return(".json");
         }
       //--- Default return
       return(".txt");
      }
    //+------------------------------------------------------------------+

  2. LogPath(): Esta función se encarga de generar la ruta completa del archivo de registro basándose en la configuración actual de la clase. En primer lugar, convierte la extensión del archivo configurado utilizando la función LogFileExtensionToStr(). A continuación, comprueba el tipo de rotación configurado. Si la rotación se basa en el tamaño del archivo o no hay rotación, solo devuelve el nombre del archivo en el directorio configurado. Si la rotación se basa en la fecha, incluye la fecha actual (formato AAAA-MM-DD) como prefijo en el nombre del archivo.

    //+------------------------------------------------------------------+
    //| Generate log file path based on config                           |
    //+------------------------------------------------------------------+
    string CLogifyHandlerFile::LogPath(void)
      {
       string file_extension = this.LogFileExtensionToStr(m_config.file_extension);
       string base_name = m_config.base_filename + file_extension;
       
       if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE || m_config.rotation_mode == LOG_ROTATION_MODE_NONE)
         {
          return(m_config.directory + "\\\\" + base_name);
         }
       else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE)
         {
          MqlDateTime date;
          TimeCurrent(date);
          string date_str = IntegerToString(date.year) + "-" + IntegerToString(date.mon, 2, '0') + "-" + IntegerToString(date.day, 2, '0');
          base_name = date_str + (m_config.base_filename != "" ? "-" + m_config.base_filename : "") + file_extension;
          return(m_config.directory + "\\\\" + base_name);
         }
       
       //--- Default return
       return(base_name);
      }
    //+------------------------------------------------------------------+

El método Emit() se encarga de registrar los mensajes de registro en un archivo. En el código actual, simplemente muestra los registros en la consola del terminal. Vamos a mejorar esto para que abra el archivo de registro, agregue una nueva línea con los datos formateados y cierre el archivo después de escribir. Si no se puede abrir el archivo, se mostrará un mensaje de error en la consola.

void CLogifyHandlerFile::Emit(MqlLogifyModel &data)
  {
   //--- Checks if the configured level allows
   if(data.level >= this.GetLevel())
     {
      //--- Get the full path of the file
      string log_path = this.LogPath();
      
      //--- Open file
      ResetLastError();
                  int handle_file = FileOpen(log_path, FILE_READ|FILE_WRITE|FILE_ANSI, '\t', m_config.codepage);
                  if(handle_file == INVALID_HANDLE)
                    {
                     Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Asegúrese de que el directorio existe y tiene permisos de escritura. (Code: "+IntegerToString(GetLastError())+")");
                     return;
                    }
      
      //--- Write
      FileSeek(handle_file, 0, SEEK_END);
      FileWrite(handle_file, data.formated);
      
      //--- Close file
      FileClose(handle_file);
     }
  }

Así que ya tenemos la versión más sencilla de la clase que añade los registros a un archivo; realicemos algunas pruebas sencillas para verificar si las funciones básicas funcionan correctamente.


Primera prueba con archivos

Utilizaremos el archivo de prueba que ya hemos utilizado en los ejemplos anteriores, LogifyTest.mqh. El objetivo es configurar el sistema de registro para guardar los registros en archivos, utilizando la clase base CLogify y el controlador de archivos que acabamos de implementar.

  1. Creamos una variable de tipo MqlLogifyHandleFileConfig para almacenar la configuración específica del controlador de archivos.
  2. Configuramos el controlador para que utilice el formato y las reglas deseadas, como la rotación de archivos por tamaño.
  3. Añadimos este controlador a la clase base CLogify.
  4. Configuramos un formateador para determinar cómo se mostrará cada registro en el archivo.

Vea el código completo:

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,10);
   
   //--- Handler File
   CLogifyHandlerFile *handler_file = new CLogifyHandlerFile();
   handler_file.SetConfig(m_config);
   handler_file.SetLevel(LOG_LEVEL_DEBUG);
   
   //--- Add handler in base class
   logify.AddHandler(handler_file);
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Al ejecutar el código anterior, se creará un nuevo archivo de registro en el directorio configurado (logs). Se puede visualizar en el explorador de archivos.

Al abrir el archivo en el Bloc de notas o en cualquier editor de texto, veremos el contenido generado por las pruebas de mensajes:

Antes de pasar a las mejoras, voy a realizar una prueba de rendimiento para poder entender cuánto mejora esto el rendimiento, de modo que tengamos una referencia para comparar más adelante. Dentro de la función OnTick() voy a agregar un registro al log, de modo que con cada nuevo tick el archivo de registro se abra, se escriba y se cierre.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Logs
   logify.Debug("Debug Message");
  }
//+------------------------------------------------------------------+

Utilizaré el probador de estrategias para realizar esta prueba. Incluso en las pruebas retrospectivas, el sistema de creación de archivos funciona con normalidad. Pero los archivos están guardados en otra carpeta; más adelante mostraré cómo acceder a ella. La prueba se realizará con la siguiente configuración:

Teniendo en cuenta el modelo OHLC M1, en el símbolo EURUSD con 7 días de pruebas, se tardó 5 minutos y 11 segundos en completar la prueba, considerando que en cada tick se genera un nuevo registro de log y se guarda en el archivo inmediatamente.


Pruebas con archivos JSON

Finalmente, quiero mostrar el uso práctico de los archivos de registro JSON, ya que pueden resultar útiles en algunos escenarios específicos. Para guardar como JSON, simplemente cambie el tipo de archivo en la configuración y defina un formateador válido para el formato JSON; aquí tiene un ejemplo de implementación:

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,5,5,10);
   
   //--- Handler File
   CLogifyHandlerFile *handler_file = new CLogifyHandlerFile();
   handler_file.SetConfig(m_config);
   handler_file.SetLevel(LOG_LEVEL_DEBUG);
   
   //--- Add handler in base class
   logify.AddHandler(handler_file);
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\\"datetime\\":\\"{date_time}\\", \\"level\\":\\"{levelname}\\", \\"msg\\":\\"{msg}\\"}"));
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Con los mismos mensajes de registro, este es el resultado del archivo después de ejecutar el experto en el gráfico:

{"datetime":"08:24:10", "level":"DEBUG", "msg":"RSI indicator value calculated: 72.56"}
{"datetime":"08:24:10", "level":"INFOR", "msg":"Buy order sent successfully"}
{"datetime":"08:24:10", "level":"ALERT", "msg":"Stop Loss adjusted to breakeven level"}
{"datetime":"08:24:10", "level":"ERROR", "msg":"Failed to send sell order"}
{"datetime":"08:24:10", "level":"FATAL", "msg":"Failed to initialize EA: Invalid settings"}


Conclusión

En este artículo, presentamos una guía práctica y detallada sobre cómo realizar operaciones básicas con archivos: abrir, manipular el contenido y, finalmente, cerrar el archivo de forma sencilla. También hablé de la importancia de configurar la estructura «handler», ya que, a través de esta configuración, es posible adaptar varias características, como el tipo de archivo que se va a utilizar (por ejemplo, texto, registro o incluso json) y el directorio en el que se guardará el archivo, lo que hace que la biblioteca sea muy flexible.

Además, hemos realizado mejoras específicas en la clase denominada CLogifyHandlerFile. Estos cambios permitieron registrar cada mensaje directamente en un archivo de registro. Tras esta implementación, como parte del estudio, también realicé una prueba de rendimiento para medir la eficiencia de la solución. Utilizamos un escenario específico, en el que el sistema simuló la ejecución de una estrategia de negociación sobre el activo EURUSD durante un período de una semana. Durante esta prueba, se generó un registro de bitácora para cada nuevo "tick" del mercado. Este proceso es extremadamente laborioso, ya que cada cambio en el precio del activo requiere que se guarde una nueva línea en el archivo.

Se registró el resultado final: Todo el proceso tardó 5 minutos y 11 segundos en completarse. Este resultado servirá como punto de referencia para el próximo artículo, donde implementaremos un sistema de caché (memoria temporal). El propósito de la caché es almacenar temporalmente registros, eliminando la necesidad de acceder constantemente al archivo y mejorando el rendimiento general.

Estén atentos al próximo artículo, donde exploraremos técnicas aún más avanzadas para aumentar la eficiencia y el rendimiento del sistema. ¡Nos vemos allí!

Nombre del archivo
Descripción
Experts/Logify/LogiftTest.mq5
Archivo donde probamos las funcionalidades de la biblioteca, que contiene un ejemplo práctico.
Include/Logify/Formatter/LogifyFormatter.mqh
Clase responsable de dar formato a los registros de registro, sustituyendo los marcadores de posición por valores específicos.
Include/Logify/Handlers/LogifyHandler.mqh
Clase base para gestionar los controladores de registros, incluyendo la configuración de niveles y el envío de registros.
Include/Logify/Handlers/LogifyHandlerConsole.mqh
Manejador de registros que envía registros formateados directamente a la consola del terminal en MetaTrader.
Include/Logify/Handlers/LogifyHandlerDatabase.mqh
Manejador de registros que envía registros formateados a una base de datos (actualmente solo contiene una impresión, pero pronto lo guardaremos en una base de datos sqlite real).
Include/Logify/Handlers/LogifyHandlerFile.mqh
Manejador de registros que envía registros formateados a un archivo.
Include/Logify/Logify.mqh
Clase básica para la gestión de registros, que integra niveles, modelos y formateo.
Include/Logify/LogifyLevel.mqh
Archivo que define los niveles de registro de la biblioteca Logify, lo que permite un control detallado.
Include/Logify/LogifyModel.mqh
Estructura que modela registros de registro, incluyendo detalles como nivel, mensaje, marca de tiempo y contexto.

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

Archivos adjuntos |
Logify0Part40.zip (14.21 KB)
Alpha Dolcy
Alpha Dolcy | 29 ene 2025 en 13:29
MetaQuotes:

Echa un vistazo al nuevo artículo: Dominando los Registros (Parte 4): Guardar registros en archivos.

Autor: joaopedrodev

Suena como una búsqueda digna especialmente para la prueba de espalda y la optimización.
Simulación de mercado (Parte 12): Sockets (VI) Simulación de mercado (Parte 12): Sockets (VI)
En este artículo, veremos cómo resolver algunos problemas y cuestiones al usar código escrito en Python dentro de otros programas. Más concretamente, mostraré un problema habitual que ocurre al usar Excel junto con MetaTrader 5, aunque para esta comunicación utilizaremos Python. Sin embargo, hay un pequeño inconveniente en esta implementación. No ocurre en todos los casos, sino solo en algunos específicos. Cuando ocurre, es necesario entender la razón. En este artículo, empezaré a explicar cómo resolverlo.
Simulación de mercado (Parte 11): Sockets (V) Simulación de mercado (Parte 11): Sockets (V)
Vamos a empezar a implementar la comunicación entre Excel y MetaTrader 5, pero antes es necesario entender algunas cosas importantes. Así no te quedarás rascándote la cabeza tratando de comprender por qué las cosas funcionan o no. Y antes de que frunzas el ceño ante la integración entre Python y Excel, veamos cómo podemos usar xlwings para controlar, en cierta medida, MetaTrader 5 a través de Excel. Lo que voy a mostrar aquí se centrará principalmente en la didáctica. No pienses que solo podemos hacer lo que mostraré.
La estrategia de negociación de la brecha del valor razonable inverso (Inverse Fair Value Gap, IFVG) La estrategia de negociación de la brecha del valor razonable inverso (Inverse Fair Value Gap, IFVG)
Una brecha inversa del valor razonable (Inverse Fair Value Gap, IFVG) se produce cuando el precio vuelve a una brecha del valor razonable identificada previamente y, en lugar de mostrar la reacción de apoyo o resistencia esperada, no la respeta. Este comportamiento puede indicar un posible cambio en la dirección del mercado y ofrecer una ventaja comercial contraria. En este artículo, voy a presentar mi enfoque, desarrollado por mí mismo, para cuantificar y utilizar la brecha inversa del valor razonable como estrategia para los asesores expertos de MetaTrader 5.
Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 4): Dimensionamiento dinámico de posiciones Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 4): Dimensionamiento dinámico de posiciones
El uso exitoso del trading algorítmico requiere un aprendizaje continuo e interdisciplinario. Sin embargo, la infinita gama de posibilidades puede consumir años de esfuerzo sin producir resultados tangibles. Para abordar esta cuestión, proponemos un marco que introduce gradualmente la complejidad, lo que permite a los operadores perfeccionar sus estrategias de forma iterativa en lugar de dedicar un tiempo indefinido a resultados inciertos.