English Русский 中文 Deutsch 日本語
preview
Dominando los registros (Parte 9): Implementación del patrón Builder y adición de configuraciones predeterminadas

Dominando los registros (Parte 9): Implementación del patrón Builder y adición de configuraciones predeterminadas

MetaTrader 5Ejemplos |
37 7
joaopedrodev
joaopedrodev

Introducción

Desde que comencé a usar Logify en varios proyectos personales y profesionales, rápidamente me di cuenta de que el mayor desafío no radicaba en la robustez o la funcionalidad de la biblioteca en sí, sino en su configuración. Logify es una potente herramienta para gestionar registros en Asesores Expertos, que ofrece múltiples manejadores, niveles de registro, formatos personalizados, compatibilidad con varios idiomas y mucho más. Sin embargo, todo esto requería que el usuario configurara manualmente cada controlador, formateador y parámetro, lo que puede funcionar bien para proyectos pequeños, pero que rápidamente se convierte en una tarea repetitiva, tediosa y propensa a errores a medida que aumenta el número de asesores expertos (EAs) y proyectos.

Imagínese tener que replicar, para cada EA, el mismo conjunto complejo de configuraciones: crear manejadores específicos para comentarios en el gráfico, consola, archivos y bases de datos; establecer niveles mínimos para cada manejador; definir formatos específicos para mensajes de error, depuración, alertas, etc. Cada uno de estos pasos, aunque necesario, genera un código largo, detallado y poco intuitivo, rompiendo el flujo de trabajo del desarrollo. Esta complejidad inicial se convierte en una barrera, una fricción que puede desalentar la adopción de Logify, incluso si ofrece una gestión de registros impecable en tiempo de ejecución.

Esta reflexión me motivó a pensar: ¿cómo puedo simplificar esta configuración, facilitando la vida del usuario, sin renunciar a la flexibilidad y la personalización? Fue en ese momento cuando surgió la idea de crear un builder para Logify: una clase que permite ensamblar toda la configuración de forma fluida y encadenada, con métodos intuitivos que crean manejadores con patrones sensatos y permiten ajustes rápidos y puntuales. El objetivo es transformar docenas de líneas de configuración en unas pocas llamadas a métodos, casi como si estuviéramos escribiendo un resumen claro de lo que queremos, en lugar de todo el ensamblaje manual.

En este artículo, les mostraré cómo he implementado estas mejoras. Presentaré el builder, explicando su diseño y funcionamiento. A continuación, les mostraré cómo se puede configurar Logify con ejemplos prácticos.


Comprender el patrón Builder: simplificando la construcción de objetos complejos

Antes de adentrarnos en la implementación de nuestro CLogifyBuilder, es importante comprender la idea que subyace al patrón que estamos utilizando: el Builder.

En la práctica, Builder es un patrón de diseño que tiene un único objetivo: facilitar la creación de objetos complejos, especialmente cuando estos objetos requieren múltiples pasos de configuración o tienen muchas opciones posibles. La propuesta consiste en separar el proceso de construcción de la representación final del objeto, permitiendo que la misma estructura de ensamblaje cree diferentes "variantes" de objetos listos para usar.

Pongamos un ejemplo sencillo: imagina que vas a ensamblar un coche. Puedes elegir el modelo, el color, el tipo de motor, la transmisión, los extras, el tamaño de las ruedas, el acabado interior, entre docenas de otras decisiones. Hacer todo esto directamente en el código, pasando cientos de argumentos a un constructor, no es práctico ni fácil de mantener.

Es precisamente para este tipo de situaciones que Builder brilla. Divide esta construcción en métodos encadenados (también llamados interfaces fluidas), cada uno responsable de configurar una parte específica del objeto, hasta que al final llamas a .Build() o algo similar, y recibes el objeto final, 100% listo para usar.

Este enfoque aporta tres beneficios principales:

  1. Lectura clara y lineal: la estructura del objeto se asemeja a un «guión» lógico, casi como si estuvieras describiendo lo que quieres.
  2. Reducción de errores: dado que cada paso tiene un objetivo concreto, resulta mucho más fácil detectar y corregir las configuraciones incorrectas.
  3. Flexibilidad y reutilización: el mismo constructor se puede reutilizar para crear variaciones del objeto con cambios menores.


Aplicación de Builder a Logify

En el caso de Logify, crear una instancia del registrador (CLogify) implica crear varios controladores (como consola, comentario o archivo), configurar los niveles mínimos de registro, definir formateadores específicos para cada controlador e incluso parámetros como el tamaño del panel o el estilo del marco. Hacer todo esto manualmente, línea por línea, una y otra vez, se ha convertido en un proceso tedioso.

El uso del patrón Builder en este caso resuelve este problema. En lugar de hacer que el usuario tenga que encargarse de montar manualmente cada pieza, ofrecemos una interfaz más intuitiva, como esta:

CLogify *logify = logify
   .Create()
   .AddHandlerComment()
      .SetTitle("My Logger")
      .SetSize(5)
   .Done()
   .AddHandlerConsole()
   .Done()
   .Build();

Fíjate en lo sencillo y expresivo que resulta al leerlo. El método .Create() inicia la construcción; cada .AddHandlerXXX() abre la configuración de un controlador; los métodos SetX() ajustan sus parámetros; el método .Done() finaliza la configuración de un controlador; y el método .Build() devuelve la instancia ya preparada de CLogify.

Este es el poder de Builder: permite al desarrollador describir lo que quiere, sin tener que preocuparse por los detalles de cómo se implementará en segundo plano.

Ahora que hemos entendido por qué utilizamos este patrón y cómo nos ayuda a hacer que Logify sea más práctico y escalable, veamos en la práctica cómo se ha creado esta clase y cómo podemos utilizarla en la vida real.


Constructores especializados para cada manejador

Hemos creado un nuevo archivo <Include/Logify/LogifyBuilder.mqh>. En su interior se encuentra la clase CLogifyBuilder, que ya contiene, como campo privado, una instancia de CLogify; esta instancia será manipulada y, finalmente, devuelta al usuario.

