Dominando Registros de Log (Parte 5): Otimizando o Handler com Cache e Rotação
Introdução
No primeiro artigo desta série, Dominando Registros de Log (Parte 1): Conceitos Fundamentais e Primeiros Passos em MQL5, iniciamos a criação de uma biblioteca de log personalizada para o desenvolvimento de Expert Advisors (EAs). Nele, exploramos a motivação por trás da criação de uma ferramenta tão essencial: superar as limitações dos logs nativos do MetaTrader 5 e trazer uma solução robusta, personalizável e poderosa para o universo MQL5.
Para recapitular os principais pontos abordados, estabelecemos a base da nossa biblioteca definindo os seguintes requisitos fundamentais:
- Estrutura robusta utilizando o padrão Singleton, garantindo consistência entre os componentes do código.
- Persistência avançada para armazenar logs em bancos de dados, fornecendo histórico rastreável para auditorias e análises aprofundadas.
- Flexibilidade nas saídas, permitindo que os logs sejam armazenados ou exibidos de forma conveniente, seja no console, em arquivos, no terminal ou em um banco de dados.
- Classificação por níveis de log, diferenciando mensagens informativas de alertas críticos e erros.
- Personalização do formato de saída, para atender às necessidades únicas de cada desenvolvedor ou projeto.
Com essa base bem estabelecida, ficou claro que o framework de logging que estamos desenvolvendo será muito mais do que um simples registro de eventos; ele será uma ferramenta estratégica para compreender, monitorar e otimizar o comportamento dos EAs em tempo real.
Até aqui, exploramos os fundamentos dos logs, aprendemos como formatá-los e entendemos como os handlers controlam o destino das mensagens. No último artigo, aprendemos como salvar registros de log em um arquivo (.txt, .log ou .json). Agora, neste quinto artigo, vamos otimizar o processo de salvamento de logs em arquivos implementando cache e rotação de arquivos. Vamos começar então!
Adicionando um formatador a cada handler
Até agora, nossa biblioteca de logging gerencia a formatação das mensagens por meio de uma única instância da classe CFormatter, que é centralizada na base da biblioteca (CLogify). Essa abordagem funciona bem para cenários simples, mas limita a flexibilidade dos handlers.
Essa abordagem funciona bem para cenários simples, mas limita a flexibilidade dos handlers. Por exemplo, enquanto um handler que grava logs em JSON pode precisar de uma estrutura específica, um handler que imprime logs no console pode exigir um formato mais legível para humanos. A solução é mover a responsabilidade do formatador para a classe base do handler (CLogifyHandler). Dessa forma, cada handler pode ter seu próprio formatador independente, permitindo maior controle sobre a formatação das mensagens de log. Vamos implementar essa mudança e ver como ela melhora a flexibilidade da biblioteca.
Indo direto ao código, começamos adicionando uma instância de CFormatter dentro de CLogifyHandler; como esta é uma tarefa simples para você que leu os artigos anteriores, vou apenas adicionar o código final destacando o que foi adicionado:
//+------------------------------------------------------------------+ //| LogifyHandler.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../LogifyModel.mqh" #include "../Formatter/LogifyFormatter.mqh" //+------------------------------------------------------------------+ //| class : CLogifyHandler | //| | //| [PROPERTY] | //| Name : CLogifyHandler | //| Heritage : No heritage | //| Description : Base class for all log handlers. | //| | //+------------------------------------------------------------------+ class CLogifyHandler { protected: string m_name; ENUM_LOG_LEVEL m_level; CLogifyFormatter *m_formatter; public: CLogifyHandler(void); ~CLogifyHandler(void); //--- Handler methods 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 //--- Set/Get void SetLevel(ENUM_LOG_LEVEL level); void SetFormatter(CLogifyFormatter *format); string GetName(void); ENUM_LOG_LEVEL GetLevel(void); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandler::CLogifyHandler(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandler::~CLogifyHandler(void) { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter ; } } //+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandler::Emit(MqlLogifyModel &data) { } //+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandler::Flush(void) { } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandler::Close(void) { } //+------------------------------------------------------------------+ //| Set level | //+------------------------------------------------------------------+ void CLogifyHandler::SetLevel(ENUM_LOG_LEVEL level) { m_level = level; } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogifyHandler::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get name | //+------------------------------------------------------------------+ string CLogifyHandler::GetName(void) { return(m_name); } //+------------------------------------------------------------------+ //| Get level | //+------------------------------------------------------------------+ ENUM_LOG_LEVEL CLogifyHandler::GetLevel(void) { return(m_level); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogifyHandler::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
Continuando com as mudanças mais simples, removemos a instância de CFormatter em CLogify; as partes que foram removidas da classe estão destacadas em vermelho, e as que foram adicionadas estão destacadas em verde:
//+------------------------------------------------------------------+ //| Logify.mqh | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" #property version "1.00" #include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh" #include "Handlers/LogifyHandler.mqh" #include "Handlers/LogifyHandlerConsole.mqh" #include "Handlers/LogifyHandlerDatabase.mqh" #include "Handlers/LogifyHandlerFile.mqh" //+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; CLogifyHandler *m_handlers[]; public: CLogify(); ~CLogify(); //--- Handler void AddHandler(CLogifyHandler *handler); bool HasHandler(string name); CLogifyHandler *GetHandler(string name); CLogifyHandler *GetHandler(int index); int SizeHandlers(void); //--- Generic method for adding logs bool Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Specific methods for each log level bool Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogify::CLogify() { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogify::~CLogify() { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter; } //--- Delete handlers int size_handlers = ArraySize(m_handlers); for(int i=0;i<size_handlers;i++) { delete m_handlers[i]; } } //+------------------------------------------------------------------+ //| Add handler to handlers array | //+------------------------------------------------------------------+ void CLogify::AddHandler(CLogifyHandler *handler) { int size = ArraySize(m_handlers); ArrayResize(m_handlers,size+1); m_handlers[size] = GetPointer(handler); } //+------------------------------------------------------------------+ //| Checks if handler is already in the array by name | //+------------------------------------------------------------------+ bool CLogify::HasHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(true); } } return(false); } //+------------------------------------------------------------------+ //| Get handler by name | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(m_handlers[i]); } } return(NULL); } //+------------------------------------------------------------------+ //| Get handler by index | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(int index) { return(m_handlers[index]); } //+------------------------------------------------------------------+ //| Gets the total size of the handlers array | //+------------------------------------------------------------------+ int CLogify::SizeHandlers(void) { return(ArraySize(m_handlers)); } //+------------------------------------------------------------------+ //| Generic method for adding logs | //+------------------------------------------------------------------+ bool CLogify::Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { //--- If the formatter is not configured, the log will not be recorded. if(m_formatter == NULL) { return(false); } //--- Textual name of the log level string levelStr = ""; switch(level) { case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break; case LOG_LEVEL_INFOR: levelStr = "INFOR"; break; case LOG_LEVEL_ALERT: levelStr = "ALERT"; break; case LOG_LEVEL_ERROR: levelStr = "ERROR"; break; case LOG_LEVEL_FATAL: levelStr = "FATAL"; break; } //--- Creating a log template with detailed information datetime time_current = TimeCurrent(); MqlLogifyModel data("",levelStr,msg,args,time_current,time_current,level,origin,filename,function,line); data.formated = m_formatter.FormatLog(data); //--- Call handlers int size = this.SizeHandlers(); for(int i=0;i<size;i++) { data.formated = m_handlers[i].GetFormatter().FormatLog(data); m_handlers[i].Emit(data); } return(true); } //+------------------------------------------------------------------+ //| Debug level message | //+------------------------------------------------------------------+ bool CLogify::Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_DEBUG,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Infor level message | //+------------------------------------------------------------------+ bool CLogify::Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_INFOR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Alert level message | //+------------------------------------------------------------------+ bool CLogify::Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ALERT,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Error level message | //+------------------------------------------------------------------+ bool CLogify::Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Fatal level message | //+------------------------------------------------------------------+ bool CLogify::Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
A única parte que foi adicionada foi no momento de formatar a mensagem. Antes, utilizávamos o formatador dentro da própria classe. Com as alterações, em cada handler utilizamos o formatador fornecido pelo handler. Ao associar um formatador diretamente a cada handler, eliminamos a restrição de um único formato e tornamos a biblioteca mais adaptável a diferentes necessidades. Agora, cada destino pode ter um estilo de log específico, garantindo que a saída seja mais apropriada para o contexto em que será utilizada. No próximo tópico, veremos como gerenciar a execução de logs em ciclos programados com a classe CIntervalWatcher, que será uma classe auxiliar para a rotação de arquivos.
Criando a classe CIntervalWatcher
O principal objetivo da CIntervalWatcher é verificar se um determinado intervalo de tempo passou desde a última chamada. Isso é essencial para gerar logs que precisam ser verificados em intervalos de tempo específicos. Seja para evitar sobrecarga de escrita ou para estruturar melhor os registros, um mecanismo de controle de ciclos é essencial, evitando processamento desnecessário a cada tick. Ela permite configurar:
- O intervalo de tempo a ser monitorado (em segundos).
- A origem do tempo (tempo atual, GMT, local ou servidor de negociação).
- Se deve retornar true na primeira execução.
Dessa forma, a classe é útil para verificar quando executar uma ação periódica dentro da biblioteca. Vamos criar uma nova pasta chamada Utils, que conterá esse arquivo. Ao final, o navegador de arquivos deverá ficar assim:

Prosseguindo para a construção da classe, primeiro criamos um enum para dar suporte a diferentes origens de tempo, chamamos de ENUM_TIME_ORIGIN
//+------------------------------------------------------------------+ //| Enum for different time sources | //+------------------------------------------------------------------+ enum ENUM_TIME_ORIGIN { TIME_ORIGIN_CURRENT = 0, // [0] Current Time TIME_ORIGIN_GMT, // [1] GMT Time TIME_ORIGIN_LOCAL, // [2] Local Time TIME_ORIGIN_TRADE_SERVER // [3] Server Time }; //+------------------------------------------------------------------+
Adicionamos variáveis privadas à classe para armazenar o último instante registrado (m_last_time), o intervalo de tempo desejado (m_interval), a origem do tempo (m_time_origin) e um sinalizador (m_first_return) para controlar o primeiro retorno. Como consequência, criamos um Set e um Get para cada atributo privado. Para facilitar a configuração de intervalos, origem do tempo e primeiro retorno, decidi criar alguns construtores extras para a classe, auxiliando você, desenvolvedor. Abaixo está o código com os construtores e métodos para acessar e obter os dados privados.
//+------------------------------------------------------------------+ //| class : CIntervalWatcher | //| | //| [PROPERTY] | //| Name : CIntervalWatcher | //| Type : Report | //| Heritage : No heredirary. | //| Description : Monitoring new time periods | //| | //+------------------------------------------------------------------+ class CIntervalWatcher { private: //--- Auxiliary attributes ulong m_last_time; ulong m_interval; ENUM_TIME_ORIGIN m_time_origin; bool m_first_return; public: CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(void); ~CIntervalWatcher(void); //--- Setters void SetInterval(ENUM_TIMEFRAMES interval); void SetInterval(ulong interval); void SetTimeOrigin(ENUM_TIME_ORIGIN time_origin); void SetFirstReturn(bool first_return); //--- Getters ulong GetInterval(void); ENUM_TIME_ORIGIN GetTimeOrigin(void); bool GetFirstReturn(void); ulong GetLastTime(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = PeriodSeconds(interval); m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = interval; m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(void) { m_interval = 10; // 10 seconds m_time_origin = TIME_ORIGIN_CURRENT; m_first_return = true; m_last_time = 0; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CIntervalWatcher::~CIntervalWatcher(void) { } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ENUM_TIMEFRAMES interval) { m_interval = PeriodSeconds(interval); } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ulong interval) { m_interval = interval; } //+------------------------------------------------------------------+ //| Set time origin | //+------------------------------------------------------------------+ void CIntervalWatcher::SetTimeOrigin(ENUM_TIME_ORIGIN time_origin) { m_time_origin = time_origin; } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ void CIntervalWatcher::SetFirstReturn(bool first_return) { m_first_return=first_return; } //+------------------------------------------------------------------+ //| Get interval | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetInterval(void) { return(m_interval); } //+------------------------------------------------------------------+ //| Get time origin | //+------------------------------------------------------------------+ ENUM_TIME_ORIGIN CIntervalWatcher::GetTimeOrigin(void) { return(m_time_origin); } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ bool CIntervalWatcher::GetFirstReturn(void) { return(m_first_return); } //+------------------------------------------------------------------+ //| Set last time | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetLastTime(void) { return(m_last_time); } //+------------------------------------------------------------------+
Para auxiliar no método principal, vamos criar a função GetTime, que retorna o tempo com base na origem definida:
//+------------------------------------------------------------------+ //| Get time in miliseconds | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetTime(ENUM_TIME_ORIGIN time_origin) { switch(time_origin) { case(TIME_ORIGIN_CURRENT): return(TimeCurrent()); case(TIME_ORIGIN_GMT): return(TimeGMT()); case(TIME_ORIGIN_LOCAL): return(TimeLocal()); case(TIME_ORIGIN_TRADE_SERVER): return(TimeTradeServer()); } return(0); } //+------------------------------------------------------------------+
O método mais importante da classe é o Inspect(), que verifica se o intervalo definido foi atingido. A lógica é a seguinte: na primeira chamada, ele verifica se m_last_time é zero (classe recém-instanciada); a função armazena o tempo atual e retorna m_first_return. Se o timestamp armazenado for diferente do timestamp atual somado ao intervalo, isso significa que o intervalo foi atingido, então m_last_time é atualizado e a função retorna true. Se o timestamp for o mesmo, significa que ainda não foi atingido, então a função retorna false.
//+------------------------------------------------------------------+ //| Check if there was an update | //+------------------------------------------------------------------+ bool CIntervalWatcher::Inspect(void) { //--- Get time ulong time_current = this.GetTime(m_time_origin); //--- First call, initial return if(m_last_time == 0) { m_last_time = time_current; return(m_first_return); } //--- Check interval if(time_current >= m_last_time + m_interval) { m_last_time = time_current; return(true); } return(false); } //+------------------------------------------------------------------+
Com o CIntervalWatcher, temos um controle mais refinado sobre a geração de logs, permitindo ciclos programáveis e maior eficiência de processamento. Esse tipo de abordagem será essencial para uma biblioteca de logging que exige execução periódica de tarefas. Agora, com a execução periódica das ações de log configurada, podemos focar na otimização do processo de gravação e na manutenção do desempenho do sistema.
Otimizando o Salvamento de Logs: Cache e Rotação de Arquivos
Embora a gravação direta de logs em arquivos que implementamos no último artigo seja uma solução funcional, ela pode se tornar ineficiente à medida que o volume de logs cresce. Para evitar impactos negativos no desempenho, é necessário otimizar esse processo. Neste tópico, vamos explorar como implementar um sistema de cache e rotação de arquivos para garantir que os logs sejam gravados de forma eficiente, sem sobrecarregar o armazenamento e mantendo a integridade dos dados.
No último artigo, discutimos com mais detalhes como essas melhorias funcionam e suas vantagens:
“Imagine este cenário: um Expert Advisor executando por semanas ou meses, registrando cada evento, erro ou notificação no mesmo arquivo. Logo, esse log começa a atingir tamanhos consideráveis, tornando a leitura e interpretação das informações bastante complexas. É aí que entra a rotação. Ela nos permite dividir essas informações em partes menores e organizadas, tornando tudo muito mais fácil de ler e analisar.
As duas formas mais comuns de fazer isso são:
- Por Tamanho: você define um limite de tamanho, geralmente em megabytes (MB), para o arquivo de log. Quando esse limite é atingido, um novo arquivo é criado automaticamente, e o ciclo começa novamente. Essa abordagem é muito prática quando o foco está em controlar o crescimento dos logs, sem a necessidade de se prender a um calendário. Assim que o arquivo de log atual atinge o limite de tamanho (em megabytes), ocorre o seguinte fluxo: o arquivo de log atual é renomeado, recebendo um índice, como “log1.log”. Os arquivos existentes no diretório também são renumerados, como “log1.log” passando a se chamar “log2.log”. Se o número de arquivos atingir o máximo permitido, os arquivos mais antigos são excluídos. Essa abordagem é útil para limitar tanto o espaço ocupado pelos logs quanto a quantidade de arquivos salvos.
- Por Data: neste caso, um novo arquivo de log é criado todos os dias. Cada um possui em seu nome a data em que foi criado, por exemplo log_2025-01-19.log, o que já resolve grande parte do problema de organização dos logs. Essa abordagem é perfeita quando você precisa analisar um dia específico, sem se perder em um único arquivo gigantesco. Este é o método que mais utilizo ao salvar logs dos meus Expert Advisors, tudo fica mais limpo, mais direto e mais fácil de navegar.
Além disso, você também pode limitar o número de arquivos de log armazenados. Esse controle é muito importante para evitar o acúmulo desnecessário de registros antigos. Imagine que você configure para manter os 30 arquivos mais recentes. Quando o 31º surgir, o sistema descarta automaticamente o mais antigo, o que evita que logs muito antigos se acumulem no disco, mantendo apenas os mais recentes.
Outro detalhe crucial é o uso de um cache. Em vez de gravar cada mensagem diretamente no arquivo assim que ela chega, as mensagens são armazenadas temporariamente no cache. Quando o cache atinge um limite definido, todo o conteúdo é gravado no arquivo de uma só vez. Isso resulta em menos operações de leitura e escrita no disco, maior desempenho e uma vida útil mais longa para seus dispositivos de armazenamento.”
Para implementar a rotação de arquivos de log, primeiro precisamos de um método auxiliar chamado SearchForFilesInDirectory(). Esse método é responsável por pesquisar todos os arquivos presentes em um diretório específico e retornar seus nomes em um array. Ele utiliza a função FileFindFirst() para iniciar a busca e, à medida que encontra arquivos, seus nomes são adicionados a esse array. Após a conclusão do processo, o método fecha o manipulador de busca utilizando FileFindClose().
Mas por que esse método é tão importante? Simples! Ele nos permite listar os arquivos de log existentes, garantindo que a classe que gerencia os logs exclua arquivos mais antigos quando necessário.
class CLogifyHandlerFile : public CLogifyHandler { private: bool SearchForFilesInDirectory(string directory, string &file_names[]); }; //+------------------------------------------------------------------+ //| Returns an array with the names of all files in the directory | //+------------------------------------------------------------------+ bool CLogifyHandlerFile::SearchForFilesInDirectory(string directory,string &file_names[]) { //--- Search for all log files in the specified directory with the given file extension string file_name; long search_handle = FileFindFirst(directory,file_name); ArrayFree(file_names); bool is_found = false; if(search_handle != INVALID_HANDLE) { do { //--- Add each file name found to the array of file names int size_file = ArraySize(file_names); ArrayResize(file_names,size_file+1); file_names[size_file] = file_name; is_found = true; } while(FileFindNext(search_handle,file_name)); FileFindClose(search_handle); } return(is_found); } //+------------------------------------------------------------------+
Agora que temos a função para buscar os arquivos, podemos incorporá-la ao método principal responsável por emitir os logs, o Emit(). Dependendo da configuração de rotação escolhida, a lógica será ajustada.
Se a rotação de logs estiver configurada para ocorrer com base no tamanho do arquivo, a função:
- Verifica se o tamanho do arquivo excedeu o limite configurado (m_config.max_file_size_mb).
- Pesquisa todos os arquivos de log no diretório.
- Remove arquivos antigos que excedem o número máximo permitido (m_config.max_file_count).
- Renomeia arquivos antigos, incrementando numericamente seus índices (log1.txt, log2.txt, etc.).
- Renomeia o arquivo de log atual como "log1" para manter a sequência.
Se a rotação for baseada em data, a função:
- Pesquisa todos os arquivos de log no diretório.
- Exclui os arquivos mais antigos que excedem o número máximo permitido (m_config.max_file_count).
Agora, vamos ver a implementação do método Emit() com ambas as lógicas de rotação de logs:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Get the full path of the file string log_path = this.LogPath(); //--- Open file ResetLastError(); int handle_file = m_file.Open(log_path, FILE_READ | FILE_WRITE | FILE_ANSI); if(handle_file == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Garanta que o diretório exista e seja gravável. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Write m_file.Seek(0, SEEK_END); m_file.WriteString(data.formated + "\n"); //--- Size in megabytes ulong size_mb = m_file.Size() / (1024 * 1024); //--- Close file m_file.Close(); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } } //+------------------------------------------------------------------+
Salvando por blocos para melhor desempenho
Prosseguindo para outra melhoria, vamos criar a lógica que considero a mais interessante do artigo: salvar registros por blocos. A ideia central é implementar um cache (memória temporária), onde os registros de log serão armazenados até atingirem um limite definido. Quando esse limite é atingido, todos os registros no cache são salvos no arquivo de log de uma só vez.
Implementaremos essa lógica em etapas. Primeiro, criaremos a estrutura de cache na classe CLogifyHandlerFile. Na seção privada da classe, adicionaremos um array do tipo MqlLogifyModel para armazenar temporariamente os registros de log. Também incluímos uma variável para controlar o índice atual do último valor salvo no cache. Sempre que um novo registro for adicionado, esse índice será incrementado. Também criamos uma instância da classe CIntervalWatcher e definimos um intervalo de um dia no construtor. Veja como fica:
class CLogifyHandlerFile : public CLogifyHandler { private: //--- Update utilities CIntervalWatcher m_interval_watcher; //--- Cache data MqlLogifyModel m_cache[]; int m_index_cache; }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::CLogifyHandlerFile(void) { m_interval_watcher.SetInterval(PERIOD_D1); ArrayFree(m_cache); m_index_cache = 0; } //+------------------------------------------------------------------+
Com a estrutura de cache e atualização criada, avançamos para o próximo passo: modificar o método Emit() para utilizar o cache.
O método Emit() é responsável por processar uma mensagem de log e enviá-la ao destino configurado (neste caso, um arquivo). Vamos adaptá-lo para que, em vez de salvar os dados diretamente no arquivo, ele os armazene temporariamente no cache. Quando o cache atinge o limite configurado, ou o intervalo definido (um dia), o método chama a função Flush(), que salva os registros acumulados no arquivo. Esse intervalo é útil porque, se os dados permanecerem em cache por mais de um dia, esse mecanismo garante que eles ainda sejam salvos diariamente, além de permitir que a rotina de rotação seja executada todos os dias.
Aqui está o código modificado:
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::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(); } } } } //+------------------------------------------------------------------+
A função Flush() é responsável por salvar os dados do cache no arquivo. Esse processo envolve abrir o arquivo, posicionar o ponteiro no final e gravar todos os registros armazenados no cache.
//+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Flush(void) { //--- Get the full path of the file string log_path = this.LogPath(); //--- Open file ResetLastError(); int handle_file = FileOpen(log_path, FILE_READ|FILE_WRITE|FILE_ANSI, '\t', m_config.codepage); if(handle_file == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Garanta que o diretório exista e seja gravável. (Code: "+IntegerToString(GetLastError())+")"); return; } //--- Loop through all cached messages int size = ArraySize(m_cache); for(int i=0;i<size;i++) { if(m_cache[i].timestamp > 0) { //--- Point to the end of the file and write the message FileSeek(handle_file, 0, SEEK_END); FileWrite(handle_file, m_cache[i].formated); } } //--- Size in megabytes ulong size_mb = FileSize(handle_file) / (1024 * 1024); //--- Close file FileClose(handle_file); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } //+------------------------------------------------------------------+
Com essa implementação, criamos uma solução de logging eficiente e escalável, capaz de lidar com grandes volumes de dados sem comprometer o desempenho do seu expert. Por fim, precisamos garantir que, quando o programa for encerrado, todos os dados em cache sejam salvos no arquivo. Para isso, basta chamar o método Flush() no método Close(), que já é chamado no destrutor da classe base CLogify.
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::~CLogifyHandlerFile(void) { this.Close(); } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Close(void) { //--- Save cache Flush(); } //+------------------------------------------------------------------+
Ao implementar cache e rotação de arquivos, reduzimos o número de operações de escrita no disco e garantimos que nossos logs sejam armazenados de forma mais eficiente. Isso confere desempenho e escalabilidade à nossa biblioteca, tornando-a mais robusta para aplicações reais. Mas será que essas otimizações realmente fazem diferença? Vamos testar.
Testes de Desempenho: Medindo a Eficiência das Melhorias
Agora que implementamos as otimizações, precisamos medir seu impacto real. Os testes de desempenho nos ajudarão a entender se o cache está reduzindo a carga de escrita e se a rotação de arquivos está funcionando conforme esperado. Para isso, executaremos o mesmo teste realizado no último artigo, comparando a versão original da biblioteca com a versão otimizada.
Para executar o teste, utilizaremos o mesmo arquivo, com algumas modificações no formatter, já que agora cada handler possui seu próprio formatter. As alterações estão destacadas da seguinte forma:
- Verde: Adições ao código
- Vermelho: Remoções
- Amarelo: O parâmetro que define o tamanho do cache. Quanto maior o cache, mais rápido o processamento.
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> CLogify logify; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,10); //--- Handler File CLogifyHandlerFile *handler_file = new CLogifyHandlerFile(); handler_file.SetConfig(m_config); handler_file.SetLevel(LOG_LEVEL_DEBUG); handler_file.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_file); logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Vamos iniciar um teste no testador de estratégias utilizando os mesmos parâmetros de data e símbolo.

Utilizando o modelo "OHLC para 1 minuto" no símbolo EURUSD e um período de 7 dias, o tempo de execução foi de 26 segundos. Vale destacar que, a cada tick, um novo registro de log é gerado, e o cache está configurado para armazenar 10 mensagens. Agora, vamos aumentar o cache para 100 mensagens e observar a diferença de desempenho:

Com essa alteração, conseguimos reduzir o tempo do teste em 2 segundos, mantendo a mesma modelagem, data e configurações de símbolo. Se compararmos com o primeiro teste realizado no artigo anterior, que levou 5 minutos e 11 segundos, a melhoria é impressionante!
Os resultados demonstram que pequenas otimizações podem gerar ganhos significativos de eficiência. A combinação de cache e rotação de arquivos torna o gerenciamento de logs mais ágil e confiável, validando as escolhas feitas até aqui. Mas como essas melhorias podem ser aplicadas na prática? Vamos explorar alguns exemplos de uso.
Exemplos de Uso da Biblioteca de Log
Agora que aprimoramos nossa biblioteca de log, é hora de colocá-la em ação! Vamos explorar exemplos práticos de como utilizá-la para criar diferentes tipos de arquivos de log, cada um com sua própria formatação e nível de severidade.
Exemplo 1: Separar Logs em Arquivos .log e .json
No primeiro cenário, configuramos dois arquivos de log: um no formato .log e outro no formato .json. Cada um possui um formato específico e um nível de severidade diferente, facilitando o gerenciamento e a análise dos logs.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ALERT); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Aqui, utilizamos a mesma variável de configuração m_config, alterando apenas os valores necessários para definir ambos os formatos de log. Isso torna a configuração mais simples e reutilizável.
Exemplo 2: Armazenando Apenas Erros em um Arquivo JSON
Agora, vamos um passo além e configurar um log específico para armazenar apenas mensagens de erro. Para isso, criamos uma pasta separada onde esse arquivo .json será salvo. Além disso, adicionamos um handler de console para exibir os logs diretamente no terminal.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs\\error",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ERROR); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Handler Console CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole(); handler_console.SetLevel(LOG_LEVEL_DEBUG); handler_console.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname} | {origin}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); logify.AddHandler(handler_console); //--- Using logs logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14"); logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1"); logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678"); logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance"); logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Neste exemplo, utilizamos três handlers de log:
- Arquivo .log → Armazena logs no formato tradicional.
- Arquivo .json → Armazena apenas mensagens de erro dentro de uma pasta separada.
- Console → Exibe logs de forma mais legível para o usuário.
Utilizar um formatador mais “humano” no console ajuda a tornar a saída mais compreensível, enquanto o JSON de erros facilita a análise posterior.
Com esses exemplos, fica claro como nossa biblioteca de logging pode ser aplicada em projetos reais. A flexibilidade para criar diferentes formatos e níveis de severidade permite um bom gerenciamento, ajudando a identificar e solucionar problemas com mais facilidade. Além disso, a estrutura modular facilita a expansão do sistema de logging conforme necessário.
Agora, tudo o que você precisa fazer é adaptar essa implementação às suas necessidades e garantir que seus logs estejam sempre bem organizados e acessíveis!
Conclusão
Neste artigo, evoluímos nossa biblioteca de logging, tornando-a mais eficiente, escalável e adaptável. Refinamos a formatação, permitindo que cada handler tenha seu próprio formatador, tornando as mensagens mais organizadas e flexíveis para diferentes necessidades, como depuração local e auditoria.
Implementamos a classe CIntervalWatcher, que controla os ciclos de execução, garantindo que os logs sejam gravados e rotacionados em intervalos bem definidos. Também otimizamos a escrita com cache, reduzindo operações de disco e gerenciando melhor o crescimento dos arquivos. Validamos essas melhorias com testes de desempenho, refinando ainda mais a solução para suportar alta carga. Além disso, apresentamos exemplos práticos para facilitar a adoção da biblioteca.
Se há uma principal lição a ser extraída deste artigo, é a importância de tratar o logging como um aspecto essencial do desenvolvimento de software. Um sistema de logging bem projetado não apenas facilita a depuração e auditorias posteriores, como também auxilia na segurança, rastreabilidade e confiabilidade de um Expert Advisor. Implementar boas práticas de logging desde o início do desenvolvimento pode poupar dores de cabeça, tornando a manutenção mais simples e a resolução de problemas mais eficiente. No próximo artigo, exploraremos como armazenar logs em um banco de dados para análises avançadas. Até lá!
| Nome do Arquivo | Descrição |
|---|---|
| Experts/Logify/LogiftTest.mq5 | Arquivo onde testamos os recursos da biblioteca, contendo um exemplo prático |
| Include/Logify/Formatter/LogifyFormatter.mqh | Classe responsável por formatar registros de log, substituindo placeholders por valores específicos |
| Include/Logify/Handlers/LogifyHandler.mqh | Classe base para gerenciar handlers de log, incluindo definição de nível e envio de logs |
| Include/Logify/Handlers/LogifyHandlerConsole.mqh | Handler de log que envia logs formatados diretamente para o console do terminal no MetaTrader |
| Include/Logify/Handlers/LogifyHandlerDatabase.mqh | Handler de log que envia logs formatados para um banco de dados (atualmente contém apenas uma impressão, mas em breve salvaremos em um banco sqlite real) |
| Include/Logify/Handlers/LogifyHandlerFile.mqh | Handler de log que envia logs formatados para um arquivo |
| Include/Logify/Utils/IntervalWatcher.mqh | Verifica se um intervalo de tempo foi atingido, permitindo criar rotinas dentro da biblioteca |
| Include/Logify/Logify.mqh | Classe principal para gerenciamento de logs, integrando níveis, modelos e formatação |
| Include/Logify/LogifyLevel.mqh | Arquivo que define os níveis de log da biblioteca Logify, permitindo controle detalhado |
| Include/Logify/LogifyModel.mqh | Estrutura que modela registros de log, incluindo detalhes como nível, mensagem, timestamp e contexto |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17137
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Redes neurais em trading: Extração eficiente de características para classificação precisa (Mantis)
Criando um Painel de Administração de Trading em MQL5 (Parte IX): Organização de Código (I)
Do básico ao intermediário: Indicadores técnicos (I)
Busca oscilatória determinística — Deterministic Oscillatory Search (DOS)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso