Gestión de errores y logging en MQL5

Sergey Eremin | 17 febrero, 2016

Introducción

Durante la ejecución de los programas pueden suceder errores de vez en cuando. Gestionarlos bien contribuye a mejorar la calidad del programa, y también hace que se pueda mantener más fácilmente. Este artículo explica varios métodos de gestión de errores, proporciona algunas recomendaciones, y enseña a utilizar la herramienta de logging de MQL5.

El tema de la gestión de los errores es relativamente complicado y controvertido. Hay muchas formas de arreglar los errores, cada una de las cuales tiene sus ventajas y sus inconvenientes. Algunos de estos métodos pueden utilizarse juntos; no hay ninguna fórmula universal, cada tarea requiere una aproximación adecuada.


Métodos básicos para gestionar los errores

Si un programa detecta algún error mientras se encuentra operando, entonces debería llevar a cabo alguna acción, o acciones, que permitan garantizar su correcto funcionamiento. Estos son algunos ejemplos de tales acciones:

Parar el programa. Si se producen errores, lo más apropiado es parar el programa en ejecución. Normalmente, estos errores críticos son peligrosos porque pueden paralizar el programa. Por ello, MQL5 proporciona un mecanismo de interrupción para controlar los errores en tiempo de ejecución; por ejemplo, el programa se para si se produce alguna división por cero o un array fuera de rango. En otros casos de terminación del programa, el mismo desarrollador debe tratar los errores. En los Asesores Expertos hay que utilizar la función ExpertRemove():

Ejemplo de parada de un Asesor Experto con la función ExpertRemove():

void OnTick()
  {
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      Alert("fallo");
      ExpertRemove();
      return;
     }
  }


Convertimos los valores incorrectos al rango de valores correctos. Generalmente, un valor determinado tiene que caer dentro del rango especificado. Sin embargo en algunos casos pueden aparecer valores fuera del rango. En cuyo caso se puede forzar que el valor vuelva al límite aceptable. Utilicemos como ejemplo el cálculo del volumen de una posición abierta. Si el volumen resultante está fuera de los valores máximo y mínimo disponibles, podemos forzarlo a que vuelva dentro de estos límites:

Ejemplo de conversión de valores incorrectos al rango del valor correcto

double VolumeCalculation()
  {
   double result=...;

   result=MathMax(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN));
   result=MathMin(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX));

   return result;
  }

No obstante, si por alguna razón el volumen resulta ser mayor que el límite máximo y el depósito no puede soportar tal carga, entonces se recomienda abortar la ejecución del programa. Este error en particular suele representar una amenaza para la cuenta.


Devolver un valor de error. En este caso, si se produce un error, se implementa un método o función que devuelve un valor predeterminado para indicar el error. Por ejemplo, si nuestro método o función devuelve un string, entonces devolvemos el valor NULL en caso de error.

Ejemplo de devolución de un valor de error:

#define SOME_STR_FUNCTION_FAIL_RESULT (NULL)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      return SOME_STR_FUNCTION_FAIL_RESULT;
     }
   return result;
  }

void OnTick()
  {
   string someStr=SomeStrFunction();

   if(someStr==SOME_STR_FUNCTION_FAIL_RESULT)
     {
      Print("fallo");
      return;
     }
  }

Pero esta aproximación puede derivar en errores de programación. Si la acción no está documentada, o si el programador no está familiarizado con la implementación del código, entonces no será consciente del valor del error. Pueden presentarse algunos problemas si la función/método puede devolver casi cualquier valor en el modo normal de operación, incluyendo algún error.


Asignar el resultado de la ejecución a una variable global especial. Esta aproximación suele aplicarse en los métodos y funciones que no devuelven ningún valor. La idea es asignar el resultado del método o función a una variable global determinada, y entonces el código de llamada comprueba el valor de la variable. Este es el propósito de la función predeterminada SetUserError() de MQL5.

Ejemplo de asignación del código de error con SetUserError()

#define SOME_STR_FUNCTION_FAIL_CODE (123)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE);
      return "";
     }
   return result;
  }

void OnTick()
  {
   ResetLastError();
   string someStr=SomeStrFunction();

   if(GetLastError()==ERR_USER_ERROR_FIRST+SOME_STR_FUNCTION_FAIL_CODE)
     {
      Print("fallo");
      return;
     }
  }

En este caso puede que el programador no se de cuenta de los errores, sin embargo, este enfoque permite no solo informar del error, sino también indicar el código específico. Lo que es particularmente importante si el error tiene varias fuentes.


Devolver el resultado de la ejecución como booleano y el valor resultante como una variable pasada por referencia. Este enfoque es algo mejor que los dos anteriores porque disminuye la probabilidad de los errores de programación. Puede suceder que los métodos y las funciones no operen adecuadamente:

Ejemplo que devuelve el resultado de una operación como valor booleano

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fallo");
      return;
     }
  }

Esto se puede combinar con la opción mencionada anteriormente, si hay varios errores diferentes y necesitamos identificar el error exacto. Se puede devolver un valor false y asignar un código de error a una variable global.

Ejemplo que devuelve un valor booleano como resultado de la función y asigna un código de error con la función SetUserError()

#define SOME_STR_FUNCTION_FAIL_CODE_1 (123)
#define SOME_STR_FUNCTION_FAIL_CODE_2 (124)

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";

      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_1);

      return false;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_2);
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fallo, código = "+(string)(GetLastError()-ERR_USER_ERROR_FIRST));
      return;
     }
  }

Sin embargo cuesta interpretar esta función al leer el código.


Devolver el resultado como valor de una enumeración (enum), y el valor resultante como variable pasada por referencia. Si hay varios tipos de errores diferentes, esta opción permite devolver en caso de fallo un tipo de error específico sin utilizar variables globales. Solo un valor se corresponde con la ejecución correcta; el resto se consideran como error.

Ejemplo que devuelve el resultado de una función como valor de una enumeración (enum)

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

ENUM_SOME_STR_FUNCTION_RESULT SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_1;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_2;
     }

   value=resultValue;
   return SOME_STR_FUNCTION_SUCCES;
  }

void OnTick()
  {
   string someStr="";

   ENUM_SOME_STR_FUNCTION_RESULT result=SomeStrFunction(someStr);

   if(result!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fallo, error = "+EnumToString(result));
      return;
     }
  }

Este enfoque aporta una ventaja muy importante: eliminar las variables globales; puesto que una mala gestión puede causar graves problemas.


Devolver el resultado como una estructura que contiene una variable booleana, o el valor de una enumeración, y un valor. Esta opción se relaciona con el método anterior que elimina la necesidad de pasar variables por referencia. Aquí es preferible el uso de enum porque permite que la lista de los posibles resultados de la ejecución se pueda extender en el futuro.

Ejemplo que devuelve el resultado de una operación en forma de estructura que contiene los valores de una enumeración y los valores resultantes

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

struct SomeStrFunctionResult
  {
   ENUM_SOME_STR_FUNCTION_RESULT code;
   char              value[255];
  };

SomeStrFunctionResult SomeStrFunction()
  {
   SomeStrFunctionResult result;

   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_1;
      return result;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_2;
      return result;
     }

   result.code=SOME_STR_FUNCTION_SUCCES;
   StringToCharArray(resultValue,result.value);
   return result;
  }

void OnTick()
  {
   SomeStrFunctionResult result=SomeStrFunction();

   if(result.code!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fallo, error = "+EnumToString(result.code));
      return;
     }
   string someStr=CharArrayToString(result.value);
  }


Intentamos ejecutar la operación varias veces. Generalmente, antes de dar por fallida una operación, vale la pena intentar ejecutarla varias veces. Por ejemplo, si un archivo no se puede leer porque otro proceso lo está utilizando, intente abrirlo varias veces repetidas, en intervalos pequeños de tiempo. La probabilidad de que el otro proceso libere el archivo es alta de modo que nuestro método o función podrá leerlo correctamente.

Ejemplo que intenta abrir un archivo varias veces

string fileName="test.txt";
int fileHandle=INVALID_HANDLE;

for(int iTry=0; iTry<=10; iTry++)
  {
   fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);

   if(fileHandle!=INVALID_HANDLE)
     {
      break;
     }
   Sleep(iTry*200);
  }

Nota: El ejemplo de arriba muestra sólo la idea fundamental de este enfoque; si quiere programar una aplicación real práctica tiene que analizar los errores. Si por ejemplo ocurre un error 5002 (nombre de archivo incorrecto) o 5003 (nombre de archivo demasiado largo), no tiene sentido hacer más intentos. Además, un enfoque así no debería aplicarse en aquellos sistemas donde no se desea que ocurra ninguna desaceleración del rendimiento general.


Notificar al usuario de forma explícita. Hay que notificar a los usuarios sobre la existencia de algunos errores, ya sea mediante una ventana emergente o una etiqueta en un gráfico, etc. Las notificaciones explícitas suelen utilizarse en combinación con la suspensión o la detención completa del programa. Hay que avisar al usuario claramente cuando, por ejemplo, su cuenta no tiene fondos suficientes, o cuando escribe los parámetros de entrada de forma incorrecta.

Ejemplo que avisa al usuario de que ha escrito unos parámetros de entrada incorrectos

input uint MAFastPeriod = 10;
input uint MASlowPeriod = 200;

int OnInit()
  {
//---
   if(MAFastPeriod>=MASlowPeriod)
     {
      Alert("El periodo de la media móvil rápida tiene que ser menor que el de la meda móvil lenta.");
      return INIT_PARAMETERS_INCORRECT;
     }
//---
   return(INIT_SUCCEEDED);
  }

Por supuesto que hay muchos más métodos de control de errores, la lista proporcionada solo revela los que se utilizan normalmente.


Recomendaciones generales sobre la gestión de los errores

Elegir el nivel adecuado de gestión de errores. Cada programa necesita unos requerimientos completamente distintos para gestionar sus errores. El manejo de los errores se puede omitir en algunos casos, por ejemplo en los scripts pequeños, en los programas que solo utilizaremos un par de veces, para comprobar pequeñas ideas, o en scripts que no compartiremos con terceros. Sin embargo debemos procesar todos los errores si el proyecto tiene cientos y miles de usuarios potenciales. Como decimos, cada caso particular requiere una buena comprensión del nivel de procesamiento de errores que se necesita.

Elegir el nivel adecuado de interacción con el usuario. El programa puede operar sin notificaciones; la interacción explícita del usuario solo hace fata en algunos errores. Es importante encontrar una solución intermedia. Por supuesto que no hay que bombardear al usuario con advertencias de error, pero tampoco hay que olvidar las notificaciones en las situaciones críticas. El siguiente enfoque ofrece una buena solución. Solo enviaremos notificaciones explícitas a los usuarios cuando se produzcan errores críticos, o en aquellas situaciones que requieren intervención; en el resto de casos mantendremos los archivos de log.

Comprobar los resultados de todas las funciones. Si alguna función o método devuelve un valor, entre los cuales hay errores, entonces es mejor comprobarlos. No deje escapar ninguna oportunidad que le permita mejorar la calidad del programa.

Comprobar las condiciones antes de realizar determinadas acciones, si es posible. Por ejemplo, antes de abrir una posición comprobar lo siguiente:

  1. Se puede operar con robots de trading en el terminal: TerminalInfoInteger(TERMINAL_TRADE_ALLOWED).
  2. La cuenta permite utilizar robots de trading: AccountInfoInteger(ACCOUNT_TRADE_EXPERT).
  3. Existe una conexión con el servidor de trading: TerminalInfoInteger(TERMINAL_CONNECTED).
  4. Los parámetros de la operación de trading son correctos: OrderCheck().

Mantener un funcionamiento adecuado en todas las partes del programa. Un ejemplo común: un trailing stop loss que no tiene en cuenta la frecuencia de consultas realizadas al servidor de trading. La llamada a esta función se implementa normalmente en cada tick. Esta función es capaz de enviar solicitudes de modificación casi en cada tick, o incluso varias solicitudes de operaciones distintas, si hay un movimiento unidireccional continuo, o si se producen algunos errores al intentar modificar las transacciones.

Esta característica no supone ningún problema cuando las cotizaciones se reciben con poca frecuencia. De lo contrario, puede haber problemas serios. Las solicitudes de modificación extremadamente frecuentes pueden provocar que el broker deje de operar con nuestro asesor experto, en una cuenta determinada, lo que derivará en conversaciones incómodas con el personal de atención al ciente. La solución más sencilla es limitar la frecuencia de intentos en las peticiones de modificación; recuerde la hora de la consulta previa y no realice ninguna otra hasta que hayan pasado veinte segundos.

Ejemplo de ejecución de trailing stop loss en menos de 30 segundos

const int TRAILING_STOP_LOSS_SECONDS_INTERVAL=30;

void TrailingStopLoss()
  {
   static datetime prevModificationTime=0;

   if((int)TimeCurrent() -(int)prevModificationTime<=TRAILING_STOP_LOSS_SECONDS_INTERVAL)
     {
      return;
     }

//--- Modificación Stop Loss
     {
      ...
      ...
      ...
      prevModificationTime=TimeCurrent();
     }
  }

Según mi propia experiencia, si intenta colocar demasiadas órdenes pendientes en un período corto de tiempo puede ocurrir el mismo problema.


Conseguir una relación adecuada entre la estabilidad y la corrección. Conviene definir un compromiso que mantenga un equilibrio entre la estabilidad y la exactitud del código. La estabilidad hace que el programa continúe operando aunque se produzcan errores, incluso si se generan resultados ligeramente inexactos. La exactitud , o corrección, no permite devolver resultados inexactos o ejecutar acciones erróneas. Es mejor parar el programa que devolver resultados inexactos, o hacer cualquier otra cosa mal.

Por ejemplo, si un indicador no puede calcular algo, es preferible no generar ninguna señal que apagarlo completamente. Por el contrario, es mejor parar un robot de trading que abrir una posición con un volumen excesivo. Además, antes de pararse, un robot de trading puede enviar una notificación PUSH a los usuarios, permitiendo que estos puedan aprender de los problemas, y sepan tratarlos correctamente.


Mostrar información útil de los errores. Intente que los mensajes de error sean informativos. No es suficiente que el programa responda con un error como "no se puede abrir la posición", sin añadir ninguna explicación complementaria. Hay que enviar mensajes más específicos como "no se puede abrir la posición: el volumen de la posición abierta es incorrecto (0.9999)". No importa si el programa muestra el error informativo en una ventana emergente o en un archivo de log. En cualquier caso, debe servir para que el usuario o el programador entiendan la causa del error, especialmente en el archivo de log, y lo arreglen, si es posible. Evite la sobrecarga de información; no hace falta mostrar el código del error en la ventana emergente porque un usuario normal no sabrá qué hacer con el mismo.


Logging con herramientas MQL5

Los programas suelen crear archivos de log que facilitan la vida a los programadores. Estos archivos contienen los errores generados para que los programadores puedan buscarlos y sean capaces de evaluar las condiciones del sistema en un momento determinado. Además de todo esto, el logging también sirve para analizar el rendimiento del software (profiling).


Niveles de logging

Los mensajes generados en los archivos de log contienen mucha información y por tanto exigen diferentes niveles de atención. Los niveles de logging se aplican a los mensajes por separado para clasificarlos por niveles críticos; se puede personalizar el nivel de importancia de los mensajes. Normalmente se implementan varios niveles de logging:


Mantenimiento de los archivos de log

La forma más sencilla de gestionar los archivos de log con herramientas MQL5 es mediante las funciones estándar Print o PrintFormat. Como resultado, se envían todos los mensajes al Asesor Experto, indicador y log del terminal.

Ejemplo que imprime mensajes en el log de un Asesor Experto con la función Print()

double VolumeCalculation()
  {
   double result=...;
   if(result<SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN))
     {
      Print("volumen de una operación (",DoubleToString(result,2),") parece menor que lo aceptable y se ha ajustado a "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN),2));
      result=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
     }
   return result;
  }

Esta función tiene varios inconvenientes:

  1. Los mensajes de varios programas pueden mezclarse en el "grupo" total, y por tanto el análisis se puede complicar.
  2. Se puede acceder fácilmente a los archivos de log, de modo que cualquier usuario los puede eliminar de forma o accidental o deliberada.
  3. Configurar e implementar los niveles de logging es complicado.
  4. Por otro lado, es imposible redireccionar los mensajes de log a otra fuente, como por ejemplo un archivo externo, una base de datos, email, etc.
  5. Y es imposible implementar una rotación obligatoria de los archivos de log (reemplazar los archivos por datos y hora, o al llegar a un tamaño determinado).

Ventajas de esta aproximación:

  1. Basta con utilizar la misma función sin tener que reinventar nada nuevo.
  2. En muchos casos el archivo de log se ve directamente en el terminal y no hay que buscar por separado.

Las limitaciones de las funciones Print() y PrintFormat() se pueden superar con la implementación de un mecanismo de logging personal. Sin embargo, si tenemos que reutilizar el código, habrá que transferirlo a un proyecto nuevo y a un mecanismo de logging.

Consideremos el siguiente escenario como ejemplo de mecanismo de logging en MQL5.

Ejemplo de implementación de un mecanismo de logging personalizado en MQL5

//+---------------------------------------------------------------------------+
//|                                                                logger.mqh |
//|                                            Copyright 2015, Sergey Eryomin |
//|                                                      http://www.ensed.org |
//+---------------------------------------------------------------------------+
#property copyright "Sergey Eryomin"
#property link      "http://www.ensed.org"

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")
//--- número máximo de archivos por operación "un nuevo archivo de log por cada 1 Mb"
#define MAX_LOG_FILE_COUNTER (100000) 
//--- número de bytes de un megabyte
#define BYTES_IN_MEGABYTE (1048576)
//--- longitud máxima del nombre del archivo de log
#define MAX_LOG_FILE_NAME_LENGTH (255)
//--- niveles de logging
enum ENUM_LOG_LEVEL
  {
   LOG_LEVEL_DEBUG,
   LOG_LEVEL_INFO,
   LOG_LEVEL_WARNING,
   LOG_LEVEL_ERROR,
   LOG_LEVEL_FATAL
  };
//--- métodos de logging
enum ENUM_LOGGING_METHOD
  {
   LOGGING_OUTPUT_METHOD_EXTERN_FILE,// archivo externo
   LOGGING_OUTPUT_METHOD_PRINT // función Print
  };
//--- métodos de notificación
enum ENUM_NOTIFICATION_METHOD
  {
   NOTIFICATION_METHOD_NONE,// desactivado
   NOTIFICATION_METHOD_ALERT,// función Alert
   NOTIFICATION_METHOD_MAIL, // función SendMail
   NOTIFICATION_METHOD_PUSH // función SendNotification
  };
//--- tipos de restricción de los archivos de log
enum ENUM_LOG_FILE_LIMIT_TYPE
  {
   LOG_FILE_LIMIT_TYPE_ONE_DAY,// nuevo archivo de log por cada día nuevo
   LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE // nuevo archivo de log por cada nuevo 1Mb
  };
//+---------------------------------------------------------------------------+
//|                                                                           |
//+---------------------------------------------------------------------------+
class CLogger
  {
public:
   //--- añadimos un mensaje al log
   //--- Nota:
   //--- si el modo de salida al archivo externo está activado pero no se puede ejecutar,
   //--- entonces la salida del mensaje se hace vía Print()
   static void Add(const ENUM_LOG_LEVEL level,const string message)
     {
      if(level>=m_logLevel)
        {
         Write(level,message);
        }

      if(level>=m_notifyLevel)
        {
         Notify(level,message);
        }
     }
   //--- establecemos los niveles de logging
   static void SetLevels(const ENUM_LOG_LEVEL logLevel,const ENUM_LOG_LEVEL notifyLevel)
     {
      m_logLevel=logLevel;
      //--- el nivel de salida del mensaje a través de las notificaciones no puede ser inferior al nivel de escritura de mensajes en el archivo de log
      m_notifyLevel=fmax(notifyLevel,m_logLevel);
     }
   //--- establecemos el método de logging
   static void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod)
     {
      m_loggingMethod=loggingMethod;
     }
   //--- establecemos el método de notificación
   static void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod)
     {
      m_notificationMethod=notificationMethod;
     }
   //--- establecemos el nombre del archivo de log
   static void SetLogFileName(const string logFileName)
     {
      m_logFileName=logFileName;
     }
   //--- establecemos el tipo de restricción del archivo de log
   static void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType)
     {
      m_logFileLimitType=logFileLimitType;
     }

private:
   //--- los mensajes con este nivel de logging y superior se almacenan en el archivo de log
   static ENUM_LOG_LEVEL m_logLevel;
   //--- los mensajes con este nivel de logging y superior se escriben como notificaciones
   static ENUM_LOG_LEVEL m_notifyLevel;
   //--- método de logging
   static ENUM_LOGGING_METHOD m_loggingMethod;
   //--- método de notificación
   static ENUM_NOTIFICATION_METHOD m_notificationMethod;
   //--- nombre del archivo de log
   static string     m_logFileName;
   //--- tipo de restricción del archivo de log
   static ENUM_LOG_FILE_LIMIT_TYPE m_logFileLimitType;
   //--- resultado de obtener el nombre del archivo           
   struct GettingFileLogNameResult
     {
                        GettingFileLogNameResult(void)
        {
         succes=false;
         ArrayInitialize(value,0);
        }
      bool              succes;
      char              value[MAX_LOG_FILE_NAME_LENGTH];
     };
   //--- resultado de comprobar el tamaño del archivo de log existente
   enum ENUM_LOG_FILE_SIZE_CHECKING_RESULT
     {
      IS_LOG_FILE_LESS_ONE_MEGABYTE,
      IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE,
      LOG_FILE_SIZE_CHECKING_ERROR
     };
   //--- escribimos en el archivo de log
   static void Write(const ENUM_LOG_LEVEL level,const string message)
     {
      switch(m_loggingMethod)
        {
         case LOGGING_OUTPUT_METHOD_EXTERN_FILE:
           {
            GettingFileLogNameResult getLogFileNameResult=GetLogFileName();
            //---
            if(getLogFileNameResult.succes)
              {
               string fileName=CharArrayToString(getLogFileNameResult.value);
               //---
               if(WriteToFile(fileName,GetDebugLevelStr(level)+": "+message))
                 {
                  break;
                 }
              }
           }
         case LOGGING_OUTPUT_METHOD_PRINT:
            default:
              {
               Print(GetDebugLevelStr(level)+": "+message);
               break;
              }
        }
     }
   //--- ejecutamos una notificación
   static void Notify(const ENUM_LOG_LEVEL level,const string message)
     {
      if(m_notificationMethod==NOTIFICATION_METHOD_NONE)
        {
         return;
        }
      string fullMessage=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+message;
      //---
      switch(m_notificationMethod)
        {
         case NOTIFICATION_METHOD_MAIL:
           {
            if(TerminalInfoInteger(TERMINAL_EMAIL_ENABLED))
              {
               if(SendMail("Logger",fullMessage))
                 {
                  return;
                 }
              }
           }
         case NOTIFICATION_METHOD_PUSH:
           {
            if(TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
              {
               if(SendNotification(fullMessage))
                 {
                  return;
                 }
              }
           }
        }
      //---
      Alert(GetDebugLevelStr(level)+": "+message);
     }
   //--- obtenemos el nombre del archivo de log para escribir
   static GettingFileLogNameResult GetLogFileName()
     {
      if(m_logFileName=="")
        {
         InitializeDefaultLogFileName();
        }
      //---
      switch(m_logFileLimitType)
        {
         case LOG_FILE_LIMIT_TYPE_ONE_DAY:
           {
            return GetLogFileNameOnOneDayLimit();
           }
         case LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE:
           {
            return GetLogFileNameOnOneMegabyteLimit();
           }
         default:
           {
            GettingFileLogNameResult failResult;
            failResult.succes=false;
            return failResult;
           }
        }
     }
   //--- obtenemos el nombre del archivo de log en caso de restricción con "nuevo archivo de log por cada día nuevo"
   static GettingFileLogNameResult GetLogFileNameOnOneDayLimit()
     {
      GettingFileLogNameResult result;
      string fileName=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+TimeToString(TimeLocal(),TIME_DATE);
      StringReplace(fileName,".","_");
      fileName=fileName+".log";
      result.succes=(StringToCharArray(fileName,result.value)==StringLen(fileName)+1);
      return result;
     }
   //--- obtenemos el nombre del archivo de log en caso de restricción con "nuevo archivo de log por cada 1 Mb nuevo"
   static GettingFileLogNameResult GetLogFileNameOnOneMegabyteLimit()
     {
      GettingFileLogNameResult result;
      //---
      for(int i=0; i<MAX_LOG_FILE_COUNTER; i++)
        {
         ResetLastError();
         string fileNameToCheck=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+(string)i;
         StringReplace(fileNameToCheck,".","_");
         fileNameToCheck=fileNameToCheck+".log";
         ResetLastError();
         bool isExists=FileIsExist(fileNameToCheck);
         //---
         if(!isExists)
           {
            if(GetLastError()==5018)
              {
               continue;
              }
           }
         //---
         if(!isExists)
           {
            result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

            break;
           }
         else
           {
            ENUM_LOG_FILE_SIZE_CHECKING_RESULT checkLogFileSize=CheckLogFileSize(fileNameToCheck);

            if(checkLogFileSize==IS_LOG_FILE_LESS_ONE_MEGABYTE)
              {
               result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

               break;
              }
            else if(checkLogFileSize!=IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE)
              {
               break;
              }
           }
        }
      //---
      return result;
     }
   //---
   static ENUM_LOG_FILE_SIZE_CHECKING_RESULT CheckLogFileSize(const string fileNameToCheck)
     {
      int fileHandle=FileOpen(fileNameToCheck,FILE_TXT|FILE_READ);
      //---
      if(fileHandle==INVALID_HANDLE)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      ResetLastError();
      ulong fileSize=FileSize(fileHandle);
      FileClose(fileHandle);
      //---
      if(GetLastError()!=0)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      if(fileSize<BYTES_IN_MEGABYTE)
        {
         return IS_LOG_FILE_LESS_ONE_MEGABYTE;
        }
      else
        {
         return IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE;
        }
     }
   //--- inicializamos el nombre del archivo de log de forma predeterminada
   static void InitializeDefaultLogFileName()
     {
      m_logFileName=MQLInfoString(MQL_PROGRAM_NAME);
      //---
#ifdef __MQL4__
      StringReplace(m_logFileName,".ex4","");
#endif

#ifdef __MQL5__
      StringReplace(m_logFileName,".ex5","");
#endif
     }
   //--- escribimos un mensaje en el archivo
   static bool WriteToFile(const string fileName,
                           const string text)
     {
      ResetLastError();
      string fullText=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+text;
      int fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);
      bool result=true;
      //---
      if(fileHandle!=INVALID_HANDLE)
        {
         //--- intentamos colocar el puntero del archivo al final del archivo            
         if(!FileSeek(fileHandle,0,SEEK_END))
           {
            Print("Logger: FileSeek() ha fallado, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
            result=false;
           }
         //--- intentamos escribir texto en el archivo
         if(result)
           {
            if(FileWrite(fileHandle,fullText)==0)
              {
               Print("Logger: FileWrite() ha fallado, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
               result=false;
              }
           }
         //---
         FileClose(fileHandle);
        }
      else
        {
         Print("Logger: FileOpen() ha fallado, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
         result=false;
        }
      //---
      return result;
     }
   //--- obtenemos el periodo actual
   static string GetPeriodStr()
     {
      ResetLastError();
      string periodStr=EnumToString(Period());
      if(GetLastError()!=0)
        {
         periodStr=(string)Period();
        }
      StringReplace(periodStr,"PERIOD_","");
      //---
      return periodStr;
     }
   //---
   static string GetDebugLevelStr(const ENUM_LOG_LEVEL level)
     {
      ResetLastError();
      string levelStr=EnumToString(level);
      //---
      if(GetLastError()!=0)
        {
         levelStr=(string)level;
        }
      StringReplace(levelStr,"LOG_LEVEL_","");
      //---
      return levelStr;
     }
  };
ENUM_LOG_LEVEL CLogger::m_logLevel=LOG_LEVEL_INFO;
ENUM_LOG_LEVEL CLogger::m_notifyLevel=LOG_LEVEL_FATAL;
ENUM_LOGGING_METHOD CLogger::m_loggingMethod=LOGGING_OUTPUT_METHOD_EXTERN_FILE;
ENUM_NOTIFICATION_METHOD CLogger::m_notificationMethod=NOTIFICATION_METHOD_ALERT;
string CLogger::m_logFileName="";
ENUM_LOG_FILE_LIMIT_TYPE CLogger::m_logFileLimitType=LOG_FILE_LIMIT_TYPE_ONE_DAY;
//+---------------------------------------------------------------------------+

Ponga este código en un archivo separado, por ejemplo en Logger.mqh, y guárdelo en <data_folder>/MQL5/Include (este archivo se adjunta en el presente artículo). La operación con la clase CLogger tiene aproximadamente este aspecto:

Ejemplo de implementación de un mecanismo de logging personal

#include <Logger.mqh>

//--- inicializamos el logger
void InitLogger()
  {
//--- establecemos los niveles de logging: 
//--- nivel DEBUG para escribir mensajes en el archivo de log
//--- nivel ERROR para notificaciones
   CLogger::SetLevels(LOG_LEVEL_DEBUG,LOG_LEVEL_ERROR);
//--- establecemos el tipo de notificación como notificación PUSH
   CLogger::SetNotificationMethod(NOTIFICATION_METHOD_PUSH);
//--- establecemos el método de logging en un archivo externo
   CLogger::SetLoggingMethod(LOGGING_OUTPUT_METHOD_EXTERN_FILE);
//--- establecemos el nombre del archivo de log
   CLogger::SetLogFileName("my_log");
//--- establecemos las restricciones del archivo de log como "nuevo archivo de log por día nuevo"
   CLogger::SetLogFileLimitType(LOG_FILE_LIMIT_TYPE_ONE_DAY);
  }

int OnInit()
  {
//---
   InitLogger();
//---
   CLogger::Add(LOG_LEVEL_INFO,"");
   CLogger::Add(LOG_LEVEL_INFO,"---------- OnInit() -----------");
   LOG(LOG_LEVEL_DEBUG,"Ejemplo de mensaje de depuración");
   LOG(LOG_LEVEL_INFO,"Ejemplo de mensaje de información");
   LOG(LOG_LEVEL_WARNING,"Ejemplo de mensaje de advertencia");
   LOG(LOG_LEVEL_ERROR,"Ejemplo de mensaje de error");
   LOG(LOG_LEVEL_FATAL,"Ejemplo de mensaje fatal");
//---
   return(INIT_SUCCEEDED);
  }

En primer lugar, la función InitLogger() inicializa los parámetros del logger, y a continuación se escriben los mensajes en el archivo. El resultado de este código se almacenará en el archivo de log llamado «my_log_USDCAD_D1_2015_09_23.log» dentro de la carpeta <data_folder>/MQL5/Files. El texto es el siguiente:

2015.09.23 09:02:10, USDCAD (D1), INFO: 
2015.09.23 09:02:10, USDCAD (D1), INFO: ---------- OnInit() -----------
2015.09.23 09:02:10, USDCAD (D1), DEBUG: Ejemplo de mensaje de depuración (LoggerTest.mq5; int OnInit(); Line: 36)
2015.09.23 09:02:10, USDCAD (D1), INFO: Ejemplo de mensaje de información (LoggerTest.mq5; int OnInit(); Line: 38)
2015.09.23 09:02:10, USDCAD (D1), WARNING: Ejemplo de mensaje de advertencia (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:02:10, USDCAD (D1), ERROR: Ejemplo de mensaje de error (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:02:10, USDCAD (D1), FATAL: Ejemplo de mensaje fatal (LoggerTest.mq5; int OnInit(); Line: 44)

Además, los mensajes de los niveles ERROR y FATAL se enviarán a través de notificaciones PUSH.

Cuando establezcamos el nivel de un mensaje como advertencia (CLogger::SetLevels(LOG_LEVEL_WARNING,LOG_LEVEL_ERROR)), la salida será así:

2015.09.23 09:34:00, USDCAD (D1), WARNING: Ejemplo de mensaje de advertencia (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:34:00, USDCAD (D1), ERROR: Ejemplo de mensaje de error (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:34:00, USDCAD (D1), FATAL: Ejemplo de mensaje fatal (LoggerTest.mq5; int OnInit(); Line: 44)

Esto significa que los mensajes por debajo del nivel WARNING no se guardarán.


Métodos públicos de la clase CLogger y la macro LOG

Analicemos los métodos públicos de la clase CLogger y la macro LOG.


void SetLevels(const ENUM_LOG_LEVEL logLevel, const ENUM_LOG_LEVEL notifyLevel). Establece los niveles de logging.

const ENUM_LOG_LEVEL logLevel — los mensajes con este nivel de logging y superior se guardan en el archivo de log. Por defecto = LOG_LEVEL_INFO.

const ENUM_LOG_LEVEL notifyLevel — los mensajes con este nivel de logging y superior se muestran como notificaciones. Por defecto = LOG_LEVEL_FATAL.

Valores posibles para ambos:


void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod). Establece un método de logging.

const ENUM_LOGGING_METHOD loggingMethod — método de logging. De forma predeterminada = LOGGING_OUTPUT_METHOD_EXTERN_FILE.

Valores posibles:


void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod). Establece un método de notificación.

const ENUM_NOTIFICATION_METHOD notificationMethod — método de notificación. De forma predeterminada = NOTIFICATION_METHOD_ALERT.

Valores posibles:


void SetLogFileName(const string logFileName). Establece el nombre del archivo de log.

const string logFileName — nombre del archivo de log. El valor predeterminado será el nombre del programa donde se utiliza el logger (ver el método privado InitializeDefaultLogFileName()).


void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType). Establece el tipo de restricción en el archivo de log.

const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType — tipo de restricción en el archivo de log. Valor predeterminado: LOG_FILE_LIMIT_TYPE_ONE_DAY.

Valores posibles:


void Add(const ENUM_LOG_LEVEL level,const string message). Añade el mensaje al log.

const ENUM_LOG_LEVEL level — nivel del mensaje. Valores posibles:

const string message — texto del mensaje.


Aparte del método Add, también se implementa la macro LOG, y al escribir en el archivo de log se añade al mensaje el nombre del archivo, la signatura de la función y el número de línea:

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")

Esta macro es especialmente útil en la fase de depuración.

Así pues, el ejemplo muestra el mecanismo de logging, que permite:

  1. Configurar los niveles de logging (DEBUG..FATAL).
  2. Establecer el nivel de los menajes cuando hay que notificar a los usuarios.
  3. Establecer dónde se escribe el log; en el log del Asesor Experto vía Print() o en un archivo externo.
  4. Para la salida de archivos externos, se indica el nombre del archivo y se establecen restricciones en los archivos de log: un archivo por fecha, o uno para cada megabyte.
  5. Especificar el tipo de notificación (Alert(), SendMail(), SendNotify()).

La solución aquí propuesta es tan solo un ejemplo, y algunas tareas requerirán que se modifique, incluyendo la eliminación de la funcionalidad no deseada. Por ejemplo, además de los archivos externos y el log normal, también se puede implementar como método de logging la escritura en una base de datos.


Conclusión

En este artículo hemos enseñado a gestionar los errores y a trabajar con el log con las herramientas de MQL5. Gestionar los errores correctamente y trabajar con el log de forma adecuada contribuye a incrementar la calidad del software desarrollado, y también simplifica el mantenimiento.