//+------------------------------------------------------------------+
//|                                                LogifyBuilder.mqh |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#include "Logify.mqh"
//+------------------------------------------------------------------+
//| class : CLogifyBuilder                                           |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyBuilder                                      |
//| Heritage    : No heritage                                        |
//| Description : Build CLogify objects, following the Builder design|
//|               pattern.                                           |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyBuilder
  {
private:
   CLogify           *m_logify;
   
public:
                     CLogifyBuilder(void)
                    ~CLogifyBuilder(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyBuilder::CLogifyBuilder(void)
  {
   m_logify = new CLogify();
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyBuilder::~CLogifyBuilder(void)
  {
  }
//+------------------------------------------------------------------+

La clase CLogifyBuilder es el punto central de la construcción de un objeto CLogify, pero delega la configuración de cada tipo de controlador a constructores especializados: CLogifyHandlerCommentBuilder, CLogifyHandlerConsoleBuilder, CLogifyHandlerDatabaseBuilder y CLogifyHandlerFileBuilder.

Cada constructor encapsula los detalles de un único tipo de salida de registro. Esto evita mezclar responsabilidades y mantiene limpio y modular el núcleo del proceso de construcción. Analicemos su estructura, tomando como ejemplo ConsoleBuilder.


CLogifyHandlerConsoleBuilder

La clase comienza definiendo la estructura mínima necesaria para admitir la API fluida. Recibe un puntero al constructor principal (CLogifyBuilder*) en el constructor, manteniendo una referencia al contexto de construcción. Esto te permite volver a este contexto con Done() después de configurar el manejador:

//+------------------------------------------------------------------+
//| class : CLogifyHandlerConsoleBuilder                             |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyHandlerConsoleBuilder                        |
//| Heritage    : No heritage                                        |
//| Description : Console handler constructor.                       |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerConsoleBuilder
  {
private:

   CLogifyBuilder    *m_parent;

public:
                     CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify);
                    ~CLogifyHandlerConsoleBuilder(void);

   CLogifyBuilder    *Done(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify)
  {
   m_parent = logify;
  };
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::~CLogifyHandlerConsoleBuilder(void)
  {
  }
//+------------------------------------------------------------------+
//| Finalizes the handler configuration.                             |
//+------------------------------------------------------------------+
CLogifyBuilder    *CLogifyHandlerConsoleBuilder::Done(void)
  {
   m_parent.AddHandler(GetPointer(m_handler));
   delete GetPointer(this);
   return(m_parent);
  }
//+------------------------------------------------------------------+

Done() es el punto de retorno al constructor principal, agrega el controlador a CLogify y destruye el constructor intermedio. Esto mantiene la fluidez del proceso y evita la retención innecesaria de memoria.

Todos los constructores especializados siguen la misma anatomía:

  • CLogifyBuilder *m_parent: referencia al constructor padre, utilizado para devolver a través de Done().
  • CLogifyFormatter *m_formatter: instancia de formateador que se asociará con el controlador.
  • CLogifyHandlerX *m_handler: el propio controlador, que se está configurando.

Los constructores más complejos (como File o Database) también utilizan una estructura de configuración interna (MqlLogifyHandleXConfig), que almacena los valores temporalmente hasta que el controlador esté listo para ser registrado.

Esta separación entre los datos de configuración y su aplicación al controlador permite aplicar validaciones, usar preajustes y combinar opciones sin sobrecargar la lógica del propio controlador.


Constructor completo del manejador de consola

A continuación, el constructor con los métodos de configuración ya implementados:

//+------------------------------------------------------------------+
//| class : CLogifyHandlerConsoleBuilder                             |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyHandlerConsoleBuilder                        |
//| Heritage    : No heritage                                        |
//| Description : Console handler constructor.                       |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerConsoleBuilder
  {
private:

   CLogifyBuilder    *m_parent;
   CLogifyFormatter  *m_formatter;
   CLogifyHandlerConsole *m_handler;

public:
                     CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify);
                    ~CLogifyHandlerConsoleBuilder(void);

   CLogifyHandlerConsoleBuilder *SetLevel(ENUM_LOG_LEVEL level);
   CLogifyHandlerConsoleBuilder *SetFormatter(string format);
   CLogifyHandlerConsoleBuilder *SetFormatter(ENUM_LOG_LEVEL level, string format);
   CLogifyBuilder    *Done(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify)
  {
   m_parent = logify;
   m_formatter = new CLogifyFormatter();
   m_handler = new CLogifyHandlerConsole();

   m_handler.SetFormatter(GetPointer(m_formatter));
  };
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::~CLogifyHandlerConsoleBuilder(void)
  {
  }
//+------------------------------------------------------------------+
//| Sets the log level for the handler.                              |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetLevel(ENUM_LOG_LEVEL level)
  {
   m_handler.SetLevel(level);
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Sets the default format string for the formatter.                |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetFormatter(string format)
  {
   m_formatter.SetFormat(format);
   m_handler.SetFormatter(GetPointer(m_formatter));
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Sets a log-level-specific format for the formatter.              |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetFormatter(ENUM_LOG_LEVEL level, string format)
  {
   m_formatter.SetFormat(level,format);
   m_handler.SetFormatter(GetPointer(m_formatter));
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Finalizes the handler configuration.                             |
//+------------------------------------------------------------------+
CLogifyBuilder    *CLogifyHandlerConsoleBuilder::Done(void)
  {
   m_parent.AddHandler(GetPointer(m_handler));
   delete GetPointer(this);
   return(m_parent);
  }
//+------------------------------------------------------------------+

Una vez listo esto, pasamos al resto de los constructores especializados.


Otros constructores especializados

CLogifyHandlerCommentBuilder

Responsable de configurar el manejador que escribe mensajes directamente en el gráfico (Comment()), utilizando la estructura MqlLogifyHandleCommentConfig.

Le permite definir:

  • SetSize(int): número de mensajes mostrados.
  • SetFrameStyle(ENUM_LOG_FRAME_STYLE): marco alrededor del área del registro.
  • SetDirection(ENUM_LOG_DIRECTION): orientación vertical u horizontal.
  • SetTitle(string): título fijo en la parte superior del registro.

También acepta SetLevel() y SetFormatter(), de forma global o por nivel. La configuración se finaliza con Done().

CLogifyHandlerDatabaseBuilder

Configura el manejador de persistencia de la base de datos (estructura binaria, por el momento). Utiliza MqlLogifyHandleDatabaseConfig. Ofrece:

  • SetDirectory(string)
  • SetBaseFileName(string)
  • SetMessagesPerFlush(int)

La estructura es idéntica a la de los demás constructores, manteniendo la coherencia.

CLogifyHandlerFileBuilder

El más completo de todos. Configura la escritura en archivos .log, .txt, etc., a través de MqlLogifyHandleFileConfig.

Opciones disponibles:

  • SetDirectory(), SetFilename(), SetFileExtension()
  • SetRotationMode(), por fecha, tamaño o manual
  • SetMessagesPerFlush()
  • SetCodepage(), como CP_UTF8
  • SetFileSizeMB(), SetMaxFileCount()

Y también tres métodos de utilidad con ajustes preestablecidos listos para usar:

  • ConfigNoRotation()
  • ConfigDateRotation()
  • ConfigSizeRotation()

Estos atajos encapsulan la configuración completa con llamadas únicas, lo que resulta útil para patrones recurrentes. Los constructores de los demás manejadores siguen la misma estructura, con variaciones de configuración específicas. Puede consultar los códigos completos en los archivos adjuntos.


La clase principal: CLogifyBuilder

Ahora que hemos explorado cómo funcionan los constructores especializados, es hora de analizar el componente que los coordina: la clase CLogifyBuilder.

Este componente se encarga de crear y mantener la instancia principal de CLogify, a la que se añadirán todos los manejadores. Pero, en lugar de configurarlo todo directamente, delega esta responsabilidad en constructores especializados, cada uno de los cuales se encarga de un tipo específico de controlador. Así pues, CLogifyBuilder actúa como una especie de director de orquesta, coordinando la construcción modular del registrador.

A continuación se muestra la implementación completa de la clase:

//+------------------------------------------------------------------+
//| class : CLogifyBuilder                                           |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyBuilder                                      |
//| Heritage    : No heritage                                        |
//| Description : Build CLogify objects, following the Builder design|
//|               pattern.                                           |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyBuilder
  {
private:

   CLogify           *m_logify;

public:
                     CLogifyBuilder(void);
                    ~CLogifyBuilder(void);

   CLogifyBuilder    *UseLanguage(ENUM_LANGUAGE language);

   //--- Starts configuration handlers
   CLogifyHandlerCommentBuilder *AddHandlerComment(void);
   CLogifyHandlerConsoleBuilder *AddHandlerConsole(void);
   CLogifyHandlerDatabaseBuilder *AddHandlerDatabase(void);
   CLogifyHandlerFileBuilder *AddHandlerFile(void);

   void              AddHandler(CLogifyHandler *handler);
   CLogify           *Build(void);
  };
//+------------------------------------------------------------------+

Esta clase concentra algunas funciones importantes:

  • UseLanguage(ENUM_LANGUAGE language): Permite configurar el idioma principal del sistema de registro. Esto afecta a los mensajes de error internos (a través de CLogifyError) y al formato que depende de la localización.
  • AddHandlerX(): Estos son los puntos de entrada para configurar los manejadores. Cada método (AddHandlerConsole(), AddHandlerFile(), etc.) instancia un constructor especializado, pasándose a sí mismo como un puntero (this) para que pueda devolver un valor a través de Done() una vez configurado.
  • AddHandler(CLogifyHandler *handler): Este método es llamado internamente por constructores especializados al final de la configuración (Done()). Registra el manejador ya configurado en la instancia de CLogify que se está creando.
  • Build(): Finaliza el proceso de construcción, libera el constructor de la memoria con delete GetPointer(this) y devuelve el registrador listo. Esto refuerza la idea de que la instancia del constructor solo existe durante el proceso de ensamblaje.

Con esta estructura, el patrón Builder queda completo: modular, claro y ampliable. Puedes ver el código completo en los archivos adjuntos.


Un punto de entrada elegante

Aunque ya tenemos el constructor CLogifyBuilder, exponer directamente este constructor para que el usuario lo instancie con new CLogifyBuilder() no es la forma más expresiva ni intuitiva de empezar a construir el registrador.

Por eso hemos añadido un método estático llamado Create() a la clase CLogify:

//+------------------------------------------------------------------+
//| Returns an instance of the builder                               |
//+------------------------------------------------------------------+
#include "LogifyBuilder.mqh"
CLogifyBuilder *CLogify::Create(void)
  {
   return(new CLogifyBuilder());
  }
//+------------------------------------------------------------------+

Y el método se declara de esta manera en la clase CLogify:

class CLogify
  {
public:
   static CLogifyBuilder *Create(void);
  };

El método Create() es estático porque:

  1. Pertenece a la clase, no a la instancia — todavía no tienes una instancia de CLogify cuando quieres empezar a crearla.
  2. No depende de ningún estado interno — lo único que hace es crear y devolver un constructor.
  3. Evita el acoplamiento directo al constructor — si mañana cambia la implementación del constructor, podrás mantener la misma interfaz estática en CLogify y conservar la compatibilidad con el código existente.


Configuración predeterminada

A medida que la biblioteca Logify va tomando forma, debemos tener en cuenta una situación habitual: el usuario que quiere registrar mensajes rápidamente, sin tener que configurar nada. No les importan los manejadores, los lenguajes, los formatos ni los directorios; solo necesitan que los mensajes se muestren de forma visible durante el desarrollo o las pruebas. Para solucionar esto, hemos introducido el método EnsureDefaultHandler().

Este método actúa como un mecanismo de respaldo automático: si no se ha configurado explícitamente ningún controlador, añade dos controladores básicos y funcionales, uno para la consola y otro para Comment(). Ambos mecanismos se apoyan en recursos nativos de MQL5 y garantizan la visualización inmediata del mensaje.

void CLogify::EnsureDefaultHandler()
  {
   //--- Check if there is no handler
   if(this.SizeHandlers() == 0)
     {
      this.AddHandler(new CLogifyHandlerConsole());
      this.AddHandler(new CLogifyHandlerComment());
     }
  }

La llamada se produce dentro del método Append(), no en el constructor:

bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0, int code_error = 0)
  {
   //--- Ensures that there is at least one handler
   this.EnsureDefaultHandler();
   
   // (continues...)
  }

Esta decisión tiene un objetivo estratégico: si añadiéramos manejadores por defecto en el constructor, se incluirían en cualquier configuración que se realizara posteriormente, lo que significaría que el registro acabaría conteniendo manejadores duplicados o no deseados. Esto resulta especialmente problemático cuando el usuario desea dirigir toda la salida a un único destino, como un archivo, una base de datos o un servidor remoto.

Al trasladar esta lógica a Append(), dejamos el control en manos del desarrollador. Funciona así:

  • Si no hay ningún controlador configurado, EnsureDefaultHandler() activa ambos valores predeterminados en la primera llamada a Append().
  • Si se añade al menos un controlador manualmente, el método no hace nada.
  • El comportamiento predeterminado es seguro y visible, pero no interfiere cuando existe una configuración explícita.

Este enfoque equilibra la comodidad y la previsibilidad. Para quienes buscan algo rápido y funcional, el sistema «funciona por sí solo». Para quienes necesiten un control preciso, la biblioteca respeta estrictamente las decisiones del desarrollador.

Con esto, Logify se convierte en una solución «plug-and-play» sin renunciar a la personalización, lo que supone un paso importante para facilitar su adopción tanto por parte de principiantes como de equipos que exigen estándares de registro más estrictos.


Pruebas

Comparemos cómo era utilizar la biblioteca antes y después de las mejoras descritas en este artículo.

Antes, configurar un registro requería una serie de pasos manuales: instanciar objetos, definir niveles, crear formateadores, rellenar estructuras de configuración y ensamblar controladores uno por uno. Ahora, gracias a la introducción de los ajustes predeterminados mediante EnsureDefaultHandler(), el desarrollador puede empezar a utilizar la biblioteca con una sola línea de código.

A continuación, puedes ver los dos escenarios uno al lado del otro:

Código antiguo Nuevo código
//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   MqlLogifyHandleCommentConfig m_config;
   m_config.size = 5;
   m_config.frame_style = LOG_FRAME_STYLE_SINGLE;
   m_config.direction = LOG_DIRECTION_UP;
   m_config.title = "Expert name";

   CLogifyFormatter *formatter = new CLogifyFormatter("{date_time} [{levelname}]: {msg}");
   formatter.SetFormat(LOG_LEVEL_ERROR,"{date_time} [{levelname}]: {msg} [{err_constant} | {err_code} | {err_description}]");

   CLogifyHandlerComment *handler_comment = new CLogifyHandlerComment();
   handler_comment.SetConfig(m_config);
   handler_comment.SetLevel(LOG_LEVEL_DEBUG);
   handler_comment.SetFormatter(formatter);

   CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole();
   handler_console.SetLevel(LOG_LEVEL_DEBUG);
   handler_console.SetFormatter(formatter);

   logify = new CLogify();
   logify.AddHandler(handler_comment);
   logify.AddHandler(handler_console);

   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");

   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   logify = new CLogify();
   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+

Este enfoque minimalista cubre la mayoría de los casos sin ninguna configuración manual, lo que resulta ideal para la creación rápida de prototipos y la realización de pruebas.

En los casos en los que necesitemos un mayor control sobre el comportamiento del registro, entra en juego el nuevo Builder. Ofrece una interfaz fluida y, lo más importante, totalmente tipada, lo que significa que el propio editor de código sugiere los métodos disponibles en tiempo real, lo que reduce los errores y elimina la necesidad de memorizar las firmas de las funciones.

Al escribir «logify.Create().AddHandler», el editor ya sugiere todos los manejadores disponibles:

Y cuando continúas con .AddHandlerComment(), solo aparecen las opciones válidas para ese tipo concreto de controlador:

Así es como queda el código al final, con la configuración del manejador de comentarios y un formato específico para los errores.

//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   logify = logify.Create().AddHandlerComment().SetLevel(LOG_LEVEL_DEBUG).SetFormatter(LOG_LEVEL_ERROR,"{date_time} [{levelname}] {msg} ({err_constant} {err_code}: {err_description})").SetTitle("My expert").SetSize(5).Done().Build();
//---
   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+

Esta experiencia guiada elimina dudas y reduce la fricción en el desarrollo. El código es limpio, sencillo y a prueba de errores.


Solución para MetaTrader 5, versión 5100 o superior

Con el lanzamiento de la versión 5100 de MetaTrader 5, algunos cambios internos en el compilador exigen ahora una mayor claridad en el manejo de los tipos en llamadas como DatabaseColumnLong() y DatabaseColumnInteger().

En la práctica, esto significa que ya no es seguro pasar directamente referencias a campos de estructuras (como data[size].timestamp) en estas funciones. Para evitar errores de compilación, lo ideal es almacenar primero el valor en una variable temporal del tipo adecuado y, solo entonces, pasarlo por referencia a la función.

Código antiguo Nuevo código
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- The rest of the method code remains the same

   //--- Reads query results line by line
   for(int i=0;DatabaseRead(request);i++)
     {
      int size = ArraySize(data);
      ArrayResize(data,size+1,size);
      
      //--- Maps database data to the MqlLogifyModel model
      DatabaseColumnText(request,1,data[size].formated);
      DatabaseColumnText(request,2,data[size].levelname);
      DatabaseColumnText(request,3,data[size].msg);
      DatabaseColumnText(request,4,data[size].args);
      DatabaseColumnLong(request,5,data[size].timestamp);
      string value;
      DatabaseColumnText(request,6,value);
      data[size].date_time = StringToTime(value);
      DatabaseColumnInteger(request,7,data[size].level);
      DatabaseColumnText(request,8,data[size].origin);
      DatabaseColumnText(request,9,data[size].filename);
      DatabaseColumnText(request,10,data[size].function);
      DatabaseColumnLong(request,11,data[size].line);
     }
   
   //--- The rest of the method code remains the same
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- The rest of the method code remains the same

   //--- Reads query results line by line
   for(int i=0;DatabaseRead(request);i++)
     {
      int size = ArraySize(data);
      ArrayResize(data,size+1,size);
      
      //--- Maps database data to the MqlLogifyModel model
      DatabaseColumnText(request,1,data[size].formated);
      DatabaseColumnText(request,2,data[size].levelname);
      DatabaseColumnText(request,3,data[size].msg);
      DatabaseColumnText(request,4,data[size].args);
      long timestamp = (long)data[size].timestamp;
      DatabaseColumnLong(request,5,timestamp);
      string value;
      DatabaseColumnText(request,6,value);
      data[size].date_time = StringToTime(value);
      int level = data[size].level;
      DatabaseColumnInteger(request,7,level);
      DatabaseColumnText(request,8,data[size].origin);
      DatabaseColumnText(request,9,data[size].filename);
      DatabaseColumnText(request,10,data[size].function);
      long line = (long)data[size].line;
      DatabaseColumnLong(request,11,line);
     }
   
   //--- The rest of the method code remains the same
  }
//+------------------------------------------------------------------+

El resto del código permanece igual. Se trata de un ajuste puntual, pero necesario para mantener la compatibilidad con las versiones recientes del terminal.

Conviene recordar que este tipo de ajuste es común cuando el compilador se vuelve más exigente con los tipos y suele deberse a la necesidad de evitar problemas sutiles en tiempo de ejecución. Por lo tanto, aunque pueda parecer un cambio sencillo, se trata de una actualización importante para garantizar que Logify siga funcionando de forma estable en las versiones más recientes de MetaTrader 5.


Conclusión

Hasta ahora, configurar la biblioteca Logify era potente, pero un poco burocrático. Había que crear objetos, configurar manualmente los parámetros, recordar el orden de las llamadas... en resumen, funcionaba, pero era un poco engorroso.

En esta parte del artículo, hemos resuelto ese problema. Hemos creado una nueva forma de trabajar con el registro: sencilla, clara y rápida. El builder entró en escena para que todo resultara más natural: escribes logify.Create() y el propio editor te muestra las siguientes opciones. ¿Quieres un manejador de comentarios? Escribe AddHandlerComment(). ¿Quieres cambiar el título? Aparece SetTitle(). No necesitas memorizar nada, no necesitas repasar la documentación. Solo tienes que seguir el flujo.

Además, lo hemos hecho aún más fácil de usar con la configuración predeterminada. Si solo quieres registrar mensajes y no te preocupa personalizar el registro, no necesitas hacer nada. Simplemente crea el objeto y comienza a usarlo. Logify se encarga por sí solo de mostrar los mensajes en la consola y en el gráfico.

Por último, hemos ajustado un detalle técnico importante: con la llegada de MetaTrader 5 build 5100, el compilador se ha vuelto más estricto a la hora de pasar referencias en funciones como DatabaseColumnLong() y DatabaseColumnInteger(). Para garantizar la compatibilidad, hemos añadido pequeñas correcciones a CLogifyHandlerDatabase, utilizando variables intermedias antes de pasar los datos a estas funciones. Para quienes utilizan la biblioteca, nada cambia, pero en segundo plano permanece estable, incluso con las actualizaciones de la terminal.

Al final, conseguimos lo que a todo desarrollador le gusta: menos código, menos errores y más claridad. Ahora la biblioteca se comunica mejor con sus usuarios, sin imponer rigidez alguna y sin complicar lo que debería ser sencillo. A medida que Logify evolucione, volviéndose aún más flexible e incorporando nuevas funciones que creo que serán útiles para la mayoría de los usuarios, les ofreceré nuevos artículos que mostrarán las mejoras y facilitarán nuestro día a día. La idea es que la biblioteca crezca junto con quienes la utilizan, sin magia, solo con código bien pensado.

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/Error/Languages/ErrorMessages.XX.mqh Contiene los mensajes de error en cada idioma, donde X representa el acrónimo del idioma.
Include/Logify/Error/Error.mqh
Estructura de datos para almacenar errores.
Include/Logify/Error/LogifyError.mqh
Clase para obtener información detallada sobre errores.
Include/Logify/Formatter/LogifyFormatter.mqh
Clase responsable de formatear los registros de log, reemplazando los marcadores de posición con valores específicos.
Include/Logify/Handlers/LogifyHandler.mqh
Clase base para gestionar los manejadores de registros, incluyendo la configuración de niveles y el envío de registros.
Include/Logify/Handlers/LogifyHandlerComment.mqh
Gestor de registros que envía registros formateados directamente al comentario en el gráfico de la terminal en MetaTrader.
Include/Logify/Handlers/LogifyHandlerConsole.mqh
Gestor 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 (por ahora solo contiene una salida impresa, pero pronto los guardaremos en una base de datos SQLite real)
Include/Logify/Handlers/LogifyHandlerFile.mqh
Controlador de registros que envía registros formateados a un archivo.
Include/Logify/Utils/IntervalWatcher.mqh
Comprueba si ha transcurrido un intervalo de tiempo, lo que le permite crear rutinas dentro de la biblioteca.
Include/Logify/Logify.mqh Clase principal para la gestión de registros, integrando niveles, modelos y formato.
Include/Logify/LogifyBuilder.mqh Clase responsable de crear un objeto CLogify, simplificando la configuración.
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 los registros de log, 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/18602

Archivos adjuntos |
Logify.zip (154.39 KB)
Spoxus Spoxus
Spoxus Spoxus | 2 jul 2025 en 21:16

Me pregunto si quiero mostrar solo los mensajes de depuración y de error. Y tengo todos los mensajes de información, alerta, etc., integrados en el EA. ¿Quizás podríamos establecer un valor booleano para cada tipo en «enum ENUM_LOG_LEVEL» para mostrar lo que queramos?

En el código de producción, si desactivamos algunos de los registros, estos no deberían compilarse en el archivo ex5 final.

joaopedrodev
joaopedrodev | 31 jul 2025 en 16:58
Spoxus Spoxus de producción, si desactivamos algunos de los registros, estos no deberían compilarse en el archivo ex5 final.

Para ello, puedes utilizar una variable o incluso una dirección IP en el EA que almacene el valor de nivel deseado y simplemente lo pase al controlador. Aquí tienes un ejemplo.

//+------------------------------------------------------------------+
//| Importar                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify Logify;
//+------------------------------------------------------------------+
//| Entradas                                                           |
//+------------------------------------------------------------------+
input ENUM_LOG_LEVEL InpLogLevel = LOG_LEVEL_INFO; // Nivel de registro
//+------------------------------------------------------------------+
//| Función de inicialización del experto                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Logify.EnsureDefaultHandler();
   Logify.GetHandler(0).SetLevel(InpLogLevel);
   
   Logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   Logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   Logify.Error("Failed to send sell order", 10016,"Order Management");
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Esto mostrará únicamente los mensajes con un nivel de gravedad mayor o igual al definido en el controlador.

joaopedrodev
joaopedrodev | 31 jul 2025 en 16:59
hini #:

El idioma predeterminado de los mensajes de error del registro se puede ajustar al idioma de la terminal del usuario siguiendo este código

La parte 10 de este artículo está pendiente de publicación. En ella se explica cómo suprimir los registros idénticos y también cómo configurar el idioma predeterminado de la terminal. ¡Gracias por la sugerencia!

Amy Liu
Amy Liu | 4 ene 2026 en 13:41
Este es el mejor artículo que he leído nunca — ¡me encanta!
Amy Liu
Amy Liu | 4 ene 2026 en 13:52
Spoxus Spoxus de producción, si desactivamos algunos de los registros, estos no deberían compilarse en el archivo ex5 final.
joaopedrodev #:

Para ello, puedes utilizar una variable o incluso una dirección IP en el experto que almacene el valor de nivel deseado y simplemente lo pase al controlador. Aquí tienes un ejemplo.

Esto mostrará únicamente los mensajes con un nivel de gravedad mayor o igual al definido en el controlador.

El autor muestra cómo cambiar el nivel en tiempo de ejecución sin modificar el código.

Creo que solo quiere mostrar un nivel de registro. Tiene que modificar el código como se indica a continuación:

//--- P. ej.
void CLogifyHandlerComment::Emit(MqlLogifyModel &data)
  {
   //--- Comprobar si el nivel de registro está permitido
   if(data.level != this.GetLevel()) // cambia «<» por «!=»
     {
      return;
     }

   //--- Registros de turnos para mantener el historial
   for(int i = m_config.size-1; i > 0; i--)
     {
      m_cache[i] = m_cache[i-1];
     }
   m_cache[0] = data;

   //--- Generar el comentario completo
   string comment = BuildHeader();
   comment += FormatLogLines();
   comment += BuildFooter();

   //--- Mostrar en el gráfico
   Comment(comment);
  }
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Automatización de estrategias de trading en MQL5 (Parte 20): Estrategia multisímbolo con CCI y AO Automatización de estrategias de trading en MQL5 (Parte 20): Estrategia multisímbolo con CCI y AO
En este artículo, creamos una estrategia de trading multisimbolo utilizando los indicadores CCI y AO para captar reversiones de tendencia. Analizamos su diseño, la implementación de MQL5 y el proceso de backtesting. El artículo concluye con consejos para mejorar el rendimiento.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Repetición y simulación de mercado: Gran final Repetición y simulación de mercado: Gran final
Sé que muchos podrían haber imaginado que publicaría más artículos para explicar otros aspectos del sistema. Los elementos que faltan son sencillos de implementar. Aun así, su desarrollo te permitirá comprobar hasta qué punto estás realmente preparado.