Herramientas personalizadas de depuración y perfilado para el desarrollo en MQL5 (Parte I): Registro avanzado
Este es el plan:
- Introducción
- Creación de un marco de registro personalizado
- Uso del marco de registro
- Ventajas del marco de registro personalizado
- Conclusión
Introducción
Cualquiera que haya dedicado tiempo a programar asesores expertos, indicadores o scripts en MQL5 conoce esa frustración: una operación en vivo se comporta de forma extraña, una fórmula compleja arroja un resultado erróneo o su asesor experto se bloquea justo cuando el mercado se anima. La solución rápida habitual —esparcir instrucciones Print(), poner en marcha el Strategy Tester y rezar para que el problema se manifieste— deja de funcionar cuando el código crece demasiado.
MQL5 plantea dificultades de depuración que los lenguajes de programación habituales no presentan. Los sistemas de trading se ejecutan en tiempo real (por lo que la sincronización es crucial), manejan dinero real (por lo que los errores son costosos) y deben mantenerse extremadamente rápidos incluso en mercados volátiles. Las funciones integradas de MetaEditor —un depurador paso a paso, Print() y Comment() para la salida básica y un generador de perfiles de alto nivel— son útiles pero genéricas. Sencillamente, no fueron diseñados para los diagnósticos precisos que necesitan sus algoritmos de negociación.
Por eso, crear su propio conjunto de herramientas de depuración y análisis de rendimiento supone un cambio radical. Las utilidades personalizadas pueden proporcionar la información detallada y los flujos de trabajo a medida que faltan en el conjunto estándar, lo que le permite detectar errores antes, optimizar el rendimiento y salvaguardar la calidad del código.
Esta serie le guiará en la creación de un conjunto de herramientas de este tipo. Comenzaremos con la piedra angular: un marco de registro versátil mucho más potente que las llamadas dispersas a Print(); luego añadiremos depuradores avanzados, analizadores de rendimiento personalizados, un sistema de pruebas unitarias y verificadores de código estático. Al final, dispondrá de un conjunto completo de herramientas que transforman la resolución reactiva de problemas en un control de calidad proactivo.
Cada entrega es práctica: ejemplos completos de MQL5 listos para usar, explicaciones detalladas de cómo funcionan y la justificación de cada decisión de diseño. Obtendrá herramientas que podrá usar de inmediato y los conocimientos necesarios para adaptarlas a sus propios proyectos.
En primer lugar: la necesidad diagnóstica más básica de todas: ver exactamente qué hace su programa, momento a momento. Vamos a crear ese marco de registro personalizado.
Creación de un marco de registro personalizado
En esta sección, desarrollaremos un marco de registro flexible y potente que va mucho más allá de la función básica Print() proporcionada por MQL5. Nuestro registrador personalizado admitirá múltiples formatos de salida, niveles de gravedad e información contextual que harán que la depuración de sistemas de negociación complejos sea mucho más eficaz.
Por qué la función Print() habitual se queda corta
Antes de ponernos manos a la obra y construir el nuevo sistema, conviene entender por qué depender únicamente de Print() no es suficiente para proyectos profesionales:
- No existe una jerarquía de gravedad: todos los mensajes se clasifican en la misma categoría, por lo que las alertas críticas quedan sepultadas bajo la información rutinaria.
- Contexto limitado: Print() no puede indicar qué función activó el mensaje ni cuál era el estado de la aplicación en ese momento.
- Salida de una sola vía: todo se canaliza a la pestaña Expertos; no hay una ruta integrada a los archivos ni a destinos alternativos.
- Filtrado cero: no se pueden silenciar los registros de depuración detallados en producción sin silenciar también los errores que nos interesan.
- Texto no estructurado: la salida de formato libre es difícil de analizar automáticamente para las herramientas.
Nuestro marco de registro personalizado aborda cada uno de estos problemas y sienta una base sólida para la resolución de problemas en códigos de negociación complejos.
Diseñando el registrador
Construiremos un sistema limpio, modular y orientado a objetos en torno a tres componentes principales:- LogLevels: una enumeración que nombra los niveles de gravedad (DEBUG, INFO, WARN, ERROR, FATAL).
- ILogHandler: una interfaz que nos permite conectar diferentes receptores, como FileLogHandler o ConsoleLogHandler.
- CLogger: un orquestador singleton que contiene los manejadores y expone la API de registro.
A continuación, analizaremos cada parte.
Niveles de registro
Primero, definimos los niveles de gravedad en LogLevels.mqh:
enum LogLevel { LOG_LEVEL_DEBUG = 0, // Detailed information for debugging purposes. LOG_LEVEL_INFO = 1, // General information about the system's operation. LOG_LEVEL_WARN = 2, // Warnings about potential issues that are not critical. LOG_LEVEL_ERROR = 3, // Errors that affect parts of the system but allow continuity. LOG_LEVEL_FATAL = 4, // Serious problems that interrupt the system's execution. LOG_LEVEL_OFF = 5 // Turn off logging. };
Estos niveles nos permiten categorizar los mensajes por importancia y filtrarlos en consecuencia. Por ejemplo, durante el desarrollo, es posible que desee ver todos los mensajes (incluidos los de depuración), pero en producción, es posible que solo desee ver los de advertencia o superiores.
La interfaz del manejador
A continuación, definimos una interfaz para los manejadores de registros en ILogHandler.mqh:
#property strict #include "LogLevels.mqh" #include <Arrays/ArrayObj.mqh> // For managing handlers //+------------------------------------------------------------------+ //| Interface: ILogHandler | //| Description: Defines the contract for log handling mechanisms. | //| Each handler is responsible for processing and | //| outputting log messages in a specific way (e.g., to | //| console, file, database). | //+------------------------------------------------------------------+ interface ILogHandler { //--- Method to configure the handler with specific settings virtual bool Setup(const string settings=""); //--- Method to process and output a log message virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0); //--- Method to perform any necessary cleanup virtual void Shutdown(); }; //+------------------------------------------------------------------+
Este archivo de cabecera, ILogHandler.mqh, define un componente crucial del marco de registro: la interfaz ILogHandler. En MQL5, una interfaz actúa como un modelo o contrato, especificando un conjunto de métodos que cualquier clase que la implemente debe proporcionar. El objetivo de ILogHandler es estandarizar la forma en que los diferentes mecanismos de salida de registros (como escribir en la consola o en un archivo) interactúan con la clase principal del registrador.
La interfaz ILogHandler declara tres métodos virtuales que las clases de manejadores concretas deben implementar:- virtual bool Setup(const string settings=""): Este método está diseñado para inicializar y configurar el manejador de registro específico. Acepta un argumento de cadena opcional (settings) que se puede usar para pasar parámetros de configuración (como rutas de archivo, cadenas de formato o niveles mínimos de registro) al manejador durante su fase de configuración. Este método devuelve verdadero si la configuración fue exitosa y falso en caso contrario, lo que permite al registrador principal saber si el manejador está listo para usarse.
- virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0): Este es el método principal responsable de procesar y generar un único mensaje de registro. Recibe todos los detalles necesarios sobre el evento de registro: la marca de tiempo (time), el nivel de gravedad (level de LogLevels.mqh), la fuente u origen del mensaje (origin), el texto real del mensaje de registro (message) y un ID de asesor experto opcional (expert_id). Cada clase implementadora definirá cómo formatear y dónde enviar esta información en función de su propósito específico (por ejemplo, imprimir en la consola, escribir en un archivo).
- virtual void Shutdown(): Este método está diseñado para realizar operaciones de limpieza cuando el manejador de registro ya no es necesario, normalmente durante la secuencia de apagado del registrador principal o de la aplicación. Las implementaciones podrían utilizar este método para cerrar descriptores de archivo abiertos, liberar recursos asignados o vaciar cualquier salida almacenada en búfer para garantizar que todos los registros se guarden antes de la finalización.
Al definir esta interfaz estándar, el marco de registro logra flexibilidad y extensibilidad. La clase principal CLogger puede gestionar una colección de diferentes objetos ILogHandler, enviando mensajes de registro a cada uno de ellos sin necesidad de conocer los detalles específicos de cómo funciona cada manejador. Se pueden agregar nuevos destinos de salida simplemente creando nuevas clases que implementen la interfaz ILogHandler.
Manejador de registro de consola
Este archivo de cabecera proporciona la clase ConsoleLogHandler, una implementación concreta de la interfaz ILogHandler. Su propósito específico es dirigir los mensajes de registro formateados a la pestaña "Expertos" de la plataforma MetaTrader 5, que actúa como área de salida de la consola durante la ejecución de un Asesor Experto (EA) o un script.
#property strict #include "ILogHandler.mqh" #include "LogLevels.mqh" //+------------------------------------------------------------------+ //| Class: ConsoleLogHandler | //| Description: Implements ILogHandler to output log messages to | //| the MetaTrader 5 Experts tab (console). | //+------------------------------------------------------------------+ class ConsoleLogHandler : public ILogHandler { private: LogLevel m_min_level; // Minimum level to log string m_format; // Log message format string //--- Helper to format the log message string FormatMessage(const datetime time, const LogLevel level, const string origin, const string message); //--- Helper to get string representation of LogLevel string LogLevelToString(const LogLevel level); public: ConsoleLogHandler(const LogLevel min_level = LOG_LEVEL_INFO, const string format = "[{time}] {level}: {origin} - {message}"); ~ConsoleLogHandler(); //--- ILogHandler implementation virtual bool Setup(const string settings="") override; virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) override; virtual void Shutdown() override; //--- Setters void SetMinLevel(const LogLevel level) { m_min_level = level; } void SetFormat(const string format) { m_format = format; } };
La clase ConsoleLogHandler hereda públicamente de ILogHandler, lo que significa que promete proporcionar implementaciones para los métodos Setup, Log y Shutdown definidos en la interfaz. Contiene dos variables miembro privadas: m_min_level de tipo LogLevel almacena el nivel mínimo de gravedad que debe tener un mensaje para ser registrado por este controlador, y m_format de tipo string contiene la plantilla utilizada para formatear el mensaje de salida. También declara métodos auxiliares privados, FormatMessage y LogLevelToString, y métodos públicos para la implementación de la interfaz y métodos de configuración para sus miembros privados.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ ConsoleLogHandler::ConsoleLogHandler(const LogLevel min_level = LOG_LEVEL_INFO, const string format = "[{time}] {level}: {origin} - {message}") { m_min_level = min_level; m_format = format; // No specific setup needed for console logging initially } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ ConsoleLogHandler::~ConsoleLogHandler() { // No specific cleanup needed }
El constructor inicializa un nuevo objeto ConsoleLogHandler. Acepta dos argumentos opcionales: min_level (por defecto LOG_LEVEL_INFO) y format (por defecto una plantilla estándar "[{time}] {level}: {origin} - {message}"). Estos argumentos se utilizan para establecer los valores iniciales de las variables miembro m_min_level y m_format, respectivamente. Esto permite a los usuarios configurar el nivel de filtrado y la apariencia de la salida del manejador al crearlo.
El destructor se encarga de liberar los recursos cuando se destruye un objeto ConsoleLogHandler. En esta implementación específica, no hay recursos asignados dinámicamente ni manejadores abiertos administrados directamente por esta clase, por lo que el cuerpo del destructor está vacío, lo que indica que no se requieren acciones de limpieza especiales para este manejador.
//+------------------------------------------------------------------+ //| Setup | //+------------------------------------------------------------------+ bool ConsoleLogHandler::Setup(const string settings="") { // Settings could be used to parse format or min_level, but we use constructor args for now // Example: Parse settings string if needed return true; } //+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void ConsoleLogHandler::Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) { // Check if the message level meets the minimum requirement if(level >= m_min_level && level < LOG_LEVEL_OFF) { // Format and print the message to the Experts tab Print(FormatMessage(time, level, origin, message)); } } //+------------------------------------------------------------------+ //| Shutdown | //+------------------------------------------------------------------+ void ConsoleLogHandler::Shutdown() { // No specific shutdown actions needed for console logging PrintFormat("%s: ConsoleLogHandler shutdown.", __FUNCTION__); }
- Método de configuración (ConsoleLogHandler::Setup):
Este método implementa la función Setup requerida por la interfaz ILogHandler. Aunque está diseñada para la configuración, la implementación actual no utiliza el parámetro de cadena de configuración, ya que la configuración principal (nivel mínimo y formato) se gestiona a través del constructor. Simplemente devuelve verdadero, lo que indica que el controlador siempre se considera listo para su uso después de su construcción. - Método de registro (ConsoleLogHandler::Log):
Esta es la implementación principal de la acción de registro para la consola. Cuando es llamado por el CLogger principal, primero verifica si el nivel proporcionado del mensaje entrante es mayor o igual que el m_min_level configurado por el manejador y también menor que LOG_LEVEL_OFF. Si el mensaje supera este filtro, el método llama a la función auxiliar privada FormatMessage para crear la cadena de salida final basándose en la plantilla m_format y los detalles de registro proporcionados (hora, nivel, origen, mensaje). Finalmente, utiliza la función de impresión integrada de MQL5 para mostrar la cadena formateada en la pestaña Expertos. - Método de apagado (ConsoleLogHandler::Shutdown):
Este método implementa la función Shutdown de la interfaz. Al igual que el destructor, el registro en la consola no suele requerir acciones de apagado específicas, como cerrar archivos. Esta implementación simplemente imprime un mensaje que indica que el controlador de la consola se está cerrando, proporcionando una confirmación durante la secuencia de finalización de la aplicación.
//+------------------------------------------------------------------+ //| FormatMessage | //+------------------------------------------------------------------+ string ConsoleLogHandler::FormatMessage(const datetime time, const LogLevel level, const string origin, const string message) { string formatted_message = m_format; // Replace placeholders StringReplace(formatted_message, "{time}", TimeToString(time, TIME_DATE | TIME_SECONDS)); StringReplace(formatted_message, "{level}", LogLevelToString(level)); StringReplace(formatted_message, "{origin}", origin); StringReplace(formatted_message, "{message}", message); return formatted_message; } //+------------------------------------------------------------------+ //| LogLevelToString | //+------------------------------------------------------------------+ string ConsoleLogHandler::LogLevelToString(const LogLevel level) { switch(level) { case LOG_LEVEL_DEBUG: return "DEBUG"; case LOG_LEVEL_INFO: return "INFO"; case LOG_LEVEL_WARN: return "WARN"; case LOG_LEVEL_ERROR: return "ERROR"; case LOG_LEVEL_FATAL: return "FATAL"; default: return "UNKNOWN"; } } //+------------------------------------------------------------------+
- Método auxiliar (FormatMessage):
Esta función auxiliar privada toma como entrada los detalles del registro sin procesar (hora, nivel, origen, mensaje) y la cadena de formato del controlador (m_format). Reemplaza los marcadores de posición como {time}, {level}, {origin} y {message} dentro de la cadena de formato con los valores correspondientes reales. Utiliza TimeToString para formatear la marca de tiempo y llama a LogLevelToString para obtener la representación en cadena del nivel de gravedad. La cadena resultante, con el formato completo, se devuelve al método Log para su impresión. - Método auxiliar (LogLevelToString):
Esta función de utilidad privada convierte un valor de enumeración LogLevel en su representación de cadena correspondiente (por ejemplo, LOG_LEVEL_INFO se convierte en "INFO"). Utiliza una instrucción switch para gestionar los niveles de registro definidos y devuelve "UNKNOWN" para cualquier valor inesperado. Esto proporciona indicadores de nivel legibles para el ser humano en la salida de registro formateada. - Métodos de configuración (SetMinLevel, SetFormat): Estos métodos públicos permiten al usuario modificar la configuración del controlador una vez creado. SetMinLevel actualiza la variable miembro m_min_level, cambiando el umbral de filtrado para los mensajes de registro subsiguientes. SetFormat actualiza la variable miembro m_format, modificando la plantilla utilizada para dar formato a los mensajes de registro futuros.
Controlador de registro de archivos
Este archivo de cabecera contiene la clase FileLogHandler, otra implementación concreta de la interfaz ILogHandler. Este controlador está diseñado para el registro persistente, escribiendo mensajes de registro formateados en archivos. Incluye funciones más avanzadas en comparación con el controlador de consola, como la rotación automática de archivos de registro en función de la fecha y el tamaño, y la gestión del número de archivos de registro que se conservan.
#property strict #include "ILogHandler.mqh" #include "LogLevels.mqh" //+------------------------------------------------------------------+ //| Class: FileLogHandler | //| Description: Implements ILogHandler to output log messages to | //| files with rotation capabilities. | //+------------------------------------------------------------------+ class FileLogHandler : public ILogHandler { private: LogLevel m_min_level; // Minimum level to log string m_format; // Log message format string string m_file_path; // Base path for log files string m_file_prefix; // Prefix for log file names int m_file_handle; // Current file handle datetime m_current_day; // Current day for rotation int m_max_size_kb; // Maximum file size in KB before rotation int m_max_files; // Maximum number of log files to keep //--- Helper to format the log message string FormatMessage(const datetime time, const LogLevel level, const string origin, const string message); //--- Helper to get string representation of LogLevel string LogLevelToString(const LogLevel level); //--- Helper to create or rotate log file bool EnsureFileOpen(); //--- Helper to generate file name based on date string GenerateFileName(const datetime time); //--- Helper to perform log rotation void RotateLogFiles(); //--- Helper to check if file size exceeds limit bool IsFileSizeExceeded(); // Add custom helper function to sort string arrays void SortStringArray(string &arr[]); //--- New helper to clean file paths string CleanPath(const string path); public: FileLogHandler(const string file_path="MQL5\\Logs", const string file_prefix="EA_Log", const LogLevel min_level=LOG_LEVEL_INFO, const string format="[{time}] {level}: {origin} - {message}", const int max_size_kb=1024, const int max_files=5); virtual ~FileLogHandler(); //--- ILogHandler implementation virtual bool Setup(const string settings="") override; virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) override; virtual void Shutdown() override; //--- Setters void SetFilePath(const string path) { m_file_path = CleanPath(path); } void SetMinLevel(const LogLevel level) { m_min_level = level; } void SetFormat(const string format) { m_format = format; } void SetFilePrefix(const string prefix){ m_file_prefix = prefix; } void SetMaxSizeKB(const int size) { m_max_size_kb = size; } void SetMaxFiles(const int count) { m_max_files = count; } };
La clase FileLogHandler hereda de ILogHandler. Mantiene varias variables miembro privadas para gestionar su estado y configuración: m_min_level y m_format (similares al controlador de la consola), m_file_path (el directorio donde se almacenan los registros), m_file_prefix (un nombre base para los archivos de registro), m_file_handle (el identificador del archivo de registro actualmente abierto), m_current_day (utilizado para la lógica de rotación diaria), m_max_size_kb (el límite de tamaño en kilobytes para un solo archivo de registro) y m_max_files (el número máximo de archivos de registro que se conservarán).
También declara varios métodos auxiliares privados para formatear, administrar archivos y rotarlos (FormatMessage, LogLevelToString, EnsureFileOpen, GenerateFileName, RotateLogFiles, IsFileSizeExceeded, SortStringArray, CleanPath). Los métodos públicos incluyen el constructor, el destructor, las implementaciones de interfaz y los métodos de configuración (setters).
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ FileLogHandler::FileLogHandler(const string file_path, const string file_prefix, const LogLevel min_level, const string format, const int max_size_kb, const int max_files) { m_min_level = min_level; m_format = format; m_file_path = CleanPath(file_path); m_file_prefix = file_prefix; m_file_handle = INVALID_HANDLE; m_current_day = 0; m_max_size_kb = max_size_kb; m_max_files = max_files; // Create directory if it doesn't exist if(!FolderCreate(m_file_path)) { if(GetLastError() != 0) Print("FileLogHandler: Failed to create directory: ", m_file_path, ", error: ", GetLastError()); } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ FileLogHandler::~FileLogHandler() { Shutdown(); }
- Constructor (FileLogHandler::FileLogHandler):
El constructor inicializa el FileLogHandler. Recibe argumentos para la ruta del archivo, el prefijo, el nivel mínimo de registro, la cadena de formato, el tamaño máximo del archivo y el número máximo de archivos, y establece las variables miembro correspondientes. Utiliza la función auxiliar CleanPath para garantizar que la ruta del archivo utilice los separadores de directorio correctos. Es fundamental destacar que también intenta crear el directorio de registro especificado (m_file_path, relativo a la ruta de datos del terminal) mediante FolderCreate si aún no existe, garantizando así que el controlador disponga de un lugar donde guardar los archivos. - Destructor (FileLogHandler::~FileLogHandler):
El destructor garantiza una limpieza adecuada mediante la llamada al método Shutdown. Esto garantiza que el identificador del archivo de registro actualmente abierto se cierre cuando se destruya el objeto FileLogHandler, lo que evita fugas de recursos.
//+------------------------------------------------------------------+ //| Setup | //+------------------------------------------------------------------+ bool FileLogHandler::Setup(const string settings) { // Parse settings if provided // Format could be: "path=MQL5/Logs;prefix=MyEA;min_level=INFO;max_size=2048;max_files=10" if(settings != "") { string parts[]; int count = StringSplit(settings, ';', parts); for(int i = 0; i < count; i++) { string key_value[]; if(StringSplit(parts[i], '=', key_value) == 2) { string key = key_value[0]; StringTrimLeft(key); StringTrimRight(key); string value = key_value[1]; StringTrimLeft(value); StringTrimRight(value); if(key == "path") m_file_path = CleanPath(value); else if(key == "prefix") m_file_prefix = value; else if(key == "min_level") { if(value == "DEBUG") m_min_level = LOG_LEVEL_DEBUG; else if(value == "INFO") m_min_level = LOG_LEVEL_INFO; else if(value == "WARN") m_min_level = LOG_LEVEL_WARN; else if(value == "ERROR") m_min_level = LOG_LEVEL_ERROR; else if(value == "FATAL") m_min_level = LOG_LEVEL_FATAL; } else if(key == "max_size") m_max_size_kb = (int)StringToInteger(value); else if(key == "max_files") m_max_files = (int)StringToInteger(value); } } } return true; } //+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void FileLogHandler::Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) { // Check if the message level meets the minimum requirement if(level >= m_min_level && level < LOG_LEVEL_OFF) { // Ensure file is open and ready for writing if(EnsureFileOpen()) { // Format the message string formatted_message = FormatMessage(time, level, origin, message); // Write to file FileWriteString(m_file_handle, formatted_message + "\r\n"); // Flush to ensure data is written immediately FileFlush(m_file_handle); // Check if rotation is needed if(IsFileSizeExceeded()) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; RotateLogFiles(); EnsureFileOpen(); } } } } //+------------------------------------------------------------------+ //| Shutdown | //+------------------------------------------------------------------+ void FileLogHandler::Shutdown() { if(m_file_handle != INVALID_HANDLE) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; } }
- Método de configuración (FileLogHandler::Setup):
Este método implementa la función Setup de la interfaz. Proporciona una forma alternativa de configurar el controlador después de su creación utilizando una única cadena de configuración (por ejemplo, "path=MQL5/Logs;prefix=MyEA;max_size=2048"). Analiza esta cadena, la divide en pares clave-valor y actualiza las variables miembro correspondientes, como m_file_path, m_file_prefix, m_min_level, m_max_size_kb y m_max_files. Esto permite cargar la configuración desde fuentes externas si es necesario. Devuelve verdadero después del análisis. - Método Log (FileLogHandler::Log):
Este método gestiona la lógica de registro de archivos principales. Primero comprueba si el nivel del mensaje cumple con el requisito m_min_level. Si lo hace, llama a EnsureFileOpen para asegurarse de que se haya abierto un archivo de registro válido (gestionando la rotación diaria si es necesario). Si el archivo se abre correctamente, formatea el mensaje usando FormatMessage y escribe la cadena formateada seguida de un salto de línea (\r\n) en el archivo usando FileWriteString. A continuación, llama a FileFlush para garantizar que los datos se escriban inmediatamente en el disco, lo cual es importante para capturar registros incluso si la aplicación falla. Finalmente, comprueba si el tamaño del archivo actual supera el límite m_max_size_kb mediante IsFileSizeExceeded. Si se supera el límite, se cierra el archivo actual, se activa RotateLogFiles para gestionar los archivos antiguos y se vuelve a abrir un nuevo archivo mediante EnsureFileOpen. - Método de cierre (FileLogHandler::Shutdown):
Este método implementa el requisito de apagado de la interfaz. Su responsabilidad principal es cerrar el identificador de archivo de registro actualmente abierto (m_file_handle) usando FileClose si es válido (!= INVALID_HANDLE). Esto garantiza que el archivo se cierre correctamente y que todos los datos almacenados en búfer se escriban cuando se apague el registrador.
//+------------------------------------------------------------------+ //| FormatMessage | //+------------------------------------------------------------------+ string FileLogHandler::FormatMessage(const datetime time, const LogLevel level, const string origin, const string message) { string formatted_message = m_format; // Replace placeholders StringReplace(formatted_message, "{time}", TimeToString(time, TIME_DATE | TIME_SECONDS)); StringReplace(formatted_message, "{level}", LogLevelToString(level)); StringReplace(formatted_message, "{origin}", origin); StringReplace(formatted_message, "{message}", message); return formatted_message; } //+------------------------------------------------------------------+ //| LogLevelToString | //+------------------------------------------------------------------+ string FileLogHandler::LogLevelToString(const LogLevel level) { switch(level) { case LOG_LEVEL_DEBUG: return "DEBUG"; case LOG_LEVEL_INFO: return "INFO"; case LOG_LEVEL_WARN: return "WARN"; case LOG_LEVEL_ERROR: return "ERROR"; case LOG_LEVEL_FATAL: return "FATAL"; default: return "UNKNOWN"; } }
Métodos auxiliares (FormatMessage, LogLevelToString): Estos métodos auxiliares privados funcionan de forma idéntica a sus homólogos en el ConsoleLogHandler, ya que permiten formatear mensajes basándose en la cadena m_format y convierten las enumeraciones LogLevel en cadenas legibles.
//+------------------------------------------------------------------+ //| EnsureFileOpen | //+------------------------------------------------------------------+ bool FileLogHandler::EnsureFileOpen() { datetime current_time = TimeCurrent(); MqlDateTime time_struct; TimeToStruct(current_time, time_struct); // Create a datetime that represents just the current day (time set to 00:00:00) MqlDateTime day_struct; day_struct.year = time_struct.year; day_struct.mon = time_struct.mon; day_struct.day = time_struct.day; day_struct.hour = 0; day_struct.min = 0; day_struct.sec = 0; datetime current_day = StructToTime(day_struct); // Check if we need to open a new file (either first time or new day) if(m_file_handle == INVALID_HANDLE || m_current_day != current_day) { // Close existing file if open if(m_file_handle != INVALID_HANDLE) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; } // Update current day m_current_day = current_day; // Generate new file name string file_name = GenerateFileName(current_time); // Open file for writing (append if exists) m_file_handle = FileOpen(file_name, FILE_WRITE | FILE_READ | FILE_TXT); if(m_file_handle == INVALID_HANDLE) { Print("FileLogHandler: Failed to open log file: ", file_name, ", error: ", GetLastError()); return false; } // Move to end of file for appending FileSeek(m_file_handle, 0, SEEK_END); } return true; } //+------------------------------------------------------------------+ //| GenerateFileName | //+------------------------------------------------------------------+ string FileLogHandler::GenerateFileName(const datetime time) { MqlDateTime time_struct; TimeToStruct(time, time_struct); string date_str = StringFormat("%04d%02d%02d", time_struct.year, time_struct.mon, time_struct.day); return m_file_path + "\\" + m_file_prefix + "_" + date_str + ".log"; } //+------------------------------------------------------------------+ //| IsFileSizeExceeded | //+------------------------------------------------------------------+ bool FileLogHandler::IsFileSizeExceeded() { if(m_file_handle != INVALID_HANDLE) { // Get current position (file size) ulong size = FileSize(m_file_handle); // Check if size exceeds limit (convert KB to bytes) return (size > (ulong)m_max_size_kb * 1024); } return false; }
- Método auxiliar (EnsureFileOpen):
Este método auxiliar fundamental gestiona la apertura y la rotación diaria de los archivos de registro. Compara la fecha actual (derivada de TimeCurrent()) con el valor almacenado m_current_day. Si el identificador de archivo no es válido o ha cambiado el día, cierra cualquier identificador existente, actualiza m_current_day, genera un nuevo nombre de archivo usando GenerateFileName (que incluye la fecha) y abre este nuevo archivo en modo de escritura/lectura (FILE_WRITE | FILE_READ | FILE_TXT). Utiliza FileSeek para desplazarse hasta el final del archivo, asegurando que se añadan nuevos registros. Devuelve verdadero si un archivo se abre correctamente o ya está abierto, y falso en caso de error. - Método auxiliar (GenerateFileName):
Esta utilidad genera la ruta completa de un archivo de registro en función de la hora actual. Formatea la parte de la fecha de la hora en una cadena AAAA MM DD y la combina con la ruta de archivo configurada (m_file_path), el prefijo de archivo (m_file_prefix) y la extensión .log. - Método auxiliar (IsFileSizeExceeded):
Esta función comprueba si el tamaño del archivo de registro actualmente abierto (m_file_handle) ha superado el límite configurado m_max_size_kb. Recupera el tamaño del archivo usando FileSize y lo compara con el límite (convertido a bytes). Devuelve verdadero si se supera el tamaño, falso en caso contrario.
//+------------------------------------------------------------------+ //| RotateLogFiles | //+------------------------------------------------------------------+ void FileLogHandler::RotateLogFiles() { // Get list of log files string terminal_path = TerminalInfoString(TERMINAL_DATA_PATH); string full_path = terminal_path + "\\" + m_file_path; string file_pattern = m_file_prefix + "_*.log"; string files[]; int file_count = 0; long search_handle = FileFindFirst(full_path + "\\" + file_pattern, files[file_count]); if(search_handle != INVALID_HANDLE) { file_count++; // Find all matching files while(FileFindNext(search_handle, files[file_count])) { file_count++; ArrayResize(files, file_count + 1); } // Close search handle FileFindClose(search_handle); } // Resize array to actual number of found files before sorting ArrayResize(files, file_count); // Sort the string array using the custom sorter SortStringArray(files); // Delete oldest files if we have too many int files_to_delete = file_count - m_max_files + 1; // +1 for the new file we'll create if(files_to_delete > 0) { for(int i = 0; i < files_to_delete; i++) { if(!FileDelete(m_file_path + "\\" + files[i])) Print("FileLogHandler: Failed to delete old log file: ", files[i], ", error: ", GetLastError()); } } } //+------------------------------------------------------------------+ //| SortStringArray | //+------------------------------------------------------------------+ void FileLogHandler::SortStringArray(string &arr[]) { int n = ArraySize(arr); for(int i = 0; i < n - 1; i++) { for(int j = i + 1; j < n; j++) { if(arr[i] > arr[j]) { string temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } } //+------------------------------------------------------------------+ //| New implementation: CleanPath | //+------------------------------------------------------------------+ string FileLogHandler::CleanPath(const string path) { string result = path; // Replace all "/" with "\\" StringReplace(result, "/", "\\"); return result; } //+------------------------------------------------------------------+
- Método auxiliar (RotateLogFiles):
Este método implementa la política de retención de archivos de registro. Encuentra todos los archivos en el directorio de registro que coincidan con el patrón (m_file_prefix_*.log) utilizando FileFindFirst y FileFindNext. Almacena los nombres de archivo en una matriz de cadenas y los ordena alfabéticamente (lo que generalmente corresponde al orden cronológico debido al formato de fecha en el nombre del archivo) utilizando la función auxiliar SortStringArray. A continuación, calcula cuántos archivos superan el límite m_max_files y elimina los más antiguos (los que aparecen primero en la lista ordenada) mediante FileDelete. - Método auxiliar (SortStringArray):
Esta es una implementación sencilla del algoritmo de ordenación de burbuja, específicamente para ordenar la matriz de nombres de archivos de registro obtenida en RotateLogFiles. Se utiliza porque la biblioteca estándar de MQL5 carece de una función de ordenación integrada para matrices de cadenas. - Método auxiliar (CleanPath):
Esta utilidad garantiza que las rutas de los directorios utilicen el separador de barra invertida (\) que esperan las funciones de archivos de MQL5, sustituyendo cualquier barra (/) que se encuentre en la cadena de la ruta de entrada. - Métodos de configuración (SetFilePath, SetMinLevel, etc.):
Estos métodos públicos permiten modificar los parámetros de configuración del controlador (ruta, prefijo, nivel, formato, límites de tamaño) después de su creación inicial, lo que proporciona flexibilidad.
CLogger
Este archivo de cabecera define la clase CLogger, que actúa como el orquestador central de todo el sistema de registro de eventos. Se implementa utilizando el patrón de diseño Singleton, lo que garantiza que solo exista una instancia del registrador en toda la aplicación. Esta instancia única gestiona todos los controladores de registro registrados y proporciona la interfaz principal para que el código del usuario envíe mensajes de registro.#property strict #include "LogLevels.mqh" #include "ILogHandler.mqh" //+------------------------------------------------------------------+ //| Class: CLogger | //| Description: Singleton class for managing and dispatching log | //| messages to registered handlers. | //+------------------------------------------------------------------+ class CLogger { private: static CLogger *s_instance; ILogHandler* m_handlers[]; LogLevel m_global_min_level; long m_expert_magic; string m_expert_name; //--- Private constructor for Singleton CLogger(); ~CLogger(); public: //--- Get the singleton instance static CLogger* Instance(); //--- Cleanup the singleton instance static void Release(); //--- Handler management bool AddHandler(ILogHandler *handler); void ClearHandlers(); //--- Configuration void SetGlobalMinLevel(const LogLevel level); void SetExpertInfo(const long magic, const string name); //--- Logging methods void Log(const LogLevel level, const string origin, const string message); void Debug(const string origin, const string message); void Info(const string origin, const string message); void Warn(const string origin, const string message); void Error(const string origin, const string message); void Fatal(const string origin, const string message); //--- Formatted logging methods void LogFormat(const LogLevel level, const string origin, const string formatted_message); void DebugFormat(const string origin, const string formatted_message); void InfoFormat(const string origin, const string formatted_message); void WarnFormat(const string origin, const string formatted_message); void ErrorFormat(const string origin, const string formatted_message); void FatalFormat(const string origin, const string formatted_message); };
La clase CLogger contiene varios miembros privados. s_instance es un puntero estático para almacenar la única instancia de la clase en sí. m_handlers es una matriz dinámica de punteros ILogHandler, que almacena referencias a todos los manejadores de registro activos (como los manejadores de consola o de archivos). m_global_min_level establece un umbral de filtrado global; los mensajes por debajo de este nivel se ignoran incluso antes de ser enviados a los manejadores individuales. Las variables m_expert_magic y m_expert_name almacenan información opcional sobre el Asesor Experto mediante el registrador, que puede incluirse en los mensajes de registro para un mejor contexto.
El constructor y el destructor son privados para garantizar el cumplimiento del patrón Singleton. Los métodos públicos proporcionan acceso a la instancia, la gestión del controlador, la configuración y diversas funciones de registro.
//+------------------------------------------------------------------+ //| Static instance initialization | //+------------------------------------------------------------------+ CLogger *CLogger::s_instance = NULL; //+------------------------------------------------------------------+ //| Get Singleton Instance | //+------------------------------------------------------------------+ CLogger* CLogger::Instance() { if(s_instance == NULL) { s_instance = new CLogger(); } return s_instance; } //+------------------------------------------------------------------+ //| Release Singleton Instance | //+------------------------------------------------------------------+ void CLogger::Release() { if(s_instance != NULL) { delete s_instance; s_instance = NULL; } } //+------------------------------------------------------------------+ //| Constructor (Private) | //+------------------------------------------------------------------+ CLogger::CLogger() { m_global_min_level = LOG_LEVEL_DEBUG; m_expert_magic = 0; m_expert_name = ""; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogger::~CLogger() { ClearHandlers(); }
- Implementación de Singleton (Instance, Release, CLogger):
El patrón Singleton se implementa mediante el método estático Instance(), que crea el objeto CLogger en su primera llamada y devuelve la misma instancia en las llamadas posteriores. El constructor (CLogger::CLogger) es privado, lo que impide la instanciación directa desde fuera de la clase; inicializa los valores predeterminados para el nivel mínimo global y la información del experto. El método estático Release() se proporciona para eliminar explícitamente la instancia singleton y liberar recursos, y normalmente se llama durante el cierre de la aplicación. - Destructor (CLogger::~CLogger):
El destructor se llama cuando la instancia singleton se elimina mediante el método Release(). Su principal responsabilidad es limpiar los manejadores administrados llamando al método ClearHandlers, asegurándose de que se llame al método Shutdown de cada manejador y de que se eliminen los propios objetos manejadores.
//+------------------------------------------------------------------+ //| AddHandler | //+------------------------------------------------------------------+ bool CLogger::AddHandler(ILogHandler *handler) { if(CheckPointer(handler) == POINTER_INVALID) { Print("CLogger::AddHandler - Error: Invalid handler pointer."); return false; } int size = ArraySize(m_handlers); ArrayResize(m_handlers, size + 1); m_handlers[size] = handler; return true; } //+------------------------------------------------------------------+ //| ClearHandlers | //+------------------------------------------------------------------+ void CLogger::ClearHandlers() { for(int i = 0; i < ArraySize(m_handlers); i++) { ILogHandler *handler = m_handlers[i]; if(CheckPointer(handler) != POINTER_INVALID) { handler.Shutdown(); delete handler; } } ArrayResize(m_handlers, 0); } //+------------------------------------------------------------------+ //| SetGlobalMinLevel | //+------------------------------------------------------------------+ void CLogger::SetGlobalMinLevel(const LogLevel level) { m_global_min_level = level; } //+------------------------------------------------------------------+ //| SetExpertInfo | //+------------------------------------------------------------------+ void CLogger::SetExpertInfo(const long magic, const string name) { m_expert_magic = magic; m_expert_name = name; }
- Gestión de controladores (AddHandler, ClearHandlers):
El método AddHandler permite añadir un nuevo controlador de registro (cualquier objeto que implemente ILogHandler) a la lista interna del registrador (m_handlers). Comprueba si el puntero es válido, redimensiona el array dinámico y añade el controlador. El método ClearHandlers itera a través del array m_handlers, llama al método Shutdown en cada controlador válido, elimina el objeto controlador en sí (suponiendo que el registrador toma posesión) y, finalmente, borra el array. Esto es fundamental para una correcta liberación de recursos. - Configuración (SetGlobalMinLevel, SetExpertInfo):
Estos métodos permiten personalizar el comportamiento del registrador. SetGlobalMinLevel ajusta el umbral de filtrado global (m_global_min_level), afectando a todos los mensajes antes de que lleguen a los manejadores. SetExpertInfo permite configurar el número mágico y el nombre del EA, que luego los manejadores pueden incluir automáticamente en los mensajes de registro para una mejor identificación, especialmente cuando varios EA pueden estar registrando información simultáneamente.
//+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void CLogger::Log(const LogLevel level, const string origin, const string message) { // Check global level first if(level < m_global_min_level || level >= LOG_LEVEL_OFF) return; datetime current_time = TimeCurrent(); string effective_origin = origin; if(m_expert_name != "") effective_origin = m_expert_name + "::" + origin; // Dispatch to all registered handlers for(int i = 0; i < ArraySize(m_handlers); i++) { ILogHandler *handler = m_handlers[i]; if(CheckPointer(handler) != POINTER_INVALID) { handler.Log(current_time, level, effective_origin, message, m_expert_magic); } } } //+------------------------------------------------------------------+ //| Convenience Logging Methods | //+------------------------------------------------------------------+ void CLogger::Debug(const string origin, const string message) { Log(LOG_LEVEL_DEBUG, origin, message); } void CLogger::Info(const string origin, const string message) { Log(LOG_LEVEL_INFO, origin, message); } void CLogger::Warn(const string origin, const string message) { Log(LOG_LEVEL_WARN, origin, message); } void CLogger::Error(const string origin, const string message) { Log(LOG_LEVEL_ERROR, origin, message); } void CLogger::Fatal(const string origin, const string message) { Log(LOG_LEVEL_FATAL, origin, message); } //+------------------------------------------------------------------+ //| LogFormat | //+------------------------------------------------------------------+ void CLogger::LogFormat(const LogLevel level, const string origin, const string formatted_message) { // Check global level first if(level < m_global_min_level || level >= LOG_LEVEL_OFF) return; Log(level, origin, formatted_message); } //+------------------------------------------------------------------+ //| Convenience Formatted Logging Methods | //+------------------------------------------------------------------+ void CLogger::DebugFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_DEBUG, origin, formatted_message); } void CLogger::InfoFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_INFO, origin, formatted_message); } void CLogger::WarnFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_WARN, origin, formatted_message); } void CLogger::ErrorFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_ERROR, origin, formatted_message); } void CLogger::FatalFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_FATAL, origin, formatted_message); } //+------------------------------------------------------------------+
- Método de registro principal (Log):
Este es el método principal que recibe las solicitudes de registro. En primer lugar, comprueba si el nivel del mensaje cumple con el valor m_global_min_level. Si la prueba es exitosa, recupera la hora actual y construye una cadena effective_origin, posiblemente anteponiendo el nombre del experto configurado (m_expert_name). A continuación, recorre el array m_handlers y llama al método Log de cada controlador válido, pasándole la marca de tiempo, el nivel, el origen, el mensaje y el número mágico del experto. Esto envía eficazmente el mensaje de registro a todos los destinos de salida activos. - Métodos auxiliares de registro (Debug, Info, Warn, Error, Fatal):
Estos métodos públicos proporcionan una interfaz más sencilla para registrar mensajes en niveles de gravedad específicos. Cada método (por ejemplo, Debug, Info) se limita a llamar al método Log principal con el valor correspondiente de la enumeración LogLevel (LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, etc.), lo que reduce la cantidad de código necesario en la aplicación del usuario para registrar un mensaje. - Métodos de registro formateado (LogFormat, DebugFormat, etc.):
Estos métodos ofrecen una forma alternativa de registrar mensajes que ya están formateados. LogFormat toma una cadena de mensaje preformateada y llama al método principal Log. Los métodos de conveniencia como DebugFormat, InfoFormat, etc., simplemente llaman a LogFormat con el nivel de gravedad apropiado. Estas opciones resultan útiles si la lógica de formato de mensajes es compleja y se gestiona en otro lugar antes de llamar al registrador.
Una vez finalizada la implementación de CLogger, es hora de verlo en acción.
Uso del marco de registro
Este EA sirve como demostración práctica de cómo integrar y utilizar el marco de registro MQL5 personalizado (que comprende CLogger, ILogHandler, ConsoleLogHandler y FileLogHandler). Muestra la configuración, el uso durante el funcionamiento y la limpieza de los componentes de registro dentro de una estructura EA estándar.
La sección inicial de LoggingExampleEA.mq5 configura las propiedades estándar del Asesor Experto e incluye los componentes necesarios del marco de registro personalizado.
Tras definir las propiedades, las declaraciones #include son cruciales para integrar la funcionalidad de registro. CLogger.mqh incluye la definición de la clase principal del registrador. ConsoleLogHandler.mqh incluye la clase para registrar información en la consola de MetaTrader (pestaña Expertos). El archivo FileLogHandler.mqh incluye la clase responsable de registrar la información en archivos. Esto incluye hacer que las clases y funciones definidas dentro de esos archivos de encabezado estén disponibles para su uso dentro de esta EA.
Parámetros de entrada (input):
// Input parameters input int MagicNumber = 654321; // EA Magic Number input double LotSize = 0.01; // Fixed lot size input int StopLossPips = 50; // Stop Loss in pips input int TakeProfitPips = 100; // Take Profit in pips input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO; // Minimum level for console output input LogLevel FileLogLevel = LOG_LEVEL_DEBUG; // Minimum level for file output
Esta sección define los parámetros externos que los usuarios pueden configurar al adjuntar el Asesor Experto a un gráfico. Estos parámetros permiten personalizar el comportamiento de negociación del EA y, lo que es más importante, sus ajustes de registro.
- input int MagicNumber = 654321; : Este es un parámetro estándar de EA utilizado para identificar las órdenes colocadas por esta instancia específica del EA. Esto ayuda a distinguir sus operaciones de las de otros asesores expertos o de las operaciones manuales.
- input double LotSize = 0.01; : Define el volumen de negociación fijo (tamaño del lote) que se utilizará para las órdenes colocadas por el EA.
- input int StopLossPips = 50; : Establece la distancia de stop loss en pips para las órdenes.
- input int TakeProfitPips = 100; : Establece la distancia de toma de ganancias en pips para las órdenes.
- input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO; : Este parámetro permite al usuario seleccionar el nivel mínimo de gravedad para los mensajes que deben mostrarse en la pestaña MetaTrader Experts (consola). Utiliza el tipo de enumeración LogLevel definido en LogLevels.mqh. Por defecto, está configurado en LOG_LEVEL_INFO, lo que significa que los mensajes INFO, WARN, ERROR y FATAL se mostrarán en la consola, mientras que los mensajes DEBUG se suprimirán.
- input LogLevel FileLogLevel = LOG_LEVEL_DEBUG; : De manera similar, esta entrada establece el nivel mínimo de gravedad para los mensajes escritos en el archivo de registro. También utiliza la enumeración LogLevel. El valor predeterminado es LOG_LEVEL_DEBUG, lo que indica que todos los mensajes, incluida la información de depuración detallada, se guardarán en el archivo de registro. Esto permite obtener una salida de consola menos detallada durante el funcionamiento normal, al tiempo que se conservan registros detallados para su posterior análisis o resolución de problemas.
Estas entradas específicas para el registro demuestran cómo el marco se puede configurar fácilmente de forma externa, lo que permite a los usuarios ajustar el nivel de detalle del registro sin modificar el código de EA.
// Global logger pointer (optional, can use CLogger::Instance() directly) CLogger *g_logger = NULL;
El EA declara una única variable global: CLogger *g_logger = NULL; : Esta línea declara un puntero llamado g_logger que puede apuntar a un objeto de la clase CLogger. Se inicializa a NULL, lo que significa que inicialmente no apunta a ningún objeto válido. Este puntero global está destinado a contener la única instancia de CLogger obtenida a través del patrón singleton (CLogger::Instance()).
Si bien es posible usar el método estático CLogger::Instance() directamente donde sea necesario para el registro, almacenar la instancia en esta variable global después de recuperarla en OnInit() proporciona una forma conveniente de acceder al objeto del registrador desde diferentes funciones (OnTick, OnDeinit, OnChartEvent) sin llamar repetidamente a CLogger::Instance(). Actúa como un puntero en caché al registrador singleton.
OnInit():
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Get the logger instance g_logger = CLogger::Instance(); if(CheckPointer(g_logger) == POINTER_INVALID) { Print("Critical Error: Failed to get Logger instance!"); return(INIT_FAILED); } //--- Set EA information for context in logs g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME)); //--- Configure Handlers --- // 1. Console Handler ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel); if(CheckPointer(console_handler) != POINTER_INVALID) { // Optionally customize format // console_handler.SetFormat("[{level}] {message}"); if(!g_logger.AddHandler(console_handler)) { Print("Warning: Failed to add ConsoleLogHandler."); delete console_handler; // Clean up if not added } } else { Print("Warning: Failed to create ConsoleLogHandler."); } // 2. File Handler string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber); FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", // Directory relative to MQL5/Files log_prefix, // File name prefix FileLogLevel, // Minimum level to log to file "[{time}] {level} ({origin}): {message}", // Format 2048, // Max file size in KB (e.g., 2MB) 10); // Max number of log files to keep if(CheckPointer(file_handler) != POINTER_INVALID) { if(!g_logger.AddHandler(file_handler)) { Print("Warning: Failed to add FileLogHandler."); delete file_handler; // Clean up if not added } } else { Print("Warning: Failed to create FileLogHandler."); } //--- Log initialization message g_logger.Info(__FUNCTION__, "Expert Advisor initialized successfully."); g_logger.Debug(__FUNCTION__, StringFormat("Settings: Lots=%.2f, SL=%d, TP=%d, ConsoleLevel=%s, FileLevel=%s", LotSize, StopLossPips, TakeProfitPips, EnumToString(ConsoleLogLevel), EnumToString(FileLogLevel))); //--- succeed return(INIT_SUCCEEDED); }
En este ejemplo, OnInit() es crucial para configurar el marco de registro personalizado. El primer paso dentro de OnInit es recuperar la instancia única del registrador:
g_logger = CLogger::Instance();
Este método estático garantiza que solo exista un objeto CLogger. El puntero devuelto se almacena en la variable global g_logger para facilitar su acceso posterior. A continuación, se realiza una comprobación básica de errores mediante CheckPointer para garantizar que la instancia se haya obtenido correctamente; de lo contrario, se imprime un error crítico en el registro estándar y falla la inicialización (INIT_FAILED).
g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME));
Esta línea configura el registrador con el contexto del EA que lo utiliza. Pasa el MagicNumber (de los parámetros de entrada) y el nombre del EA (obtenido mediante MQL5InfoString(MQL5_PROGRAM_NAME)). Esta información puede incluirse automáticamente en los mensajes de registro por parte de los gestores (dependiendo de su cadena de formato), lo que facilita la identificación de los registros de EA específicos, especialmente si se están ejecutando varios EA.
Un ConsoleLogHandler se crea dinámicamente mediante `new`:
ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel);
Se configura directamente en el constructor con el nivel de registro mínimo especificado por el parámetro de entrada ConsoleLogLevel. El código incluye un ejemplo comentado (console_handler.SetFormat("[{level}] {message}");) que muestra cómo se podría personalizar el formato de salida después de la creación, si fuera necesario. A continuación, se añade el controlador al registrador principal: if(!g_logger.AddHandler(console_handler))
Si falla la adición del controlador (devuelve false), se imprime una advertencia y el objeto controlador creado se elimina mediante la función delete para evitar fugas de memoria. También se incluye una comprobación de errores en la creación inicial (new) del controlador.
De forma similar, se crea un FileLogHandler:// 2. File Handler string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber); FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", // Directory relative to MQL5/Files log_prefix, // File name prefix FileLogLevel, // Minimum level to log to file "[{time}] {level} ({origin}): {message}", // Format 2048, // Max file size in KB (e.g., 2MB) 10); // Max number of log files to keep
Se crea un prefijo de archivo de registro utilizando el nombre de EA y un número mágico para una identificación única. El constructor de FileLogHandler se invoca con varios argumentos: la ruta del directorio («MQL5/Logs/EA_Logs», relativa al directorio MQL5/Files del terminal), el prefijo generado, el nivel mínimo indicado en el parámetro FileLogLevel, una cadena de formato personalizada, el tamaño máximo del archivo en KB (2048 KB = 2 MB) y el número máximo de archivos de registro que se conservarán (10). Al igual que el manejador de consola, se agrega al registrador mediante g_logger.AddHandler(), con un manejo de errores y una limpieza (eliminación) similares si la creación o la adición fallan.
Una vez configurados los controladores, el EA registra mensajes para confirmar la inicialización:g_logger.Info(__FUNCTION__, \"Expert Advisor initialized successfully.\"); g_logger.Debug(__FUNCTION__, StringFormat(\"Settings: ...\"));
Un mensaje de nivel «Info» confirma que la operación se ha realizado correctamente. Un mensaje de nivel de depuración registra los parámetros de entrada clave utilizando StringFormat. __FUNCTION__ se utiliza como cadena de origen, proporcionando automáticamente el nombre de la función actual (OnInit). Estos mensajes serán procesados por los controladores añadidos en función de los niveles mínimos que tengan configurados.
Por último, si todas las inicializaciones se completan con éxito, la función devuelve INIT_SUCCEEDED, lo que indica al terminal que el EA está listo para comenzar a procesar los ticks. Si se produce algún error crítico (como no poder obtener la instancia del registrador), devuelve INIT_FAILED.
OnDeinit():
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Log deinitialization if(CheckPointer(g_logger) != POINTER_INVALID) { string reason_str = "Unknown reason"; switch(reason) { case REASON_ACCOUNT: reason_str = "Account change"; break; case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; break; case REASON_CHARTCLOSE: reason_str = "Chart closed"; break; case REASON_PARAMETERS: reason_str = "Input parameters changed"; break; case REASON_RECOMPILE: reason_str = "Recompiled"; break; case REASON_REMOVE: reason_str = "EA removed from chart"; break; case REASON_TEMPLATE: reason_str = "Template applied"; break; case REASON_CLOSE: reason_str = "Terminal closed"; break; } g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")"); // Release the logger instance (this calls Shutdown() on all handlers) CLogger::Release(); g_logger = NULL; // Set pointer to NULL after release } else { Print("Logger instance was already invalid during Deinit."); } //--- Print to standard log just in case logger failed Print(MQL5InfoString(MQL5_PROGRAM_NAME) + ": Deinitialized. Reason code: " + IntegerToString(reason)); }
En LoggingExampleEA.mq5, la función OnDeinit se encarga de cerrar correctamente el marco de registro:
if(CheckPointer(g_logger) != POINTER_INVALID)
La función comprueba en primer lugar si el puntero global del registrador, g_logger, sigue siendo válido. Esto evita errores si se llama a OnDeinit después de que el registrador ya se haya liberado o si la inicialización falló.
Dentro del bloque if, el código determina una cadena legible para humanos que corresponde al código de motivo pasado a OnDeinit mediante una instrucción switch. Esto proporciona contexto sobre por qué la EA se detiene. A continuación, se registra un mensaje informativo mediante g_logger.Info(), que incluye la cadena de texto con el motivo determinado y el código del motivo original.
string reason_str = "Unknown reason"; switch(reason) { case REASON_ACCOUNT: reason_str = "Account change"; break; case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; break; ... ... case REASON_CLOSE: reason_str = "Terminal closed"; break; } g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")");
De este modo se garantiza que los momentos finales del funcionamiento de la EA, incluido el motivo de la detención, queden registrados en los registros (tanto en la consola como en el archivo, en función de los niveles configurados).
Este es el paso más importante para la limpieza del registro:
CLogger::Release();
Al invocar el método estático Release() de la clase CLogger, se activa la eliminación de la instancia del registrador de tipo singleton. Como parte de su proceso de destrucción, el destructor de CLogger recorre todos los manejadores añadidos (en este caso, los manejadores de consola y de archivo), llama a sus respectivos métodos Shutdown() (lo que, en el caso del FileLogHandler, implica cerrar el archivo de registro abierto) y, a continuación, elimina los propios objetos de los controladores. Esto garantiza que todos los recursos se liberen adecuadamente y que los archivos se cierren correctamente.
Anular el puntero global:
g_logger = NULL;
Tras liberar la instancia, el puntero global g_logger se restablece explícitamente a NULL. Es una buena práctica indicar que el puntero ya no apunta a un objeto válido.
Un bloque else maneja el caso en el que g_logger ya no era válido cuando se llamó a OnDeinit, imprimiendo un mensaje en el registro estándar de Experts. Además, una instrucción Print final fuera de la lógica del registrador garantiza que siempre se registre un mensaje de desinicialización en el registro estándar, incluso si el registrador personalizado falló por completo.
Esta implementación demuestra el procedimiento correcto para apagar el marco de registro personalizado, asegurando que los archivos de registro se cierren correctamente y que los recursos se liberen cuando finalice el EA.
OnTick():
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) { // Attempt to re-initialize logger if it became invalid unexpectedly // This is defensive coding, ideally it shouldn't happen if OnInit succeeded. Print("Error: Logger instance invalid in OnTick! Attempting re-init..."); if(OnInit() != INIT_SUCCEEDED) { Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA."); ExpertRemove(); // Stop the EA return; } } //--- Log tick arrival MqlTick latest_tick; if(SymbolInfoTick(_Symbol, latest_tick)) { g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d", TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS), latest_tick.bid, latest_tick.ask, (int)latest_tick.volume_real)); } else { g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError())); } //--- Example Logic: Check for a simple crossover // Note: Use more robust indicator handling in a real EA double ma_fast[], ma_slow[]; int copied_fast = CopyBuffer(iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_fast); int copied_slow = CopyBuffer(iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_slow); if(copied_fast < 3 || copied_slow < 3) { g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data."); return; // Not enough data yet } // ArraySetAsSeries might be needed depending on how you access indices // ArraySetAsSeries(ma_fast, true); // ArraySetAsSeries(ma_slow, true); bool cross_up = ma_fast[1] > ma_slow[1] && ma_fast[2] <= ma_slow[2]; bool cross_down = ma_fast[1] < ma_slow[1] && ma_fast[2] >= ma_slow[2]; if(cross_up) { g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal."); // --- Add trading logic here --- // Example: SendBuyOrder(); } else if(cross_down) { g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal."); // --- Add trading logic here --- // Example: SendSellOrder(); } // Log account info periodically static datetime last_account_log = 0; if(TimeCurrent() - last_account_log >= 3600) // Log every hour { g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f", AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN), AccountInfoDouble(ACCOUNT_MARGIN_FREE))); last_account_log = TimeCurrent(); } }
Acercándonos...
//--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) { // Attempt to re-initialize logger if it became invalid unexpectedly // This is defensive coding, ideally it shouldn't happen if OnInit succeeded. Print("Error: Logger instance invalid in OnTick! Attempting re-init..."); if(OnInit() != INIT_SUCCEEDED) { Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA."); ExpertRemove(); // Stop the EA return; } }
De forma similar a OnDeinit, la función comienza comprobando si el puntero g_logger es válido mediante CheckPointer. Como medida de seguridad, si se detecta que el registrador no es válido (lo cual, en teoría, no debería ocurrir tras una llamada correcta a OnInit), se intenta reinicializar el registrador llamando de nuevo a OnInit(). Si la reinicialización falla, se registra un error crítico mediante la función Print estándar y se detiene el EA utilizando ExpertRemove().
Además, el EA intenta recuperar la información más reciente sobre los ticks mediante la función SymbolInfoTick().//--- Log tick arrival MqlTick latest_tick; if(SymbolInfoTick(_Symbol, latest_tick)) { g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d", TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS), latest_tick.bid, latest_tick.ask, (int)latest_tick.volume_real)); } else { g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError())); }
Si se realiza correctamente, registra un mensaje de depuración que contiene la marca de tiempo del tick, el precio de compra, el precio de venta y el volumen, con el formato establecido mediante StringFormat. Esto proporciona un registro detallado de los datos de precios entrantes, útil para la depuración. Si SymbolInfoTick falla, se registra un mensaje de advertencia que incluye el código de error obtenido mediante GetLastError().
El código incluye además un ejemplo sencillo de cómo comprobar si se produce un cruce de medias móviles (MA)://--- Example Logic: Check for a simple crossover // Note: Use more robust indicator handling in a real EA double ma_fast[], ma_slow[]; int copied_fast = CopyBuffer(iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_fast); int copied_slow = CopyBuffer(iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_slow); if(copied_fast < 3 || copied_slow < 3) { g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data."); return; // Not enough data yet } // ArraySetAsSeries might be needed depending on how you access indices // ArraySetAsSeries(ma_fast, true); // ArraySetAsSeries(ma_slow, true); bool cross_up = ma_fast[1] > ma_slow[1] && ma_fast[2] <= ma_slow[2]; bool cross_down = ma_fast[1] < ma_slow[1] && ma_fast[2] >= ma_slow[2]; if(cross_up) { g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal."); // --- Add trading logic here --- // Example: SendBuyOrder(); } else if(cross_down) { g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal."); // --- Add trading logic here --- // Example: SendSellOrder(); }
En primer lugar, intenta copiar datos de dos indicadores iMA. Si no se copian suficientes datos, se registra un mensaje de advertencia y la función finaliza. Si hay datos disponibles, comprueba si se ha producido un cruce entre las medias móviles rápidas y lentas en las dos barras anteriores. Cuando se detecta un cruce (al alza o a la baja), se registra un mensaje de nivel Info que indica la posible señal de negociación. Esto demuestra cómo se registran los eventos significativos dentro de la estrategia de negociación.
Finalmente, registramos la información periódicamente, en lugar de en cada ciclo:
// Log account info periodically static datetime last_account_log = 0; if(TimeCurrent() - last_account_log >= 3600) // Log every hour { g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f", AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN), AccountInfoDouble(ACCOUNT_MARGIN_FREE))); last_account_log = TimeCurrent(); }
Una variable estática llamada last_account_log registra la última vez que se registró la información de la cuenta. El código comprueba si la hora actual (TimeCurrent()) es al menos 3600 segundos (1 hora) mayor que la última hora registrada. Si es así, se registra un mensaje informativo que contiene el saldo actual de la cuenta, el capital, el margen y el margen libre, y se actualiza last_account_log. Esto evita saturar los registros con información repetitiva, al tiempo que proporciona actualizaciones de estado periódicas.
En general, la función OnTick muestra cómo usar el registrador para diferentes propósitos durante la ejecución del EA: depuración detallada (Debug ticks), advertencias sobre posibles problemas (Warn on data copy failure), mensajes informativos sobre eventos importantes (Info on signals) y actualizaciones periódicas del estado (Info on account status).
OnChartEvent():
La función OnChartEvent() es un controlador de eventos MQL5 diseñado para procesar varios eventos que ocurren directamente en el gráfico donde se está ejecutando el EA. Estos eventos pueden incluir interacciones del usuario, como pulsaciones de teclado o movimientos del ratón, clics en objetos gráficos o eventos personalizados generados por el EA u otros programas MQL5.
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) return; //--- Log chart events string event_name = "Unknown Chart Event"; switch(id) { case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; break; case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; break; // Add other CHARTEVENT cases as needed case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; break; case CHARTEVENT_CUSTOM+1: event_name = "CustomEvent_1"; break; // Example custom event } g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d (%s), lparam=%d, dparam=%.5f, sparam='%s'", id, event_name, lparam, dparam, sparam)); } //+------------------------------------------------------------------+
Al igual que en OnTick y OnDeinit, la función comienza asegurándose de que el puntero del registrador global g_logger sea válido:
if(CheckPointer(g_logger) == POINTER_INVALID) return;
Si el registrador no es válido, la función simplemente retorna, evitando cualquier procesamiento posterior o posibles errores.
La función principal identifica el tipo de evento y registra sus detalles:
//--- Log chart events string event_name = "Unknown Chart Event"; switch(id) { case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; break; case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; break; // Add other CHARTEVENT cases as needed case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; break; case CHARTEVENT_CUSTOM+1: event_name = "CustomEvent_1"; break; // Example custom event } g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d (%s), lparam=%d, dparam=%.5f, sparam='%s'", id, event_name, lparam, dparam, sparam));
Una instrucción switch toma cada ID de evento entrante y lo convierte en un nombre de evento fácil de entender para el usuario, como CHARTEVENT_KEYDOWN, CHARTEVENT_MOUSE_MOVE o CHARTEVENT_OBJECT_CLICK. Incluso ilustra cómo reaccionar ante una señal definida por el usuario (CHARTEVENT_CUSTOM + 1).
A continuación, emitimos un mensaje de nivel de depuración con g_logger.Debug(). Esta entrada registra el ID del evento, el nombre del evento resuelto y los valores de los parámetros (lparam, dparam, sparam) formateados mediante StringFormat. Mantener esta información a nivel de depuración resulta muy valioso durante el desarrollo y las pruebas, ya que permite rastrear las interacciones de los gráficos y seguir los flujos de eventos personalizados en toda la aplicación.
Ventajas del marco de registro personalizado
Nuestro sistema de registro personalizado ofrece varias mejoras con respecto a la función básica Print():
- Filtrado por gravedad: Vea únicamente los mensajes que le interesan, ordenados por prioridad.
- Salidas múltiples: Envíe los registros a la consola, a archivos o a otros destinos simultáneamente.
- Información contextual detallada: Las marcas de tiempo, la fuente y los detalles de EA se añaden automáticamente.
- Formato flexible: Adapte el diseño de los mensajes a sus preferencias de lectura.
- Rotación de archivos: Evite que los archivos de registro crezcan sin límite.
- Control centralizado: Active o desactive el registro de forma global o para manejadores individuales.
Estas capacidades hacen que la depuración de sistemas de negociación complejos resulte mucho más eficiente. Podrá identificar rápidamente los problemas, observar el comportamiento a lo largo del tiempo y centrarse en los datos que realmente importan.
Conclusión
Una vez implementado este marco de registro personalizado, podrá prescindir de las instrucciones Print() aleatorias y adentrarse en un mundo donde su código se comunica mediante mensajes claros, contextualizados y totalmente ajustables. Los fallos críticos saltan a la vista, los registros exhaustivos están listos para su posterior revisión y los archivos de registro se mantienen ordenados. Mejor aún, el sistema se adapta a sus necesidades: cambie los manejadores, modifique los formatos o ajuste el nivel de detalle cuando lo necesite. El próximo artículo incorporará herramientas de análisis de rendimiento y pruebas unitarias para que pueda detectar problemas de rendimiento y fallos de lógica mucho antes de que aparezcan en un gráfico en tiempo real. Así es como se ve la verdadera artesanía en MQL5.
Y tenga en cuenta que esta es solo la primera etapa del viaje. Todavía tenemos previsto implementar técnicas avanzadas de depuración, analizadores de rendimiento personalizados, un sistema de pruebas unitarias robusto y análisis automatizados de la calidad del código. Al finalizar la serie, cambiará la búsqueda reactiva de errores por una rutina de calidad disciplinada y proactiva.
¡Hasta entonces, felices operaciones y feliz programación!
Resumen del archivo:
| Nombre del archivo | Descripción del archivo |
|---|---|
| LogLevels.mqh | Define la enumeración LogLevel con valores de gravedad DEBUG→OFF utilizados en todo el marco de trabajo. |
| ILogHandler.mqh | Declara la interfaz ILogHandler (Setup/Log/Shutdown) que implementan todas las clases concretas de salida de registro. |
| ConsoleLogHandler.mqh | Implementa ILogHandler para imprimir mensajes de registro formateados en la pestaña "Expertos" de MetaTrader con filtrado basado en niveles. |
| FileLogHandler.mqh | Implementa ILogHandler para escribir registros en archivos rotativos diarios/de tamaño limitado, manteniendo un historial de archivos configurable. |
| CLogger.mqh | Registrador singleton que almacena manejadores, aplica filtrado global por gravedad y ofrece métodos auxiliares de registro. |
| LoggingExampleEA.mq5 | Ejemplo de Asesor Experto que muestra cómo configurar, usar y desactivar el marco de registro personalizado en la práctica. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/17933
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Del básico al intermedio: Eventos en Objetos (III)
Creación de un Panel de administración de operaciones en MQL5 (Parte XI): Interfaz moderna de funciones de comunicación (I)
Simulación de mercado: Iniciando SQL en MQL5 (V)
Del básico al intermedio: Eventos en Objetos (II)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso