English Русский 中文 Español Deutsch 日本語
Tratamento de erros e registro em log na MQL5

Tratamento de erros e registro em log na MQL5

MetaTrader 5Exemplos | 7 setembro 2016, 11:02
3 650 1
Sergey Eremin
Sergey Eremin

Introdução

Durante a operação da maioria dos programas, ocasionalmente poderão ocorrer erros. O tratamento adequado desses erros é um dos aspectos importantes da funcionalidade de sóftwares estáveis e de alta qualidade. Este artigo examinará os métodos básicos de tratamento de erros, aconselhará sobre como usá-los e abordará o tema do registro em log via MQL5.

O tratamento de erros é um tema bastante difícil e controverso. Há muitas maneiras de fazer o tratamento de erros, e cada uma delas possui certas vantagens e desvantagens. Muitos desses métodos podem ser usados conjuntamente e não há nenhuma fórmula universal para cada tarefa específica, quer dizer, é preciso escolher uma abordagem adequada para cada tarefa.


Métodos básicos de tratamento de erro

Se o programa, durante o seu trabalho, estiver enfrentando algum erro, ele mesmo deverá executar alguma ação (ou algumas ações) para funcionar corretamente. Abaixo apresentamos alguns exemplos desse tipo de ações:

Interromper a execução do programa. Se houverem alguns erros, a ação mais apropriada será parar o programa em execução. Normalmente, após a aparição deste tipo de problemas críticos, a execução do programa é impossível, não faz sentido ou simplesmente é perigoso. A MQL5 fornece um mecanismo de interrupção para erros de tempo de execução, por exemplo, no caso de "uma divisão por zero" ou "uma matriz ficar fora do intervalo", o programa pára de funcionar. Em outros casos, o programador deve tomar conta por si mesmo da interrupção do trabalho. Por exemplo, para Expert Advisors, é possível adotar a função ExpertRemove():

Exemplo de interrupção de trabalho do Expert Advisor usando ExpertRemove():

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

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


Converter valores incorretos para o intervalo de valores correto. Muitas vezes, um valor deve estar dentro do intervalo especificado. No entanto, em alguns casos, podem aparecer valores fora deste intervalo. Nesse caso, é possível ter um retorno forçado do valor para os limites permitidos. Como exemplo temos o cálculo de volume de uma posição aberta. Se o valor resultante estiver fora dos valores mínimo e máximo, ele pode ser forçado a voltar dentro desses limites:

Exemplo de conversão de um valor incorreto para o intervalo válido

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

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

   return result;
  }

No entanto, se, por algum motivo, o volume acabar por ser maior do que o limite máximo, e o depósito for incapaz de suportar tal carga, o adequado será registrar tudo e interromper a execução do programa. Muitas vezes, este erro em partícular é muito perigoso para a conta.


Retornar o valor de erro. Neste caso, se ocorrer um erro, um método ou uma função deverá retornar o valor pré-determinado que vai sinalizar o erro. Por exemplo, se nosso método ou função deve retornar uma seqüência de caracteres, NULL pode ser retornado, no caso de um erro.

Exemplo de retorno do valor de erro

#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("fail");
      return;
     }
  }

No entanto, esta abordagem pode conduzir a erros de programador. Se essa ação não for documentada, ou se o programador não se familiarizar com o documento ou a implementação de código, então ele não estará ciente do possível valor de erro. Além disso, podem ocorrer problemas, se, no modo normal de operação, a função ou o método retornam qualquer valor, incluindo aquele que é considerado com um erro.


Atribuir o resultado da execução de uma variável global especial. Muitas vezes, esta abordagem é aplicada a métodos ou funções que não retornam quaisquer valores. A ideia é que o resultado deste método ou função seja atribuído a uma certa variável global e, em seguida, o valor dessa variável seja verificado no código de chamada. Na MQL5, para este fim, existe a funcionalidade padrão (SetUserError()).

Exemplo de atribuição de código de erro usando 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("fail");
      return;
     }
  }

Neste caso, o programador pode não estar ciente dos possíveis erros, no entanto, esta abordagem permite-lhe informar não apenas sobre a existência do erro, mas também indicar o seu código específico. Isto é especialmente importante se as causas do erro são variadas.


Retornar o resultado de execução como um bool e o valor resultante como uma variável passada por uma referência. Esta abordagem é um pouco melhor do que as duas anteriores, uma vez que reduz a probabilidade de erro de programação. Neste caso, o mais difícil é não notar que o método ou função pode não fazer seu trabalho corretamente:

Exemplo de retorno de resultado de execução de função como um bool

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("fail");
      return;
     }
  }

Se houver vários erros diferentes e nós precisarmos conhecer exatamente seu tipo, então esta opção poderá ser combinada com a anterior. É possível retornar false, enquanto à variável global é atribuído um código de erro.

Exemplo de retorno de resultado de execução da função como um bool e atribuição do código de erro usando 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("fail, code = "+(string)(GetLastError()-ERR_USER_ERROR_FIRST));
      return;
     }
  }

No entanto, esta opção é difícil de entender (durante a leitura de código) e de apoiar continuamente.


Retornar o resultado como um valor a partir da enumeração (enum), e o valor resultante (se ele existir) como uma variável passada por uma referência. Esta opção permite, em caso de falha, retornar um tipo de erro específico, se os tipos de erros possíveis forem variados, sem uso de variáveis globais. Apenas um valor corresponderá à execução correta, e o resto será considerado como erro.

Exemplo de retorno de resultado de execução de função como uma enumeração (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("fail, error = "+EnumToString(result));
      return;
     }
  }

Livrar-se de variáveis globais é uma importante vantagem desta abordagem, pois a manipulação negligente ou incompetente delas pode trazer grandes problemas.


Retornar o resultado como uma instância de estrutura que consiste em uma variável booliana ou valor de enumeração (enum) e um valor resultante. É uma variante relacionada ao tema do método anterior que eliminava a necessidade de passar as variáveis por uma referência. Neste caso, prefere-se utilizar enum, uma vez que permitirá, no futuro, expandir a lista de possíveis resultados de execução.

Exemplo de retorno de resultado de execução de função como uma instância de estrutura que consiste dos valores de enumeração (enum) e do valor resultante

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("fail, error = "+EnumToString(result.code));
      return;
     }
   string someStr=CharArrayToString(result.value);
  }


Tentar executar a operação várias vezes. Muitas vezes, vale a pena tentar executar uma operação várias vezes antes de considerá-la sem sucesso. Por exemplo, se você não pode ler o arquivo, uma vez que é usado por outro processo, é possível realizar várias tentativas em intervalos crescentes. Há uma grande possibilidade de que o outro processo libere o arquivo e, assim, o nosso método possa se referir a ele.

Exemplo de execução várias tentativas para abrir o arquivo

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: este exemplo só reflete a essência dessa abordagem, na aplicação prática é preciso de analisar os erros que ocorram. Por exemplo, se ocorrer o erro 5002 (Nome de arquivo inválido) ou 5003 (O nome do arquivo é muito longo), as tentativas subseqüentes não farão sentido. Além disso, tenha em mente que esta abordagem não deve ser usada em sistemas onde qualquer desaceleração no desempenho global seja indesejável.


Alertar um usuário explicitamente. Os usuários precisam ser notificados explicitamente -janela pop-up, uma etiqueta no gráfico, etc.- sobre certos erros. As alertas explícitas podem ser frequentemente usadas em combinação ao suspender ou parar completamente um programa. Por exemplo, se a conta não tiver suficiente dinheiro ou se o usuário inserir um valor claramente incorreto nos parâmetros de entrada, então quando ele deverá ser claramente notificado sobre isso.

Exemplo de alerta de usuário sobre parâmetros de entrada inválidos

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

int OnInit()
  {
//---
   if(MAFastPeriod>=MASlowPeriod)
     {
      Alert("O período da média móvel rápida deve ser inferior ao período da média móvel lenta!");
      return INIT_PARAMETERS_INCORRECT;
     }
//---
   return(INIT_SUCCEEDED);
  }

Certamente existem outros métodos de tratamento de erros, a lista fornecida revela apenas os que são mais comumente usados.


Recomendações gerais para tratamento de erros

Selecione o nível adequado de tratamento de erros. Existem diferentes níveis de tratamento de erros para aplicar a diferentes programas. Se for executado um script pequeno, que irá ser utilizado apenas algumas vezes, ao verificar uma ideia pequena, você poderá ignorar o tratamento de erro. No entanto, se estamos falando sobre um projeto que envolve centenas de milhares de usuários potenciais, a abordagem apropriada seria lidar com todos os erros possíveis. Sempre tente compreender o nível de tratamento de erro que é necessário em cada caso.

Selecione o nível adequado de interação de usuário. Certos erros exigem a interação explícita do usuário, outros não: o programa pode continuar trabalhando por conta própria, sem qualquer notificação do usuário. É importante encontrar o equilíbrio: os usuários não devem ser bombardeados com avisos de erro ou, pelo contrário, obter zero notificações em situações críticas. Uma boa solução seria notificar claramente o usuário sobre os erros críticos ou que exigem sua participação, enquanto para todos os outros erros seria uma boa abordagem criar um arquivo de log.

Verifique os resultados de execução que retornam todas as funções e métodos. Se alguma função ou método poder retornar valores, entre os quais há algum que está avisando sobre erros, o melhor será verificá-los. Não vale a pena desprezar esta oportunidade de melhorar a qualidade do programa.

Se possível, por favor, verifique as condições antes de executar determinadas operações. Por exemplo, antes de tentar abrir uma negociação vale a pena conferir:

  1. Se está permitida a negociação usando robôs no lado do terminal: TerminalInfoInteger(TERMINAL_TRADE_ALLOWED).
  2. Se está permitida a negociação usando robôs para essa conta: AccountInfoInteger(ACCOUNT_TRADE_EXPERT).
  3. Se há conexão ao servidor de negociação: TerminalInfoInteger(TERMINAL_CONNECTED).
  4. Se os parâmetros de negociação são corretos: OrderCheck().

Preste atenção na adequada execução das diferentes partes do programa. O trailing Stop-Loss é um exemplo - bastante comum - de código onde não é levada em conta a freqüência de pedidos ao servidor de negociação. Normalmente, a chamada dessa função é implementada em cada tick. Se houver um movimento contínuo de sentido único, ou, se ocorrerem alguns erros ao tentar modificar as transações, essa função pode enviar pedidos de modificação de negociação quase em cada tick (ou múltiplos pedidos para várias negociações).

Quando são recebidas com pouca freqüência cotações, essa função não vai causar problemas. Mas, caso contrário, pode haver grandes problemas: os pedidos muito freqüentes de modificação de transações podem causar a desconexão, pela corretora, da negociação automatizada para uma determinada conta e conversas desagradáveis com o serviço de Suporte ao Cliente A solução mais fácil consiste em restringir a freqüência de tentativas de execução de pedidos para modificar as negociações: lembrar o pedido anterior e, se passarem menos de XX segundos, não tentar executá-lo novamente.

Exemplo de execução da função Trailing Stop não mais do que uma vez a cada 10 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;
     }

//--- modificação do Stop Loss
     {
      ...
      ...
      ...
      prevModificationTime=TimeCurrent();
     }
  }

O mesmo problema pode ocorrer, quando você tenta colocar demasiadas ordens pendentes dentro de um curto período de tempo, como já foi experimentado pelo autor.


Aponte para uma relação adequada entre durabilidade e validade. Ao criar o programa, é preciso procurar o compromisso entre a durabilidade e a exatidão do código. A durabilidade implica que, mesmo em caso de erro, o programa continuará trabalhando, embora isto possa levar a resultados ligeiramente imprecisos. A validade, por sua vez, não permite retornar resultados imprecisos ou executar ações incorretas, isto é, elas devem ou ser precisas ou não existir, em outras palavras, é melhor parar a execução do programa do que retornar um resultado impreciso ou fazer algo errado.

Por exemplo, se o indicador não conseguir calcular algo, é melhor deixar que ele emita a ausência de sinal do que desligar completamente o trabalho. Pelo contrário, quando se trata de um robô de negociação, é melhor deixá-lo completar o seu trabalho do que lhe permitir abrir uma negociação com um volume excessivo. Além disso, antes de parar de trabalhar, o robô pode avisar o usuário por meio de uma notificação por push, para informá-lo sobre o problema e responder rapidamente a ele.


Exiba informações úteis sobre os erros. Tente fazer as mensagens sobre erros suficientemente informativas. É ruim quando o programa, sem mais explicações, exibe o erro: «não é possível abrir a negociação». É aconselhável que a mensagem seja do tipo «não é possível abrir a negociação: volume incorreto de posição aberta (0.9999)». Não importa se o programa exibe as informações sobre o erro em uma janela pop-up ou em um arquivo de log, ele, em qualquer caso, deve ser suficiente para que o usuário ou programador (especialmente durante a análise o arquivo de log) possa compreender a causa do erro e, se possível, corrigi-lo. Além disso, os usuários não devem ser sobrecarregados com informações: não é necessário exibir o código do erro em uma janela pop-up, uma vez que um usuário não poderá fazer muita coisa com ele.


Registro em log via MQL5

Normalmente, os arquivos de log são criados pelo programa, para os próprios programadores, com o propósito de facilitar a pesquisa de causas para os diferentes falhas e erros, e para avaliar o estado do sistema em qualquer momento, etc. Além disso, o registro em log pode ser usado para criar perfis de software.


Níveis de registro em log

Geralmente, as mensagens, que são recebidas nos arquivos de log, levam diferentes níveis de severidade e requerem diferentes níveis de atenção. Para separar mensagens com níveis de severidade diferentes, bem como ter possibilidade de definir o nível de severidade das mensagens em exibição, empregam-se os níveis de registro em log. Geralmente, implementam-se vários níveis de registro em log:

  • Debug: mensagens de depuração. Este nível de registro em log é habilitado nas fases de desenvolvimento, depuração e comissionamento.
  • Info: mensagens informativas. Elas carregam informações sobre as diferentes ações de sistema (por exemplo, início/término do trabalho, a abertura/fechamento da transação, etc.). Normalmente, as mensagens deste nível não requerem qualquer reação, além disso, podem ajudar significativamente no estudo de cadeias de eventos que levaram a alguns erros de trabalho.
  • Warning: mensagem de aviso. Esse nível de mensagens pode incluir uma descrição da situação que causou os erros que não requerem a intervenção do utilizador. Por exemplo, se o volume calculado da transação foi inferior ao mínimo permitido e o programa corrigiu-o automaticamente, então isto pode ser informado no arquivo de log com o nível «Warning».
  • Error: mensagens sobre erros que requerem uma intervenção explícita. Este nível de registro em log é normalmente usado quando ocorre um erro (impossibilidade de armazenar um arquivo, impossibilidade de abrir ou modificar a transação, etc.). Em outras palavras, este nível inclui erros que o programa não é capaz de superar e que exigem uma intervenção explícita (do usuário ou programador).
  • Fatal: mensagens de erro crítico que desativam o trabalho futuro do programa. Essas mensagens exigem uma resposta rápida, muitas vezes, para esse nível, proporcionam-se alertas do usuário ou programados via e-mail, SMS, etc. Como será mostrado abaixo, na MQL5, para alertas, você pode usar notificações por push.


Manutenção de arquivos de log

A maneira mais fácil, para manutenção de arquivos de log via MQL5, se resume à implementação da função padrão Print ou PrintFormat. Como resultado, todas as mensagens serão enviadas para o diário comum dos EAs, indicadores e scripts.

Exemplo de exibição de mensagem no diário comum dos EAs usando a função Print()

double VolumeCalculation()
  {
   double result=...;
   if(result<SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN))
     {
      Print("O volume da transação (",DoubleToString(result,2),") resultou ser inferior ao aceitável e foi ajustado para "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN),2));
      result=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
     }
   return result;
  }

Essa abordagem possui várias desvantagens:

  1. As mensagens de vários programas podem ser despejadas em uma "pilha" comum, o que impede a sua análise
  2. O arquivo de log, tendo em vista a fácil disponibilidade, pode ser acidentalmente ou deliberadamente excluído pelo usuário.
  3. É difícil implementar e configurar os níveis de registro em log.
  4. É impossível redirecionar a exibição da mensagem de log para outra fonte (arquivo externo, banco de dados, correio, etc.).
  5. É impossível implementar a rotação forçada dos arquivos de log (alterar arquivos segundo a data, tempo ou atingimento de certo tamanho).

Vantagens dessa abordagem:

  1. Você não precisa inventar, basta aplicar a mesma função.
  2. Em muitos casos, o arquivo de log pode ser visto diretamente no terminal e não tem de ser pesquisado separadamente.

A implementação do mecanismo pessoal de registro em log pode eliminar todas as desvantagens de usar as funções Print() e PrintFormat(), mas, se for necessário re-utilizar o código, será necessária uma transferência do mecanismo de registro em log para um novo projeto (ou a recusa a usá-lo no código).

Como um exemplo de implementação do mecanismo de registro em log, na MQL5, você pode considerar o seguinte cenário.

Exemplo de implementação do mecanismo pessoal de registro em log, na 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 arquivos para o modo "um novo arquivo de log para cada novo megabyte"
#define MAX_LOG_FILE_COUNTER (100000) 
//--- número de bytes em um megabyte
#define BYTES_IN_MEGABYTE (1048576)
//--- comprimento máximo do nome do arquivo de log
#define MAX_LOG_FILE_NAME_LENGTH (255)
//--- níveis de registro em log
enum ENUM_LOG_LEVEL
  {
   LOG_LEVEL_DEBUG,
   LOG_LEVEL_INFO,
   LOG_LEVEL_WARNING,
   LOG_LEVEL_ERROR,
   LOG_LEVEL_FATAL
  };
//--- métodos de registro em log
enum ENUM_LOGGING_METHOD
  {
   LOGGING_OUTPUT_METHOD_EXTERN_FILE,// arquivo externo
   LOGGING_OUTPUT_METHOD_PRINT // função Print
  };
//--- métodos de notificação
enum ENUM_NOTIFICATION_METHOD
  {
   NOTIFICATION_METHOD_NONE,// desabilitado
   NOTIFICATION_METHOD_ALERT,// função Alert
   NOTIFICATION_METHOD_MAIL, // função SendMail
   NOTIFICATION_METHOD_PUSH // função SendNotification
  };
//--- tipo de restrições de arquivos de log
enum ENUM_LOG_FILE_LIMIT_TYPE
  {
   LOG_FILE_LIMIT_TYPE_ONE_DAY,// novo arquivo de log para cada novo dia
   LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE // novo arquivo de log para cada novo megabyte
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CLogger
  {
public:
   //--- adicionar mensagem ao log
   //--- Nota:
   //--- se o modo de saída para o arquivo externo estiver habilitado, mas não conseguir ser 
   //--- executado, então a saída da mensagem será feita via 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);
        }
     }
   //--- definir níveis de registro em log
   static void SetLevels(const ENUM_LOG_LEVEL logLevel,const ENUM_LOG_LEVEL notifyLevel)
     {
      m_logLevel=logLevel;
      //--- o nível de saída das mensagens através de notificações não deve ser inferior ao nível de armazenamento de mensagens em um arquivo de log
      m_notifyLevel=fmax(notifyLevel,m_logLevel);
     }
   //--- definir o modo de registro em log
   static void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod)
     {
      m_loggingMethod=loggingMethod;
     }
   //--- definir o modo de notificações
   static void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod)
     {
      m_notificationMethod=notificationMethod;
     }
   //--- definir o nome do arquivo de log
   static void SetLogFileName(const string logFileName)
     {
      m_logFileName=logFileName;
     }
   //--- definir o tipo de restrição para o arquivo de log
   static void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType)
     {
      m_logFileLimitType=logFileLimitType;
     }

private:
   //--- o nível de registro-em-log/mensagem não deve ser inferior ao nível de armazenamento no arquivo-de-log/diário
   static ENUM_LOG_LEVEL m_logLevel;
   //--- o nível de registro-em-log/mensagem não deve ser inferior aos serão exibidos como notificações
   static ENUM_LOG_LEVEL m_notifyLevel;
   //--- método de registro em log
   static ENUM_LOGGING_METHOD m_loggingMethod;
   //--- método de notificações
   static ENUM_NOTIFICATION_METHOD m_notificationMethod;
   //--- nome do arquivo de log
   static string     m_logFileName;
   //--- tipo de restrições para arquivos de log
   static ENUM_LOG_FILE_LIMIT_TYPE m_logFileLimitType;
   //--- resultado de obter o nome do arquivo para o log           
   struct GettingFileLogNameResult
     {
                        GettingFileLogNameResult(void)
        {
         succes=false;
         ArrayInitialize(value,0);
        }
      bool              succes;
      char              value[MAX_LOG_FILE_NAME_LENGTH];
     };
   //--- resultado da verificação do tamanho do arquivo 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
     };
   //--- registrar no arquivo 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;
              }
        }
     }
   //--- executar a notificação
   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);
     }
   //--- obter o nome do arquivo de log para registro
   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;
           }
        }
     }
   //--- obter o nome do arquivo de log para o caso de restrição: "novo arquivo de log para cada novo dia"
   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;
     }
   //--- obter o nome do arquivo de log para o caso de restrição: "novo arquivo de log para cada novo megabyte"
   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;
        }
     }
   //--- executar a inicialização padrão do nome do arquivo de log
   static void InitializeDefaultLogFileName()
     {
      m_logFileName=MQLInfoString(MQL_PROGRAM_NAME);
      //---
#ifdef __MQL4__
      StringReplace(m_logFileName,".ex4","");
#endif

#ifdef __MQL5__
      StringReplace(m_logFileName,".ex5","");
#endif
     }
   //--- registrar a mensagem no arquivo
   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)
        {
         //--- tentar colocar o ponteiro do arquivo no arquivo final            
         if(!FileSeek(fileHandle,0,SEEK_END))
           {
            Print("Logger: FileSeek() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
            result=false;
           }
         //--- tentar registrar o texto no arquivo
         if(result)
           {
            if(FileWrite(fileHandle,fullText)==0)
              {
               Print("Logger: FileWrite() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
               result=false;
              }
           }
         //---
         FileClose(fileHandle);
        }
      else
        {
         Print("Logger: FileOpen() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
         result=false;
        }
      //---
      return result;
     }
   //--- obter o período atual como uma cadeia de caracteres
   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;
//+------------------------------------------------------------------+

Esse código pode ser colocado em um arquivo anexo, separado e incluído - por exemplo, o Logger.mqh - e salvá-lo no <diretório de dados do terminal>/MQL5/Include (arquivo anexado a este artigo). Depois disso, o trabalho com a classe CLogger será algo parecido com isto:

Exemplo de implementação do mecanismo pessoal de registro em log

#include <Logger.mqh>

//--- executar a inicialização do registrador de logs
void InitLogger()
  {
//--- definir os níveis de registro em log: 
//--- DEBUG-nível (nível de depuração) para registrar o as mensagens no arquivo de log
//--- ERROR-nível para notificações
   CLogger::SetLevels(LOG_LEVEL_DEBUG,LOG_LEVEL_ERROR);
//--- definir o tipo de notificações como notificações por push
   CLogger::SetNotificationMethod(NOTIFICATION_METHOD_PUSH);
//--- definir o método de registro em log para registrar em um arquivo externo
   CLogger::SetLoggingMethod(LOGGING_OUTPUT_METHOD_EXTERN_FILE);
//--- definir o nome dos arquivos de log
   CLogger::SetLogFileName("my_log");
//--- obter o tipo de restrição para o arquivo de log como "novo arquivo de log para cada novo dia"
   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,"Example of debug message");
   LOG(LOG_LEVEL_INFO,"Example of info message");
   LOG(LOG_LEVEL_WARNING,"Example of warning message");
   LOG(LOG_LEVEL_ERROR,"Example of error message");
   LOG(LOG_LEVEL_FATAL,"Example of fatal message");
//---
   return(INIT_SUCCEEDED);
  }

Inicialmente, na função InitLogger(), são inicializados todos os parâmetros possíveis do registrador de logs e, em seguida, é executado o registro de mensagens no arquivo de log. O resultado de trabalho desse código será o registro, no arquivo de log, com um nome de tipo «my_log_USDCAD_D1_2015_09_23.log», dentro do <diretório_de_dados_do_terminal>/MQL5/Files do seguinte texto:

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: Example of debug message (LoggerTest.mq5; int OnInit(); Line: 36)
2015.09.23 09:02:10, USDCAD (D1), INFO: Example of info message (LoggerTest.mq5; int OnInit(); Line: 38)
2015.09.23 09:02:10, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:02:10, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:02:10, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

Além disso, serão enviadas, via notificação por push, as mensagens de níveis ERROR e FATAL.

Se você definir o nível de mensagens, para registro no arquivo de log, como Warning (CLogger::SetLevels(LOG_LEVEL_WARNING,LOG_LEVEL_ERROR)), então, a saída´será a seguinte:

2015.09.23 09:34:00, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:34:00, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:34:00, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

Ou seja, já não será realizado o armazenamento de mensagens abaixo do nível WARNING.


Métodos públicos de classe CLogger e macro LOG

Examinemos os métodos públicos de classe CLogger e macro LOG


Método void SetLevels(const ENUM_LOG_LEVEL logLevel, const ENUM_LOG_LEVEL notifyLevel). Define os níveis de registro em log.

const ENUM_LOG_LEVEL logLevel — nível de registro em log/mensagem que não deve ser inferior ao nível de armazenamento no arquivo-de-log/diário. Padrão = LOG_LEVEL_INFO.

const ENUM_LOG_LEVEL notifyLevel — nível de registro-em-log/mensagem que não deve ser inferior aos serão exibidos como notificações. Padrão = LOG_LEVEL_FATAL.

Valores possíveis para os dois:

  • LOG_LEVEL_DEBUG,
  • LOG_LEVEL_INFO,
  • LOG_LEVEL_WARNING,
  • LOG_LEVEL_ERROR,
  • LOG_LEVEL_FATAL.


Método void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod). Define o método de registro em log.

const ENUM_LOGGING_METHOD loggingMethod — método de registro em log. Padrão = LOGGING_OUTPUT_METHOD_EXTERN_FILE.

Valores possíveis:

  • LOGGING_OUTPUT_METHOD_EXTERN_FILE — arquivo externo,
  • LOGGING_OUTPUT_METHOD_PRINT — função Print.


Método void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod). Define o método de notificação.

const ENUM_NOTIFICATION_METHOD notificationMethod — método de notificações. Padrão = NOTIFICATION_METHOD_ALERT.

Valores possíveis:

  • NOTIFICATION_METHOD_NONE — habilitado,
  • NOTIFICATION_METHOD_ALERT — função Alert,
  • NOTIFICATION_METHOD_MAIL — função SendMail,
  • NOTIFICATION_METHOD_PUSH — função SendNotification.


Método void SetLogFileName(const string logFileName). Define o nome do arquivo de log.

const string logFileName — nome do arquivo de log. O valor padrão será o nome do programa no qual é empregado o registrador de logs (veja: método privado InitializeDefaultLogFileName()).


Método void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType). Define o tipo de restrição para o arquivo de log.

const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType - tipo de restrição para o arquivo de log. Valor padrão: LOG_FILE_LIMIT_TYPE_ONE_DAY.

Valores possíveis:

  • LOG_FILE_LIMIT_TYPE_ONE_DAY — novo arquivo de log para cada novo dia. Serão criados arquivos com os nomes my_log_USDCAD_D1_2015_09_21.log, my_log_USDCAD_D1_2015_09_22.log , my_log_USDCAD_D1_2015_09_23 .log, etc.
  • LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE — novo arquivo de log para cada novo megabyte. Serão criados arquivos com os nomes my_log_USDCAD_D1_0.log, my_log_USDCAD_D1_1.log, my_log_USDCAD_D1_2.log, etc. Ir para o próximo arquivo dependendo do atingimento do tamanho anterior em 1 megabytes.


Método void Add(const ENUM_LOG_LEVEL level,const string message). Adiciona a mensagem ao Log.

const ENUM_LOG_LEVEL level — nível de mensagem. Valores possíveis:

  • LOG_LEVEL_DEBUG
  • LOG_LEVEL_INFO
  • LOG_LEVEL_WARNING
  • LOG_LEVEL_ERROR
  • LOG_LEVEL_FATAL

const string message — texto da mensagem.


Além do método Add, também é implementado o macro LOG, ele adiciona, ao texto da mensagem, o nome do arquivo, assinatura da função e número da cadeia de caracteres a partir da qual é realizado o registro no arquivo de log:

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

Esta macro pode ser especialmente útil durante a depuração.

Assim, no exemplo é mostrado o mecanismo de registro em log que permite:

  1. Configurar os níveis de registro em log (DEBUG..FATAL).
  2. Definir o nível de mensagens quando os usuários devem ser notificados.
  3. Definir onde deve ser escrito o log, isto é, no diário dos EAs via Print() ou no arquivo externo.
  4. Para a saída para um arquivo externo é preciso especificar o nome do arquivo e definir as restrições para os arquivos de log: de acordo como o arquivo para cada data separada ou de acordo como o arquivo para cada megabyte de log.
  5. Definir o tipo de notificação (Alert(), SendMail(), SendNotify()).

A opção proposta é certamente apenas um exemplo; a modificação será exigida para determinadas tarefas (incluindo a remoção de funcionalidades desnecessárias). Por exemplo, além de registrar no arquivo externo e no diário comum, é possível adicionar esse método de manutenção de logs como saída de banco de dados.


Conclusão

Este artigo tratou de questões sobre o tratamento de erros e registro em log usando os recursos da MQL5. O correto tratamento de erros e o pertinente registro em log pode melhorar significativamente a qualidade do software desenvolvido e simplificar um possível suporte futuro.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/2041

Arquivos anexados |
logger.mqh (24.94 KB)
loggertest.mq5 (4.67 KB)
Últimos Comentários | Ir para discussão (1)
Ronei Toporcov
Ronei Toporcov | 20 jul 2017 em 16:14

Eu utilizo MT5. 

Quando utilizei seu exemplo em backtest, o "mylog" é escrito na pasta C:\Program Files\MetaTrader 5 Terminal\Tester\Agent-127.0.0.1-3000\MQL5\Files.

Para facilitar, pensei em colocar em uma pasta de uso pessoal: "C:\\MT5Pessoal\\Logs\\mylog", por exemplo.

Porém ocorre sempre o erro 5002, e o tamanho do nome não excede 255 bytes.

O que pode estar errado?

Obrigado.

Expert Advisor Universal: Integração com os Módulos de Sinais Padrão do MetaTrader (parte 7) Expert Advisor Universal: Integração com os Módulos de Sinais Padrão do MetaTrader (parte 7)
Esta parte do artigo descreve as possibilidades de integração do motor CStrategy com os módulos de sinais incluídos na biblioteca padrão no MetaTrader. O artigo descreve como trabalhar com sinais, assim como demonstra uma forma de criar estratégias personalizadas com base nas mesmas.
Mais uma vez vamos falar sobre mapas de Kohonen Mais uma vez vamos falar sobre mapas de Kohonen
O artigo descreve as técnicas para trabalhar com mapas de Kohonen. Ele vai ser do interesse tanto para exploradores do mercado, com habilidades básicas nas plataformas MQL4 e MQL5, quanto para programadores experientes que enfrentam dificuldades com a conexão dos mapas de Kohonen aos seus projetos.
Expressões regulares para traders Expressões regulares para traders
As expressões regulares são uma linguagem especial para manipulação de textos de acordo com uma regra definida, às vezes, chamada de padrão ou máscara de expressão regular. Este artigo mostrará como manipular o relatório de negociação usando a biblioteca RegularExpressions para MQL5 e demostrará seus resultados de otimização.
Interfaces Gráficas IV: O Modo Multi-Janela e o Sistema de Prioridades (Capítulo 2) Interfaces Gráficas IV: O Modo Multi-Janela e o Sistema de Prioridades (Capítulo 2)
Neste capítulo, nós vamos estender a implementação da biblioteca para possibilitar a criação de interfaces de multi-janela para as aplicações MQL. Nós também vamos desenvolver um sistema de prioridades para clique esquerdo do mouse sobre os objetos gráficos. Isso se faz necessário para evitar problemas quando os elementos não respondem as ações do usuário.