English Русский 中文 Deutsch 日本語
preview
Dominando los registros (Parte 6): Guardando los registros en la base de datos

Dominando los registros (Parte 6): Guardando los registros en la base de datos

MetaTrader 5Ejemplos |
14 0
joaopedrodev
joaopedrodev

Introducción

Imagina un bullicioso mercado de transacciones digitales y magia financiera donde cada movimiento es rastreado, registrado y analizado meticulosamente para garantizar el éxito. ¿Y si no solo pudieras acceder a un historial de todas las decisiones y errores cometidos por tus asesores expertos, sino que además pudieras disponer de una potente herramienta para optimizar y perfeccionar estos bots en tiempo real? Bienvenidos a la primera parte de Dominando los registros (Parte 1): Conceptos fundamentales y primeros pasos en MQL5, donde hemos comenzado a crear una biblioteca de registro sofisticada y adaptada al desarrollo en MQL5.

Aquí, superamos las limitaciones de la interfaz de registro predeterminada de MetaTrader 5 para crear una solución de registro robusta, ajustable y dinámica que mejora el entorno de MQL5. Nuestro recorrido comenzó con la incorporación de requisitos fundamentales: una estructura Singleton fiable para una codificación coherente, registros de base de datos avanzados para un seguimiento de auditoría completo, flexibilidad de salida versátil, clasificación de los niveles de registro y formatos personalizables para las diversas necesidades de los proyectos.

Únase a nosotros mientras profundizamos en cómo puede transformar los datos brutos en información útil para comprender, controlar y mejorar el rendimiento de sus EA como nunca antes.

En este artículo, exploraremos todo, desde los conceptos fundamentales hasta la implementación práctica de un gestor de registros que escribe y consulta datos directamente desde una base de datos.


¿Qué son las bases de datos?

Los registros son el pulso de un sistema, capturando todo lo que sucede entre bastidores. Pero almacenarlos de forma eficiente es otra historia. Hasta ahora, hemos guardado los registros en archivos de texto, una solución sencilla y funcional en muchos casos. El problema surge cuando aumenta el volumen de datos: buscar información entre miles de líneas se convierte en una pesadilla para el rendimiento y la gestión.

Ahí es donde entran en juego las bases de datos. Ofrecen una forma estructurada y optimizada de almacenar, consultar y organizar la información. En lugar de revisar los archivos manualmente, podemos realizar consultas rápidas y encontrar exactamente lo que necesitamos. Pero, al fin y al cabo, ¿qué es una base de datos y por qué es tan esencial?


La estructura de una base de datos

Una base de datos funciona como un sistema de almacenamiento inteligente, donde los datos se organizan lógicamente para facilitar las búsquedas y manipulaciones. Imagínelo como un archivo bien catalogado, donde cada dato tiene su lugar definido. En el contexto de los registros, en lugar de guardar los datos en archivos dispersos, podemos almacenarlos de forma estructurada, filtrándolos rápidamente por fecha, tipo de error o cualquier otro criterio relevante.

Para comprenderlo mejor, vamos a desglosar la estructura de una base de datos en sus tres componentes fundamentales: tablas, columnas y filas.

  • Tablas: La base de la base de datos, una tabla funciona como una hoja de cálculo, donde agrupamos datos relacionados. En el caso de los registros, podríamos tener una tabla llamada "logs", dedicada exclusivamente a almacenar estos registros.

    Cada tabla está diseñada para un tipo de datos específico, lo que garantiza la organización y la eficiencia en el acceso a la información.

Columnas: Los campos de datos; dentro de una tabla hay columnas, que representan las diferentes categorías de información almacenadas. Cada columna equivale a un campo de datos y define un tipo específico de información. Por ejemplo, en una tabla de logs, podemos tener columnas como:

  • ID → Identificador único de registro
  • Marca de tiempo → Fecha y hora del registro
  • Nivel → Nivel de registro (DEPURACIÓN, INFORMACIÓN, ERROR...)
  • Mensaje → Mensaje de registro
  • Fuente → Origen del registro (qué sistema o módulo generó el registro)

Cada columna tiene una función bien definida. Por ejemplo, la marca de tiempo almacena fechas y horas, mientras que el mensaje contiene texto. Esta estructura evita redundancias y mejora el rendimiento de la búsqueda.

  • Filas: Los registros almacenados; si las columnas definen qué información se va a almacenar, las filas representan los registros individuales de la tabla. Cada fila contiene un conjunto completo de valores que rellenan las columnas correspondientes. Vea un ejemplo práctico de una tabla de logs:

    ID Marca de tiempo Nivel Mensaje Fuente
    1
     2025-02-12 10:15 DEPURACIÓN Valor calculado del indicador RSI: 72,56
    Indicadores
    2  2025-02-12 10:16 INFORMACIÓN Orden de compra enviada correctamente
    Gestión de órdenes
    3  2025-02-12 10:17 ALERTA Stop Loss ajustado al nivel de equilibrio
    Gestión de riesgos
    4  2025-02-12 10:18 ERROR No se pudo enviar la orden de venta
    Gestión de órdenes
    5  2025-02-12 10:19 FATAL Error al inicializar el EA: Configuración no válida
    Inicialización

    Cada línea constituye un registro individual que describe un evento específico.

Ahora que comprendemos la estructura de la base de datos, podemos explorar cómo aplicarla en la práctica dentro de MQL5 para almacenar y consultar registros de manera eficiente. Veámoslo en acción.


Bases de datos en MQL5

MQL5 permite almacenar y recuperar datos de forma estructurada, pero su compatibilidad con bases de datos tiene algunas peculiaridades que conviene comprender antes de pasar a la implementación.

A diferencia de los lenguajes destinados a aplicaciones web o corporativas, MQL5 no ofrece compatibilidad nativa con bases de datos relacionales robustas como MySQL o PostgreSQL. ¡Pero eso no significa que estemos obligados a usar archivos de texto! Podemos sortear esta limitación de dos maneras:

Utilizando SQLite, una base de datos ligera basada en archivos que es compatible de forma nativa con MQL5 (véanse las funciones de base de datos en MQL5), o estableciendo conexiones externas a través de API, lo que permite la integración con bases de datos más potentes. Para nuestro propósito de almacenar y consultar registros de manera eficiente, SQLite es la opción ideal. Es sencillo, rápido y no requiere un servidor dedicado, lo que lo hace perfecto para lo que necesitamos. Antes de pasar a la implementación, comprendamos mejor las características de una base de datos basada en un archivo .sqlite.

  • Ventajas
    • No requiere servidor: SQLite es una base de datos «integrada», lo que significa que no requiere la instalación ni la configuración de un servidor.
    • Listo para usar: Solo tienes que crear el archivo .sqlite y empezar a guardar datos.
    • Lectura rápida: Dado que se almacena en un único archivo, SQLite puede ser extremadamente rápido a la hora de leer datos de tamaño pequeño a mediano.
    • Baja latencia: En el caso de consultas sencillas, puede resultar más rápido que las bases de datos relacionales tradicionales.
    • Alta compatibilidad: Compatible con varios lenguajes de programación
  • Desventajas
    • Riesgo de corrupción del archivo: Si el archivo está dañado, recuperar los datos puede resultar complicado.
    • Copia de seguridad manual: La copia de seguridad debe realizarse copiando el archivo .sqlite, ya que SQLite no admite de forma nativa la replicación automática.
    • No se adapta bien a grandes volúmenes: Para grandes volúmenes de datos y accesos simultáneos, SQLite no es la mejor opción. Pero, dado que nuestro objetivo es almacenar los registros localmente, esto no supondrá ningún problema.

Ahora que conocemos las posibilidades y limitaciones de las bases de datos en MQL5, podemos profundizar en las operaciones básicas que tendremos que implementar para almacenar y recuperar registros de forma eficaz.


Los conceptos básicos de las operaciones con bases de datos que necesitamos

Antes de implementar nuestro controlador, debemos comprender las operaciones básicas que vamos a utilizar en la base de datos. Estas operaciones incluyen la creación de tablas, la inserción de nuevos registros, la recuperación de datos y, en su caso, la eliminación o actualización de registros cuando sea necesario.

En el ámbito del registro de eventos, normalmente necesitamos almacenar información como la fecha y la hora, el nivel de registro, el mensaje y, en su caso, el nombre del archivo o del componente que generó la entrada. Para ello, debemos estructurar nuestra tabla de manera que facilite la realización de consultas rápidas y eficientes.

En primer lugar, vamos a crear un asesor experto (EA) de prueba sencillo llamado DatabaseTest.mq5 dentro de la carpeta Experts/Logify. Una vez creado el archivo, tendremos algo parecido a esto:

//+------------------------------------------------------------------+
//|                                                 DatabaseTest.mq5 |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


Creación y conexión a la base de datos

El primer paso es crear la base de datos y conectarse a ella. Para ello, utilizamos la función DatabaseOpen(), que admite dos parámetros:

  • filename: Nombre del archivo respecto a la carpeta "MQL5\Files".
  • flags : Combinación de banderas de la enumeración ENUM_DATABASE_OPEN_FLAGS. Estas banderas determinan cómo se accederá a la base de datos. Las banderas disponibles son:
    • DATABASE_OPEN_READONLY - Acceso de solo lectura.
    • DATABASE_OPEN_READWRITE - Permite leer y escribir.
    • DATABASE_OPEN_CREATE - Crea la base de datos en el disco si no existe.
    • DATABASE_OPEN_MEMORY - Crea una base de datos temporal en memoria.
    • DATABASE_OPEN_COMMON - El archivo se almacenará en la carpeta común a todos los terminales.

Para nuestro ejemplo, utilizaremos DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE. De esta forma, garantizamos que la base de datos se creará automáticamente si aún no existe, evitando comprobaciones manuales.

La función DatabaseOpen() devuelve un identificador de base de datos, que guardamos en una variable para utilizarlo en operaciones posteriores. Además, es imprescindible cerrar la conexión al finalizar su uso, lo cual hacemos mediante la función DatabaseClose().

Nuestro código ahora se ve así:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Opening a database connection
   int dbHandle = DatabaseOpen(path,DATABASE_OPEN_READWRITE|DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Database error (Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   Print("Open database file");
   
   //--- Closing database after use
   DatabaseClose(handle_db);
   Print("Closed database file");
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Ahora que hemos logrado abrir y cerrar la base de datos, es hora de estructurar los datos almacenados. Comencemos creando nuestra primera tabla: logs.


Crear una tabla

Pero antes de empezar a crear tablas sin ningún criterio, debemos comprobar si ya existen. Para ello, utilizamos la función DatabaseTableExists(). Si la tabla no existe en la base de datos, la creamos con un sencillo comando SQL. Hablando de SQL (Structured Query Language), este es el lenguaje que se utiliza para interactuar con las bases de datos, lo que permite insertar, consultar, modificar o eliminar datos. Piensa en SQL como una especie de "menú de restaurante" para bases de datos: haces un pedido (consulta SQL) y recibes exactamente lo que pediste, ¡siempre y cuando el pedido esté bien formulado, por supuesto!

Ahora, veremos esto en la práctica y estructuraremos nuestra tabla de registros, asegurándonos de que se cree correctamente siempre que sea necesario.

Para lo que nos ocupa, solo necesitamos conocer unos pocos comandos SQL; el primero de ellos sirve para crear una tabla:

CREATE TABLE {table_name} ({column_name} {type_data}, …);
  • {table_name}: Name of the table to be created.
  • {column_name} {type_data}: Definition of the columns, where {type_data} indicates the data type (text, number, date, etc.).

Ahora utilizaremos la función DatabaseExecute() para ejecutar el comando de creación de la tabla. La estructura de la tabla se basará en la estructura MqlLogifyModel y contendrá los siguientes campos:

  • id: Identificador único de la fila.
  • formated: Mensaje con formato.
  • levelname: Nombre del nivel de registro.
  • msg: Mensaje original.
  • args: Argumentos del mensaje.
  • timestamp: Fecha y hora en formato numérico.
  • date_time: Fecha y hora con formato.
  • level: Nivel de gravedad del registro.
  • origin: Fuente del registro.
  • filename: Nombre del archivo de origen.
  • function: Función en la que se generó el registro.
  • line: Línea de código en la que se generó el registro.

Nuestro código ahora se ve así:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return(INIT_FAILED);
     }
   Print("[INFO] Database connection opened successfully");
   
   //--- Create the 'logs' table if it does not exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      DatabaseExecute(dbHandle,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID
         "formated TEXT,"     // Formatted log message
         "levelname TEXT,"    // Log level (INFO, ERROR, etc.)
         "msg TEXT,"          // Main log message
         "args TEXT,"         // Additional details
         "timestamp BIGINT,"  // Log event timestamp (Unix time)
         "date_time DATETIME,"// Human-readable date and time
         "level BIGINT,"      // Log level as an integer
         "origin TEXT,"       // Module or component name
         "filename TEXT,"     // Source file name
         "function TEXT,"     // Function where the log was recorded
         "line BIGINT);");    // Source code line number
      Print("[INFO] 'logs' table created successfully");
     }
   
   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Con esto queda completado el proceso de creación de la base de datos y de la tabla «logs». Una vez creada la tabla, el archivo de la base de datos debería aparecer en la carpeta «Files» del explorador de archivos:

Al hacer clic en el archivo, si el MetaEditor es compatible con él, debería abrirse en una pantalla similar a esta:

Aquí tenemos una interfaz donde podemos ver los datos de la base de datos y ejecutar diferentes comandos SQL, como se muestra resaltado en rojo. Utilizaremos mucho esta función para visualizar los datos en el editor.


Cómo insertar datos en la base de datos

En SQL, el comando utilizado para insertar datos en una tabla es:

INSERT INTO {table_name} ({column}, ...) VALUES ({value}, ...)

En el contexto de MQL5, esta afirmación se puede simplificar con la ayuda de funciones específicas que hacen que el proceso sea más intuitivo y menos propenso a errores. Las funciones principales que utilizará son:

  • DatabasePrepare() - Esta función crea un identificador para la consulta SQL, preparándola para su posterior ejecución. Constituye el primer paso para que la base de datos interprete la consulta.
  • DatabaseBind() - Con esta función, se asocian valores reales a los parámetros de la consulta. En el comando SQL, los valores se representan mediante marcadores de posición (por ejemplo, ?1, ?2, etc.), que se sustituirán por los datos proporcionados en el momento de la ejecución.
  • DatabaseRead() - Se encarga de ejecutar la consulta preparada. En el caso de los comandos que no devuelven registros (como INSERT), esta función garantiza la ejecución de la instrucción y el paso al siguiente registro, si es necesario.
  • DatabaseFinalize() - Una vez finalizado su uso, es importante liberar los recursos asociados a la consulta. Esta función cierra la consulta preparada anteriormente, evitando así fugas de memoria.

Al crear la consulta para la inserción de datos, podemos utilizar marcadores de posición para indicar dónde se insertarán los valores posteriormente. Veamos el siguiente ejemplo, que inserta un nuevo registro en la tabla «logs», siguiendo las columnas que hemos creado anteriormente:

INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);

Ten en cuenta que se han incluido todos los campos de la tabla, excepto el campo «id», que es generado automáticamente por la base de datos. Además, los valores que se van a insertar se indican mediante ?1, ?2, etc.; cada marcador de posición corresponde a un índice que se utilizará posteriormente para asociar el valor real mediante la función DatabaseBind().

//--- Prepare SQL statement for inserting a log entry
string sql = "INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) "
             "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);";
int sqlRequest = DatabasePrepare(dbHandle, sql);
if(sqlRequest == INVALID_HANDLE)
  {
   Print("[ERROR] Failed to prepare SQL statement for log insertion");
  }

//--- Bind values to the SQL statement
DatabaseBind(sqlRequest, 0, "06:24:00 [INFO] Buy order sent successfully"); // Formatted log message
DatabaseBind(sqlRequest, 1, "INFO");                                        // Log level name
DatabaseBind(sqlRequest, 2, "Buy order sent successfully");                 // Main log message
DatabaseBind(sqlRequest, 3, "Symbol: EURUSD, Volume: 0.1");                  // Additional details
DatabaseBind(sqlRequest, 4, 1739471040);                                     // Unix timestamp
DatabaseBind(sqlRequest, 5, "2025.02.13 18:24:00");                          // Readable date and time
DatabaseBind(sqlRequest, 6, 1);                                              // Log level as integer
DatabaseBind(sqlRequest, 7, "Order Management");                             // Module or component name
DatabaseBind(sqlRequest, 8, "File.mq5");                                     // Source file name
DatabaseBind(sqlRequest, 9, "OnInit");                                       // Function name
DatabaseBind(sqlRequest, 10, 100);                                           // Line number
Una vez asignados todos los valores, se utiliza la función DatabaseRead() para ejecutar la consulta preparada. Si la ejecución se realiza correctamente, se muestra un mensaje de confirmación; en caso contrario, se notifica un error:
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return(INIT_FAILED);
     }
   Print("[INFO] Database connection opened successfully");
   
   //--- Create the 'logs' table if it does not exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      DatabaseExecute(dbHandle,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID
         "formated TEXT,"     // Formatted log message
         "levelname TEXT,"    // Log level (INFO, ERROR, etc.)
         "msg TEXT,"          // Main log message
         "args TEXT,"         // Additional details
         "timestamp BIGINT,"  // Log event timestamp (Unix time)
         "date_time DATETIME,"// Human-readable date and time
         "level BIGINT,"      // Log level as an integer
         "origin TEXT,"       // Module or component name
         "filename TEXT,"     // Source file name
         "function TEXT,"     // Function where the log was recorded
         "line BIGINT);");    // Source code line number
      Print("[INFO] 'logs' table created successfully");
     }
   
   //--- Prepare SQL statement for inserting a log entry
   string sql = "INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) "
                "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);";
   int sqlRequest = DatabasePrepare(dbHandle, sql);
   if(sqlRequest == INVALID_HANDLE)
     {
      Print("[ERROR] Failed to prepare SQL statement for log insertion");
     }
   
   //--- Bind values to the SQL statement
   DatabaseBind(sqlRequest, 0, "06:24:00 [INFO] Buy order sent successfully"); // Formatted log message
   DatabaseBind(sqlRequest, 1, "INFO");                                        // Log level name
   DatabaseBind(sqlRequest, 2, "Buy order sent successfully");                 // Main log message
   DatabaseBind(sqlRequest, 3, "Symbol: EURUSD, Volume: 0.1");                  // Additional details
   DatabaseBind(sqlRequest, 4, 1739471040);                                     // Unix timestamp
   DatabaseBind(sqlRequest, 5, "2025.02.13 18:24:00");                          // Readable date and time
   DatabaseBind(sqlRequest, 6, 1);                                              // Log level as integer
   DatabaseBind(sqlRequest, 7, "Order Management");                             // Module or component name
   DatabaseBind(sqlRequest, 8, "File.mq5");                                     // Source file name
   DatabaseBind(sqlRequest, 9, "OnInit");                                       // Function name
   DatabaseBind(sqlRequest, 10, 100);                                           // Line number
   
   //--- Execute the SQL statement
   if(!DatabaseRead(sqlRequest))
     {
      Print("[ERROR] SQL insertion request failed");
     }
   else
     {
      Print("[INFO] Log entry inserted successfully");
     }
   
   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
Al ejecutar este asesor experto, aparecerán los siguientes mensajes en la consola:
[INFO] Database file opened successfully
[INFO] Table 'logs' created successfully
[INFO] Log entry inserted successfully
[INFO] Database file closed successfully

Además, al abrir la base de datos en el editor, podrás ver la tabla de registros con todos los datos introducidos, tal y como se muestra en la imagen siguiente:



Cómo leer datos de la base de datos

La lectura de datos de una base de datos implica un proceso muy similar a la inserción de registros, pero con el objetivo de recuperar información ya almacenada. En MQL5, el proceso básico para leer datos consiste en:

  1. Prepara la consulta SQL: Mediante la función DatabasePrepare(), se crea un identificador para la consulta que se va a ejecutar.
  2. Ejecuta la consulta: Con el identificador preparado, la función DatabaseRead() ejecuta la consulta y coloca el cursor en el primer registro del resultado.
  3. Extraer los datos: A partir del registro actual, se utilizan funciones específicas para obtener los valores de cada columna, según el tipo de datos esperado. Entre estas funciones se incluyen:

Con estos pasos, dispondrás de un proceso sencillo y eficaz para recuperar información y utilizarla según las necesidades de tu aplicación.

Por ejemplo, supongamos que quieres recuperar todos los registros de la tabla «logs». La consulta SQL para esta operación es bastante sencilla:

SELECT * FROM logs

Esta consulta selecciona todas las columnas de todos los registros de la tabla. En MQL5, utilizamos la función DatabasePrepare() para crear el identificador de consulta, tal y como hicimos al insertar datos.

Al final, el código queda así:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return INIT_FAILED;
     }
   Print("[INFO] Database connection opened successfully.");

   //--- Create the 'logs' table if it doesn't exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      string createTableSQL =
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT,"    // Auto-incrementing unique ID
         "formated TEXT,"                           // Formatted log message
         "levelname TEXT,"                          // Log level name (INFO, ERROR, etc.)
         "msg TEXT,"                                // Main log message
         "args TEXT,"                               // Additional arguments/details
         "timestamp BIGINT,"                        // Timestamp of the log event
         "date_time DATETIME,"                      // Human-readable date and time
         "level BIGINT,"                            // Log level as an integer
         "origin TEXT,"                             // Module or component name
         "filename TEXT,"                           // Source file name
         "function TEXT,"                           // Function where the log was recorded
         "line BIGINT);";                           // Line number in the source code

      DatabaseExecute(dbHandle, createTableSQL);
      Print("[INFO] 'logs' table created successfully.");
     }

   //--- Prepare SQL statement to retrieve log entries
   string sqlQuery = "SELECT * FROM logs";
   int sqlRequest = DatabasePrepare(dbHandle, sqlQuery);
   if(sqlRequest == INVALID_HANDLE)
     {
      Print("[ERROR] Failed to prepare SQL statement.");
     }

   //--- Execute the SQL statement
   if(!DatabaseRead(sqlRequest))
     {
      Print("[ERROR] SQL query execution failed.");
     }
   else
     {
      Print("[INFO] SQL query executed successfully.");

      //--- Bind SQL query results to the log data model
      MqlLogifyModel logData;
      DatabaseColumnText(sqlRequest, 1, logData.formated);
      DatabaseColumnText(sqlRequest, 2, logData.levelname);
      DatabaseColumnText(sqlRequest, 3, logData.msg);
      DatabaseColumnText(sqlRequest, 4, logData.args);
      DatabaseColumnLong(sqlRequest, 5, logData.timestamp);

      string dateTimeStr;
      DatabaseColumnText(sqlRequest, 6, dateTimeStr);
      logData.date_time = StringToTime(dateTimeStr);

      DatabaseColumnInteger(sqlRequest, 7, logData.level);
      DatabaseColumnText(sqlRequest, 8, logData.origin);
      DatabaseColumnText(sqlRequest, 9, logData.filename);
      DatabaseColumnText(sqlRequest, 10, logData.function);
      DatabaseColumnLong(sqlRequest, 11, logData.line);

      Print("[INFO] Data retrieved: Formatted = ", logData.formated, " | Level = ", logData.level, " | Origin = ", logData.origin);
     }

   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully.");

   return INIT_SUCCEEDED;
  }
//+------------------------------------------------------------------+

Bien, dicho esto, al ejecutar el código obtenemos este resultado

[INFO] Database file opened successfully
[INFO] SQL request successfully
[INFO] Data read! | Formated: 06:24:00 [INFO] Buy order sent successfully | Level: 1 | Origin: Order Management
[INFO] Database file closed successfully

Teniendo en cuenta estas operaciones básicas, estamos listos para configurar nuestro gestor de base de datos. Vamos a preparar el entorno necesario para integrar la base de datos con nuestra biblioteca de registro de eventos.


Configuración del controlador de la base de datos

Para poder utilizar una base de datos para almacenar registros, necesitamos configurar correctamente nuestro controlador. Esto implica definir los atributos de la estructura de configuración, de forma similar a como lo hicimos con el gestor de archivos. Vamos a crear una estructura de configuración llamada “MqlLogifyHandleDatabaseConfig”, copiar esa estructura y realizar algunos cambios:

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

He marcado en rojo los atributos como rotación, tipo de archivo, número máximo de archivos, modo de codificación, entre otros, que se eliminarán, ya que no tienen sentido en el contexto de la base de datos. Con los atributos definidos, ajustaremos el método ValidityConfig(), al final el código se verá así:

//+------------------------------------------------------------------+
//| Struct: MqlLogifyHandleDatabaseConfig                            |
//+------------------------------------------------------------------+
struct MqlLogifyHandleDatabaseConfig
  {
   string directory;                         // Directory for log files
   string base_filename;                     // Base file name
   int messages_per_flush;                   // Messages before flushing
   
   //--- Default constructor
   MqlLogifyHandleDatabaseConfig(void)
     {
      directory = "logs";                    // Default directory
      base_filename = "expert";              // Default base name
      messages_per_flush = 100;              // Default flush threshold
     }
   
   //--- Destructor
   ~MqlLogifyHandleDatabaseConfig(void)
     {
     }

   //--- Validate configuration
   bool ValidateConfig(string &error_message)
     {
      //--- Saves the return value
      bool is_valid = true;
      
      //--- Check if the directory is not empty
      if(directory == "")
        {
         directory = "logs";
         error_message = "The directory cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the base filename is not empty
      if(base_filename == "")
        {
         base_filename = "expert";
         error_message = "The base filename cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the number of messages per flush is positive
      if(messages_per_flush <= 0)
        {
         messages_per_flush = 100;
         error_message = "The number of messages per flush must be greater than zero.";
         is_valid = false;
        }
   
      //--- No errors found
      return(is_valid);
     }
  };

Una vez lista la configuración, por fin podemos empezar a implementar el controlador.


Implementación del controlador de base de datos

Ahora que hemos estructurado nuestra configuración, pasemos a la parte práctica: la implementación del controlador de la base de datos. Detallaré cada parte de la implementación, explicando las decisiones tomadas y asegurándome de que el controlador sea flexible para futuras mejoras.

Comenzamos definiendo la clase CLogifyHandlerDatabase, que extiende CLogifyHandler. Esta clase necesita almacenar la configuración del controlador, una utilidad de control de tiempo (CInvalWatcher) y una caché de mensajes de registro. Esta caché sirve para evitar la escritura excesiva en la base de datos, almacenando temporalmente los mensajes antes de escribirlos.

class CLogifyHandlerDatabase : public CLogifyHandler
  {
private:
   //--- Config
   MqlLogifyHandleDatabaseConfig m_config;
   
   //--- Update utilities
   CIntervalWatcher  m_interval_watcher;
   
   //--- Cache data
   MqlLogifyModel    m_cache[];
   int               m_index_cache;
   
public:
                     CLogifyHandlerDatabase(void);
                    ~CLogifyHandlerDatabase(void);
   
   //--- Configuration management
   void              SetConfig(MqlLogifyHandleDatabaseConfig &config);
   MqlLogifyHandleDatabaseConfig GetConfig(void);
   
   virtual void      Emit(MqlLogifyModel &data);         // Processes a log message and sends it to the specified destination
   virtual void      Flush(void);                        // Clears or completes any pending operations
   virtual void      Close(void);                        // Closes the handler and releases any resources
  };

El constructor inicializa los atributos, asegurándose de que el nombre del controlador sea "database", establece un intervalo para m_interval_watcher y borra la caché. En el destructor, llamamos a Close(), asegurándonos de que todos los registros pendientes se escriban antes de finalizar el objeto.

Otro método importante es SetConfig(), que permite configurar el controlador, almacenar la configuración y validarla para garantizar que no haya errores. El método GetConfig() simplemente devuelve la configuración actual.

CLogifyHandlerDatabase::CLogifyHandlerDatabase(void)
  {
   m_name = "database";
   m_interval_watcher.SetInterval(PERIOD_D1);
   ArrayFree(m_cache);
   m_index_cache = 0;
  }
CLogifyHandlerDatabase::~CLogifyHandlerDatabase(void)
  {
   this.Close();
  }
void CLogifyHandlerDatabase::SetConfig(MqlLogifyHandleDatabaseConfig &config)
  {
   m_config = config;
   
   string err_msg = "";
   if(!m_config.ValidateConfig(err_msg))
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg);
     }
  }
MqlLogifyHandleDatabaseConfig CLogifyHandlerDatabase::GetConfig(void)
  {
   return(m_config);
  }

Ahora vamos a llegar al meollo del asunto del gestor de la base de datos: guardar directamente los registros de log. Para ello, implementaremos los tres métodos básicos de cada manejador:

  • Emit(MqlLogifyModel &data): Procesa un mensaje de registro y lo envía a la caché.
  • Flush(): Finaliza o borra cualquier operación agregando información al destino correcto (archivo, consola, base de datos, etc.).
  • Close(): Cierra el controlador y libera los recursos asociados.

Comenzando con el método Emit(), que es responsable de agregar los datos a la caché, y si ha alcanzado el límite definido, llama a Flush().

//+------------------------------------------------------------------+
//| Processes a log message and sends it to the specified destination|
//+------------------------------------------------------------------+
void CLogifyHandlerDatabase::Emit(MqlLogifyModel &data)
  {
   //--- Checks if the configured level allows
   if(data.level >= this.GetLevel())
     {
      //--- Resize cache if necessary
      int size = ArraySize(m_cache);
      if(size != m_config.messages_per_flush)
        {
         ArrayResize(m_cache, m_config.messages_per_flush);
         size = m_config.messages_per_flush;
        }
      
      //--- Add log to cache
      m_cache[m_index_cache++] = data;
      
      //--- Flush if cache limit is reached or update condition is met
      if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect())
        {
         //--- Save cache
         Flush();
         
         //--- Reset cache
         m_index_cache = 0;
         for(int i=0;i<size;i++)
           {
            m_cache[i].Reset();
           }
        }
     }
  }
//+------------------------------------------------------------------+

Continuando con el método Flush(), leemos los datos de la caché y los añadimos a la base de datos, siguiendo la misma estructura que expliqué al principio del artículo, en la sección «Cómo insertar datos en la base de datos», utilizando la función DatabasePrepare().

//+------------------------------------------------------------------+
//| Clears or completes any pending operations                       |
//+------------------------------------------------------------------+
void CLogifyHandlerDatabase::Flush(void)
  {
   //--- Get the full path of the file
   string path = m_config.directory+"\\"+m_config.base_filename+".sqlite";
   
   //--- Open database
   ResetLastError();
   int handle_db = DatabaseOpen(path,DATABASE_OPEN_CREATE|DATABASE_OPEN_READWRITE);
   if(handle_db == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+path+"'. Asegúrate de que el directorio existe y de que se puede escribir en él. (Code: "+IntegerToString(GetLastError())+")");
      return;
     }
   
   if(!DatabaseTableExists(handle_db,"logs"))
     {
      DatabaseExecute(handle_db,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT,"
         "formated TEXT,"
         "levelname TEXT,"
         "msg TEXT,"
         "args TEXT,"
         "timestamp BIGINT,"
         "date_time DATETIME,"
         "level BIGINT,"
         "origin TEXT,"
         "filename TEXT,"
         "function TEXT,"
         "line BIGINT);");
     }
   
   //--- 
   string sql="INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);"; // parâmetro de consulta
   int request = DatabasePrepare(handle_db,sql);
   if(request == INVALID_HANDLE)
     {
      Print("Erro");
     }
   
   //--- Loop through all cached messages
   int size = ArraySize(m_cache);
   for(int i=0;i<size;i++)
     {
      if(m_cache[i].timestamp > 0)
        {
         DatabaseBind(request,0,m_cache[i].formated);
         DatabaseBind(request,1,m_cache[i].levelname);
         DatabaseBind(request,2,m_cache[i].msg);
         DatabaseBind(request,3,m_cache[i].args);
         DatabaseBind(request,4,m_cache[i].timestamp);
         DatabaseBind(request,5,TimeToString(m_cache[i].date_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS));
         DatabaseBind(request,6,(int)m_cache[i].level);
         DatabaseBind(request,7,m_cache[i].origin);
         DatabaseBind(request,8,m_cache[i].filename);
         DatabaseBind(request,9,m_cache[i].function);
         DatabaseBind(request,10,m_cache[i].line);
         DatabaseRead(request);
         DatabaseReset(request);
        }
     }
   
   //--- 
   DatabaseFinalize(request);
   
   //--- Close database
   DatabaseClose(handle_db);
  }
//+------------------------------------------------------------------+

Por último, Close() garantiza que todos los registros pendientes se escriban antes de salir.

void CLogifyHandlerDatabase::Close(void)
  {
   Flush();
  }

Con esto, hemos implementado un gestor robusto que garantiza que los registros se almacenen de forma eficiente y sin pérdida de datos. Ahora que ya tenemos nuestro gestor de bases de datos listo para registrar los registros, el siguiente paso es crear métodos eficaces para consultar dichos registros. La idea es disponer de un método base genérico, denominado Query(), que reciba un comando SQL en formato de cadena y devuelva los datos en una matriz de tipo MqlLogifyModel. A partir de ahí, podemos crear métodos específicos para facilitar las consultas recurrentes. Nuestro método Query() se encargará de abrir la base de datos, ejecutar la consulta y almacenar los resultados en la estructura de registro; véase la implementación a continuación:

class CLogifyHandlerDatabase : public CLogifyHandler
  {
public:
   //--- Query methods
   bool              Query(string query, MqlLogifyModel &data[]);
  };
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- Get the full path of the file
   string path = m_config.directory+"\\"+m_config.base_filename+".sqlite";
   
   //--- Open database
   ResetLastError();
   int handle_db = DatabaseOpen(path,DATABASE_OPEN_READWRITE);
   if(handle_db == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+path+"'. Asegúrate de que el directorio existe y de que se puede escribir en él. (Code: "+IntegerToString(GetLastError())+")");
      return(false);
     }
   
   //--- Prepare the SQL query
   int request = DatabasePrepare(handle_db,query);
   if(request == INVALID_HANDLE)
     {
      Print("Erro query");
      return(false);
     }
   
   //--- Clears array before inserting new data
   ArrayFree(data);
   
   //--- 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);
     }
   
   //--- Ends the query and closes the database
   DatabaseFinalize(handle_db);
   DatabaseClose(handle_db);
   return(true);
  }
//+------------------------------------------------------------------+

Este método nos ofrece total flexibilidad para realizar cualquier consulta SQL en la base de datos de registros. Sin embargo, para facilitar su uso, crearemos métodos auxiliares que engloben las consultas más habituales.

Para evitar que los desarrolladores tengan que escribir código SQL cada vez que quieran consultar los registros, he creado métodos que ya incluyen los comandos SQL más habituales. Sirven como atajos para buscar en los registros filtrando por nivel de gravedad, fecha, origen, mensaje, argumentos, nombre de archivo y nombre de función. A continuación se muestran los comandos SQL correspondientes a cada uno de estos filtros:

SELECT * FROM 'logs' WHERE level=1;
SELECT * FROM 'logs' WHERE timestamp BETWEEN '{start_time}' AND '{stop_time}';
SELECT * FROM 'logs' WHERE origin LIKE '%{origin}%';
SELECT * FROM 'logs' WHERE msg LIKE '%{msg}%';
SELECT * FROM 'logs' WHERE args LIKE '%{args}%';
SELECT * FROM 'logs' WHERE filename LIKE '%{filename}%';
SELECT * FROM 'logs' WHERE function LIKE '%{function}%';

Ahora implementamos los métodos específicos que utilizan estos comandos:

class CLogifyHandlerDatabase : public CLogifyHandler
  {
public:
   //--- Query methods
   bool              Query(string query, MqlLogifyModel &data[]);
   bool              QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[]);
   bool              QueryByDate(datetime start_time, datetime stop_time, MqlLogifyModel &data[]);
   bool              QueryByOrigin(string origin, MqlLogifyModel &data[]);
   bool              QueryByMsg(string msg, MqlLogifyModel &data[]);
   bool              QueryByArgs(string args, MqlLogifyModel &data[]);
   bool              QueryByFile(string file, MqlLogifyModel &data[]);
   bool              QueryByFunction(string function, MqlLogifyModel &data[]);
  };
//+------------------------------------------------------------------+
//| Get logs filtering by level                                      |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE level="+IntegerToString(level)+";",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by start end stop time                        |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByDate(datetime start_time, datetime stop_time, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE timestamp BETWEEN '"+IntegerToString((ulong)start_time)+"' AND '"+IntegerToString((ulong)stop_time)+"';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by origin                                     |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByOrigin(string origin, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE origin LIKE '%"+origin+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by message                                    |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByMsg(string msg, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE msg LIKE '%"+msg+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by args                                       |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByArgs(string args, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE args LIKE '%"+args+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by file name                                  |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByFile(string file, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE filename LIKE '%"+file+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by function name                              |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByFunction(string function, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE function LIKE '%"+function+"%';",data));
  }
//+------------------------------------------------------------------+

Ahora disponemos de un conjunto de métodos eficaces y flexibles para acceder a los registros directamente desde la base de datos. El método Query() permite ejecutar cualquier comando SQL, incluso comandos más complejos con más filtros según las necesidades específicas, mientras que los métodos auxiliares encapsulan consultas comunes, lo que hace que su uso sea más intuitivo y reduce los errores.

Ahora que ya hemos implementado nuestro controlador, es hora de comprobar si todo funciona correctamente. Visualicemos los resultados y comprobemos que los registros se almacenan y recuperan según lo previsto.


Visualización del resultado

Tras implementar el controlador, el siguiente paso es verificar si funciona como se espera. Necesitamos probar la inserción de registros, validar que los registros se almacenen correctamente en la base de datos y asegurarnos de que las consultas sean rápidas y precisas.

En las pruebas, usaré el mismo archivo LogifyTest.mq5 y simplemente agregaré algunos mensajes de registro al principio. También añadiremos algunas operaciones sin una estrategia compleja, simplemente abriremos una posición si no hay ninguna abierta, definiendo el take profit y el stop loss en la posición para realizar la salida.

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
#include <Trade/Trade.mqh>
CLogify logify;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleDatabaseConfig m_config;
   m_config.directory = "db";
   m_config.base_filename = "logs";
   m_config.messages_per_flush = 5;
   
   //--- Handler Database
   CLogifyHandlerDatabase *handler_database = new CLogifyHandlerDatabase();
   handler_database.SetConfig(m_config);
   handler_database.SetLevel(LOG_LEVEL_DEBUG);
   handler_database.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Add handler in base class
   logify.AddHandler(handler_database);
   
   //--- Using logs
   logify.Info("Expert starting successfully", "Boot", "",__FILE__,__FUNCTION__,__LINE__);
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- No positions
   if(PositionsTotal() == 0)
     {
      double price_entry = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double volume = 1;
      if(trade.Buy(volume,_Symbol,price_entry,price_entry - 100 * _Point, price_entry + 100 * _Point,"Buy at market"))
        {
         logify.Debug("Transaction data | Price: "+DoubleToString(price_entry,_Digits)+" | Symbol: "+_Symbol+" | Volume: "+DoubleToString(volume,2), "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
         logify.Info("Purchase order sent successfully", "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
        }
      else
        {
         logify.Debug("Error code: "+IntegerToString(trade.ResultRetcode(),_Digits)+" | Description: "+trade.ResultRetcodeDescription(), "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
         logify.Error("Failed to send purchase order", "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
        }
     }
  }
//+------------------------------------------------------------------+

Al probar el probador de estrategias durante 1 día en EURUSD, fue suficiente para generar 909 registros de log. Tal como lo configuramos, se guardaron en el archivo .sqlite. Para acceder a ellos, simplemente acceda a la carpeta de la terminal o pulse “Ctrl/Cmd + Shift + D” y aparecerá el explorador de archivos. Siga la ruta “MQL5/Files/db/logs.sqlite”. Con el archivo en mano, podemos abrirlo directamente en el MetaEditor, como hicimos anteriormente:


Esto supone un nuevo avance en nuestra biblioteca de registros. Ahora, nuestros registros se pueden almacenar y recuperar de forma eficiente en una base de datos, lo que proporciona una mayor escalabilidad y organización.


Conclusión

A lo largo de este artículo, hemos explorado la integración de bases de datos en nuestra biblioteca de registros, desde los conceptos fundamentales hasta la implementación práctica de un controlador específico. En primer lugar, hablamos de la importancia de las bases de datos como una alternativa más escalable y estructurada para almacenar registros, destacando sus ventajas sobre los archivos de texto convencionales. A continuación, examinamos las particularidades del uso de bases de datos en el contexto de MQL5, abordando sus limitaciones y las soluciones disponibles para superarlas.

Finalmente, analizamos los resultados de nuestra implementación, asegurándonos de que los registros se almacenaran correctamente y se pudiera acceder a ellos de forma rápida y eficiente. Además, hablamos sobre cómo visualizar estos registros, ya sea mediante consultas directas a la base de datos o mediante herramientas específicas para la monitorización de registros. Este proceso de validación fue esencial para garantizar que la solución implementada fuera funcional y eficaz en escenarios del mundo real.

Con esto, hemos completado otra etapa en el desarrollo de nuestra biblioteca de registros. La adopción de bases de datos para almacenar registros ha aportado beneficios significativos, haciendo que la gestión de registros sea más organizada, accesible y escalable. Este enfoque nos permite gestionar grandes volúmenes de datos de forma más eficiente, además de facilitar el análisis y la monitorización de la información registrada por el sistema.

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

Archivos adjuntos |
LogifyePart6p.zip (22.67 KB)
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.
Asesor de autoaprendizaje con red neuronal basada en matriz de estados Asesor de autoaprendizaje con red neuronal basada en matriz de estados
Asesor de autoaprendizaje con red neuronal basada en matriz de estados. Hoy combinaremos cadenas de Márkov con una red neuronal multicapa MLP, escrita en la biblioteca ALGLIB MQL5. ¿Cómo podemos combinar las cadenas de Márkov y las redes neuronales para realizar previsiones en Forex?
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.
Creación de un Panel de administración de operaciones en MQL5 (Parte IX): Organización del código (IV): Clase sobre el Panel de gestión de operaciones Creación de un Panel de administración de operaciones en MQL5 (Parte IX): Organización del código (IV): Clase sobre el Panel de gestión de operaciones
Esta discusión trata sobre el TradeManagementPanel actualizado en nuestro asesor experto New_Admin_Panel. La actualización mejora el panel mediante el uso de clases integradas para ofrecer una interfaz de gestión de operaciones fácil de usar. Incluye botones para abrir posiciones y controles para gestionar las operaciones existentes y las órdenes pendientes. Una característica clave es la gestión de riesgos integrada, que permite establecer los valores de stop loss y take profit directamente en la interfaz. Esta actualización mejora la organización del código para programas grandes y simplifica el acceso a las herramientas de gestión de pedidos, que a menudo son complejas en la terminal.