Ferramentas personalizadas de depuração e profiling para desenvolvimento em MQL5 (Parte I): Logging avançado
Este é o plano:
- Introdução
- Criação de um framework personalizado de logging
- Uso da camada de logging
- Vantagens da camada personalizada de logging
- Conclusão
Introdução
Quem já passou algum tempo desenvolvendo EAs, indicadores ou scripts em MQL5 conhece bem esta frustração: uma operação em conta real apresenta um comportamento estranho, uma fórmula complexa retorna um valor incorreto ou o EA simplesmente para de funcionar justamente quando o mercado fica mais agitado. A solução rápida de sempre, espalhar chamadas a Print() pelo código, executar o testador de estratégias e torcer para que o problema apareça, deixa de funcionar assim que a base de código começa a crescer.
O MQL5 impõe desafios de depuração que não são comuns em linguagens de programação convencionais. Os programas de trading funcionam em tempo real, portanto o tempo importa; operam com dinheiro real, portanto os erros custam caro; e precisam continuar extremamente rápidos mesmo em mercados voláteis. As ferramentas integradas do MetaEditor, como o depurador com execução passo a passo, Print() e Comment() para saída básica e o profiler de alto nível, são úteis, mas genéricas. Elas simplesmente não foram projetadas para o nível de diagnóstico preciso de que seus algoritmos de trading precisam.
É por isso que criar seu próprio conjunto de ferramentas de depuração e profiling pode transformar completamente o processo de desenvolvimento. Utilitários especializados permitem análises mais profundas e fluxos de trabalho adaptados às suas necessidades, recursos que não fazem parte do conjunto padrão. Com isso, você consegue localizar erros com mais rapidez, otimizar o desempenho e manter a qualidade do código sob controle.
Esta série de artigos mostrará como criar exatamente esse conjunto de ferramentas. Começaremos pela base: uma camada flexível de logging, muito mais poderosa do que chamadas dispersas a Print().. Em seguida, adicionaremos recursos avançados de depuração, profilers personalizados, uma infraestrutura de testes unitários e analisadores estáticos de código. Ao final da série, você terá um pacote completo capaz de substituir a correção emergencial de erros por um controle de qualidade proativo.
Cada parte deste guia é prática: exemplos completos e prontos para uso em MQL5, explicações detalhadas sobre seu funcionamento e a justificativa de cada decisão de projeto. Você terá ferramentas que poderá aplicar imediatamente e o conhecimento necessário para adaptá-las aos seus próprios projetos.
A seguir, vamos tratar da necessidade de diagnóstico mais básica de todas: acompanhar exatamente o que o programa está fazendo a cada instante. Vamos criar essa camada personalizada de logging.
Criação de um framework personalizado de logging
Nesta seção, desenvolveremos uma camada de logging flexível e poderosa, que vai muito além da função básica Print() oferecida pelo MQL5. Nosso logger personalizado terá suporte a vários formatos de saída, níveis de severidade e informações de contexto, tornando a depuração de sistemas de trading complexos muito mais eficiente.
Por que Print() sozinho não é suficiente
Antes de colocar a mão na massa e começar a criar um novo sistema, vale entender por que usar apenas Print() não é adequado para projetos profissionais:
- Ausência de hierarquia de severidade - todas as mensagens acabam no mesmo lugar, fazendo com que alertas críticos se percam entre registros rotineiros.
- Pouco contexto - Print não informa qual função gerou a mensagem nem qual era o estado da aplicação naquele momento.
- Saída para um único destino - tudo é enviado apenas para a guia "Experts"; não há um mecanismo integrado para gravar em arquivos ou direcionar os registros a outros destinos.
- Nenhuma filtragem - não é possível desativar logs detalhados de depuração em produção sem também ocultar mensagens de erro importantes.
- Texto não estruturado - a saída em formato livre dificulta a análise automática por outras ferramentas.
Nossa camada de logging personalizada resolve cada um desses problemas e estabelece uma base sólida para diagnosticar falhas em sistemas de trading complexos.
Projeto da arquitetura do logger
Vamos construir um sistema limpo, modular e orientado a objetos, composto por três componentes principais:- LogLevels (níveis de logging): uma enumeração (enum) que define os níveis de severidade (DEBUG, INFO, WARN, ERROR, FATAL).
- ILogHandler (interface de handler): uma interface que permite conectar diferentes destinos de saída, como FileLogHandler (gravação em arquivo) ou ConsoleLogHandler (saída para o console/guia "Experts").
- CLogger (logger): um objeto singleton que gerencia os handlers e disponibiliza a API de logging.
A seguir, veremos cada componente em detalhes.
Níveis de logging
Primeiro, definiremos os níveis de severidade no arquivo LogLevels.mqh:
enum LogLevel { LOG_LEVEL_DEBUG = 0, // Detailed information for debugging purposes. LOG_LEVEL_INFO = 1, // General information about the system's operation. LOG_LEVEL_WARN = 2, // Warnings about potential issues that are not critical. LOG_LEVEL_ERROR = 3, // Errors that affect parts of the system but allow continuity. LOG_LEVEL_FATAL = 4, // Serious problems that interrupt the system's execution. LOG_LEVEL_OFF = 5 // Turn off logging. };
Esses níveis permitem classificar as mensagens de acordo com sua importância e filtrá-las conforme necessário. Durante o desenvolvimento, por exemplo, você pode querer visualizar todas as mensagens, incluindo DEBUG. Em produção, porém, talvez seja mais conveniente exibir apenas WARN e níveis superiores.
Interface de handler
Em seguida, definiremos a interface dos handlers de log no arquivo ILogHandler.mqh:
#property strict #include "LogLevels.mqh" #include <Arrays/ArrayObj.mqh> // For managing handlers //+------------------------------------------------------------------+ //| Interface: ILogHandler | //| Description: Defines the contract for log handling mechanisms. | //| Each handler is responsible for processing and | //| outputting log messages in a specific way (e.g., to | //| console, file, database). | //+------------------------------------------------------------------+ interface ILogHandler { //--- Method to configure the handler with specific settings virtual bool Setup(const string settings=""); //--- Method to process and output a log message virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0); //--- Method to perform any necessary cleanup virtual void Shutdown(); }; //+------------------------------------------------------------------+
O arquivo de cabeçalho ILogHandler.mqh define um dos componentes centrais da camada de logging: a interface ILogHandler. Em MQL5, uma interface funciona como um contrato que estabelece o conjunto de métodos que qualquer classe que implemente essa interface deve fornecer. O objetivo da ILogHandler é padronizar a forma como diferentes mecanismos de saída de log, como gravação no console ou em arquivo, interagem com a classe principal do logger.
A própria interface ILogHandler declara três métodos virtuais que devem ser implementados pelas classes concretas de handler:- virtual bool Setup(const string settings=""): este método é responsável por inicializar e configurar um handler de log específico. Ele recebe um argumento string opcional (settings), que pode ser usado para fornecer parâmetros de configuração ao handler durante sua configuração, como caminhos de arquivos, strings de formatação ou níveis mínimos de logging. O método retorna true quando a configuração é concluída com sucesso e false caso contrário, permitindo que o logger principal saiba se o handler está pronto para uso.
- virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0): este é o método principal, responsável por processar e gerar a saída de uma única mensagem de log. Ele recebe todas as informações necessárias sobre o evento de logging: a marca de tempo (time), o nível de severidade (level, definido em LogLevels.mqh), a origem da mensagem (origin), o texto da mensagem de log (message) e, opcionalmente, o identificador do EA (expert_id). Cada classe que implementar a interface decidirá como essas informações serão formatadas e para onde serão enviadas, de acordo com sua finalidade específica, por exemplo, usando Print para exibir a mensagem no console ou gravando-a em um arquivo.
- virtual void Shutdown(): este método executa as operações de limpeza e liberação de recursos quando o handler de log deixa de ser necessário, normalmente durante o encerramento do logger principal ou da própria aplicação. As implementações podem usá-lo para fechar handles de arquivos abertos, liberar recursos alocados ou forçar a gravação dos dados armazenados em buffer, garantindo que todos os logs sejam gravados antes do encerramento do programa.
Ao definir essa interface padronizada, a camada de logging ganha flexibilidade e extensibilidade. A classe principal CLogger pode gerenciar uma coleção de diferentes objetos ILogHandler e encaminhar mensagens de log para cada um deles sem precisar conhecer os detalhes específicos de funcionamento de cada handler. Novos destinos de saída podem ser adicionados simplesmente criando novas classes que implementem a interface ILogHandler.
Handler de saída para o console
Este arquivo de cabeçalho fornece a classe ConsoleLogHandler, uma implementação concreta da interface ILogHandler. Sua função é encaminhar mensagens de log formatadas para a guia "Experts" da plataforma MetaTrader 5, que funciona como área de saída do console durante a execução de um EA ou script.
#property strict #include "ILogHandler.mqh" #include "LogLevels.mqh" //+------------------------------------------------------------------+ //| Class: ConsoleLogHandler | //| Description: Implements ILogHandler to output log messages to | //| the MetaTrader 5 Experts tab (console). | //+------------------------------------------------------------------+ class ConsoleLogHandler : public ILogHandler { private: LogLevel m_min_level; // Minimum level to log string m_format; // Log message format string //--- Helper to format the log message string FormatMessage(const datetime time, const LogLevel level, const string origin, const string message); //--- Helper to get string representation of LogLevel string LogLevelToString(const LogLevel level); public: ConsoleLogHandler(const LogLevel min_level = LOG_LEVEL_INFO, const string format = "[{time}] {level}: {origin} - {message}"); ~ConsoleLogHandler(); //--- ILogHandler implementation virtual bool Setup(const string settings="") override; virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) override; virtual void Shutdown() override; //--- Setters void SetMinLevel(const LogLevel level) { m_min_level = level; } void SetFormat(const string format) { m_format = format; } };
A classe ConsoleLogHandler herda publicamente de ILogHandler, o que significa que deve fornecer implementações para os métodos Setup, Log e Shutdown definidos pela interface. Ela contém duas variáveis-membro privadas: m_min_level, do tipo LogLevel, armazena o nível mínimo de severidade que uma mensagem deve ter para ser registrada por esse handler; e m_format, do tipo string, contém o modelo usado para formatar a mensagem de saída. A classe também declara os métodos auxiliares privados FormatMessage e LogLevelToString, além dos métodos públicos que implementam a interface e dos setters usados para configurar seus membros privados.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ ConsoleLogHandler::ConsoleLogHandler(const LogLevel min_level = LOG_LEVEL_INFO, const string format = "[{time}] {level}: {origin} - {message}") { m_min_level = min_level; m_format = format; // No specific setup needed for console logging initially } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ ConsoleLogHandler::~ConsoleLogHandler() { // No specific cleanup needed }
O construtor inicializa um novo objeto ConsoleLogHandler. Ele recebe dois argumentos opcionais: min_level, cujo valor padrão é LOG_LEVEL_INFO, e format, cujo padrão é o modelo "[{time}] {level}: {origin} - {message}". Esses argumentos definem, respectivamente, os valores iniciais das variáveis-membro m_min_level e m_format. Assim, o nível de filtragem e o formato da saída podem ser configurados no momento da criação do handler.
O destrutor é responsável pela liberação de recursos quando o objeto ConsoleLogHandler é destruído. Nesta implementação específica, a classe não gerencia diretamente recursos alocados dinamicamente nem handles abertos. Por isso, o corpo do destrutor permanece vazio, já que esse handler não exige nenhuma rotina especial de limpeza.
//+------------------------------------------------------------------+ //| Setup | //+------------------------------------------------------------------+ bool ConsoleLogHandler::Setup(const string settings="") { // Settings could be used to parse format or min_level, but we use constructor args for now // Example: Parse settings string if needed return true; } //+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void ConsoleLogHandler::Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) { // Check if the message level meets the minimum requirement if(level >= m_min_level && level < LOG_LEVEL_OFF) { // Format and print the message to the Experts tab Print(FormatMessage(time, level, origin, message)); } } //+------------------------------------------------------------------+ //| Shutdown | //+------------------------------------------------------------------+ void ConsoleLogHandler::Shutdown() { // No specific shutdown actions needed for console logging PrintFormat("%s: ConsoleLogHandler shutdown.", __FUNCTION__); }
- Método Setup (ConsoleLogHandler::Setup):
Este método implementa a função Setup exigida pela interface ILogHandler. Embora tenha sido projetado para permitir configuração, a implementação atual não utiliza o parâmetro string settings, pois os principais ajustes, como o nível mínimo e o formato, são definidos pelo construtor. O método simplesmente retorna true, indicando que o handler é considerado pronto para uso assim que é criado. - Método Log (ConsoleLogHandler::Log):
Esta é a principal implementação do registro de mensagens no console. Quando o método é chamado pela classe CLogger, ele primeiro verifica se o nível da mensagem recebida é maior ou igual ao valor de m_min_level configurado para o handler e, ao mesmo tempo, menor que LOG_LEVEL_OFF. Se a mensagem passar por esse filtro, o método chama a função auxiliar privada FormatMessage para montar a string final de saída com base no modelo armazenado em m_format e nos dados de logging fornecidos ao método, como time, level, origin e message. Por fim, ele usa a função nativa Print do MQL5 para exibir a mensagem formatada na guia "Experts". - Método Shutdown (ConsoleLogHandler::Shutdown):
Este método implementa a função Shutdown definida pela interface. Assim como no destrutor, o logging no console normalmente não exige operações específicas de encerramento, como o fechamento de arquivos. Nesta implementação, o método apenas exibe uma mensagem informando que o handler de console está sendo finalizado, fornecendo uma confirmação durante a sequência de encerramento da aplicação.
//+------------------------------------------------------------------+ //| FormatMessage | //+------------------------------------------------------------------+ string ConsoleLogHandler::FormatMessage(const datetime time, const LogLevel level, const string origin, const string message) { string formatted_message = m_format; // Replace placeholders StringReplace(formatted_message, "{time}", TimeToString(time, TIME_DATE | TIME_SECONDS)); StringReplace(formatted_message, "{level}", LogLevelToString(level)); StringReplace(formatted_message, "{origin}", origin); StringReplace(formatted_message, "{message}", message); return formatted_message; } //+------------------------------------------------------------------+ //| LogLevelToString | //+------------------------------------------------------------------+ string ConsoleLogHandler::LogLevelToString(const LogLevel level) { switch(level) { case LOG_LEVEL_DEBUG: return "DEBUG"; case LOG_LEVEL_INFO: return "INFO"; case LOG_LEVEL_WARN: return "WARN"; case LOG_LEVEL_ERROR: return "ERROR"; case LOG_LEVEL_FATAL: return "FATAL"; default: return "UNKNOWN"; } } //+------------------------------------------------------------------+
- Método auxiliar (FormatMessage):
Esta função auxiliar privada recebe como entrada os dados originais do log, como time, level, origin e message, além da string de formato do handler armazenada em m_format. Em seguida, substitui placeholders como {time}, {level}, {origin} e {message} pelos respectivos valores reais. A marca de tempo é formatada com TimeToString, enquanto LogLevelToString é chamada para obter a representação textual do nível de severidade. A string final, já totalmente formatada, é então retornada ao método Log para ser exibida. - Método auxiliar (LogLevelToString):
Esta função auxiliar privada converte um valor da enumeração LogLevel em sua representação textual correspondente. Por exemplo, LOG_LEVEL_INFO é convertido em "INFO". Para isso, utiliza uma instrução switch que trata os níveis de log definidos e retorna "UNKNOWN" caso receba algum valor inesperado. Dessa forma, os níveis de severidade aparecem de maneira clara e legível na saída formatada do log. - Métodos setters (SetMinLevel, SetFormat): Esses métodos públicos permitem alterar a configuração do handler depois de sua criação. SetMinLevel atualiza a variável-membro m_min_level, modificando o limiar de filtragem aplicado às mensagens de log seguintes. SetFormat, por sua vez, atualiza m_format e altera o modelo usado para formatar as próximas mensagens.
Handler de log em arquivo
Este arquivo de cabeçalho contém a classe FileLogHandler, outra implementação concreta da interface ILogHandler. Esse handler foi projetado para manter logs persistentes, gravando mensagens formatadas em arquivos. Em comparação com o handler de console, ele oferece recursos mais avançados, como rotação automática dos arquivos de log com base na data e no tamanho, além do controle da quantidade de arquivos mantidos.
#property strict #include "ILogHandler.mqh" #include "LogLevels.mqh" //+------------------------------------------------------------------+ //| Class: FileLogHandler | //| Description: Implements ILogHandler to output log messages to | //| files with rotation capabilities. | //+------------------------------------------------------------------+ class FileLogHandler : public ILogHandler { private: LogLevel m_min_level; // Minimum level to log string m_format; // Log message format string string m_file_path; // Base path for log files string m_file_prefix; // Prefix for log file names int m_file_handle; // Current file handle datetime m_current_day; // Current day for rotation int m_max_size_kb; // Maximum file size in KB before rotation int m_max_files; // Maximum number of log files to keep //--- Helper to format the log message string FormatMessage(const datetime time, const LogLevel level, const string origin, const string message); //--- Helper to get string representation of LogLevel string LogLevelToString(const LogLevel level); //--- Helper to create or rotate log file bool EnsureFileOpen(); //--- Helper to generate file name based on date string GenerateFileName(const datetime time); //--- Helper to perform log rotation void RotateLogFiles(); //--- Helper to check if file size exceeds limit bool IsFileSizeExceeded(); // Add custom helper function to sort string arrays void SortStringArray(string &arr[]); //--- New helper to clean file paths string CleanPath(const string path); public: FileLogHandler(const string file_path="MQL5\\Logs", const string file_prefix="EA_Log", const LogLevel min_level=LOG_LEVEL_INFO, const string format="[{time}] {level}: {origin} - {message}", const int max_size_kb=1024, const int max_files=5); virtual ~FileLogHandler(); //--- ILogHandler implementation virtual bool Setup(const string settings="") override; virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) override; virtual void Shutdown() override; //--- Setters void SetFilePath(const string path) { m_file_path = CleanPath(path); } void SetMinLevel(const LogLevel level) { m_min_level = level; } void SetFormat(const string format) { m_format = format; } void SetFilePrefix(const string prefix){ m_file_prefix = prefix; } void SetMaxSizeKB(const int size) { m_max_size_kb = size; } void SetMaxFiles(const int count) { m_max_files = count; } };
A classe FileLogHandler herda de ILogHandler. Ela possui várias variáveis-membro privadas que armazenam seu estado e sua configuração: m_min_level e m_format, com funções semelhantes às do handler de console; m_file_path, que define o diretório onde os logs serão armazenados; m_file_prefix, usado como nome-base dos arquivos de log; m_file_handle, que armazena o handle do arquivo de log atualmente aberto; m_current_day, utilizado pela lógica de rotação diária; m_max_size_kb, que define o limite de tamanho de cada arquivo de log em quilobytes; e m_max_files, que estabelece a quantidade máxima de arquivos de log a serem mantidos.
A classe também declara vários métodos auxiliares privados responsáveis por formatação, gerenciamento de arquivos e rotação: FormatMessage, LogLevelToString, EnsureFileOpen, GenerateFileName, RotateLogFiles, IsFileSizeExceeded, SortStringArray e CleanPath. Entre os métodos públicos estão o construtor, o destrutor, as implementações exigidas pela interface e os setters usados para ajustar a configuração.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ FileLogHandler::FileLogHandler(const string file_path, const string file_prefix, const LogLevel min_level, const string format, const int max_size_kb, const int max_files) { m_min_level = min_level; m_format = format; m_file_path = CleanPath(file_path); m_file_prefix = file_prefix; m_file_handle = INVALID_HANDLE; m_current_day = 0; m_max_size_kb = max_size_kb; m_max_files = max_files; // Create directory if it doesn't exist if(!FolderCreate(m_file_path)) { if(GetLastError() != 0) Print("FileLogHandler: Failed to create directory: ", m_file_path, ", error: ", GetLastError()); } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ FileLogHandler::~FileLogHandler() { Shutdown(); }
- Construtor (FileLogHandler::FileLogHandler):
O construtor inicializa o FileLogHandler. Ele recebe argumentos para o caminho do diretório de logs, o prefixo, o nível mínimo de logging, a string de formato, o tamanho máximo de cada arquivo e o número máximo de arquivos, atribuindo esses valores às respectivas variáveis-membro. O método auxiliar CleanPath é usado para garantir que o caminho utilize os separadores de diretório corretos. Além disso, o construtor tenta criar o diretório de logs especificado, definido por m_file_path em relação ao diretório de dados do terminal, usando FolderCreate caso ele ainda não exista. Isso busca garantir que o handler disponha de um diretório para gravar os arquivos. - Destrutor (FileLogHandler::~FileLogHandler):
O destrutor realiza a limpeza adequada dos recursos chamando o método Shutdown. Com isso, o handle do arquivo de log atualmente aberto é fechado quando o objeto FileLogHandler é destruído, evitando vazamento de recursos.
//+------------------------------------------------------------------+ //| Setup | //+------------------------------------------------------------------+ bool FileLogHandler::Setup(const string settings) { // Parse settings if provided // Format could be: "path=MQL5/Logs;prefix=MyEA;min_level=INFO;max_size=2048;max_files=10" if(settings != "") { string parts[]; int count = StringSplit(settings, ';', parts); for(int i = 0; i < count; i++) { string key_value[]; if(StringSplit(parts[i], '=', key_value) == 2) { string key = key_value[0]; StringTrimLeft(key); StringTrimRight(key); string value = key_value[1]; StringTrimLeft(value); StringTrimRight(value); if(key == "path") m_file_path = CleanPath(value); else if(key == "prefix") m_file_prefix = value; else if(key == "min_level") { if(value == "DEBUG") m_min_level = LOG_LEVEL_DEBUG; else if(value == "INFO") m_min_level = LOG_LEVEL_INFO; else if(value == "WARN") m_min_level = LOG_LEVEL_WARN; else if(value == "ERROR") m_min_level = LOG_LEVEL_ERROR; else if(value == "FATAL") m_min_level = LOG_LEVEL_FATAL; } else if(key == "max_size") m_max_size_kb = (int)StringToInteger(value); else if(key == "max_files") m_max_files = (int)StringToInteger(value); } } } return true; } //+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void FileLogHandler::Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) { // Check if the message level meets the minimum requirement if(level >= m_min_level && level < LOG_LEVEL_OFF) { // Ensure file is open and ready for writing if(EnsureFileOpen()) { // Format the message string formatted_message = FormatMessage(time, level, origin, message); // Write to file FileWriteString(m_file_handle, formatted_message + "\r\n"); // Flush to ensure data is written immediately FileFlush(m_file_handle); // Check if rotation is needed if(IsFileSizeExceeded()) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; RotateLogFiles(); EnsureFileOpen(); } } } } //+------------------------------------------------------------------+ //| Shutdown | //+------------------------------------------------------------------+ void FileLogHandler::Shutdown() { if(m_file_handle != INVALID_HANDLE) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; } }
- Método Setup (FileLogHandler::Setup):
Este método implementa a função Setup definida pela interface. Ele oferece uma forma alternativa de configurar o handler após sua criação por meio de uma única string de parâmetros, por exemplo, "path=MQL5/Logs;prefix=MyEA;max_size=2048". O método analisa essa string, separando-a em pares "chave-valor", e atualiza as variáveis-membro correspondentes, como m_file_path, m_file_prefix, m_min_level, m_max_size_kb e m_max_files. Isso permite, quando necessário, carregar a configuração a partir de fontes externas. Após concluir a análise, o método retorna true. - Método Log (FileLogHandler::Log):
Este método implementa a lógica principal de gravação no arquivo de log. Primeiro, verifica se o nível da mensagem atende ao requisito definido por m_min_level. Em caso afirmativo, chama EnsureFileOpen para verificar se o arquivo de log correto está aberto e abri-lo quando necessário, realizando a rotação diária quando necessário. Se o arquivo for aberto com sucesso, a mensagem é formatada por FormatMessage e a string resultante, seguida do caractere de nova linha (\r\n), é gravada no arquivo por meio de FileWriteString. Em seguida, FileFlush é chamada para forçar a gravação imediata dos dados em disco, o que é importante para preservar os logs mesmo em caso de falha da aplicação. Por fim, IsFileSizeExceeded verifica se o tamanho atual do arquivo ultrapassou o limite definido por m_max_size_kb. Se esse limite for excedido, o arquivo atual é fechado, RotateLogFiles é chamada para gerenciar os arquivos antigos e EnsureFileOpen abre um novo arquivo. - Método Shutdown (FileLogHandler::Shutdown):
Este método implementa a função Shutdown definida pela interface. Sua principal função é fechar o handle do arquivo de log atualmente aberto, armazenado em m_file_handle, usando FileClose caso o handle seja válido (!= INVALID_HANDLE). Isso permite fechar corretamente o arquivo e gravar os dados ainda pendentes no buffer quando o logger for finalizado.
//+------------------------------------------------------------------+ //| FormatMessage | //+------------------------------------------------------------------+ string FileLogHandler::FormatMessage(const datetime time, const LogLevel level, const string origin, const string message) { string formatted_message = m_format; // Replace placeholders StringReplace(formatted_message, "{time}", TimeToString(time, TIME_DATE | TIME_SECONDS)); StringReplace(formatted_message, "{level}", LogLevelToString(level)); StringReplace(formatted_message, "{origin}", origin); StringReplace(formatted_message, "{message}", message); return formatted_message; } //+------------------------------------------------------------------+ //| LogLevelToString | //+------------------------------------------------------------------+ string FileLogHandler::LogLevelToString(const LogLevel level) { switch(level) { case LOG_LEVEL_DEBUG: return "DEBUG"; case LOG_LEVEL_INFO: return "INFO"; case LOG_LEVEL_WARN: return "WARN"; case LOG_LEVEL_ERROR: return "ERROR"; case LOG_LEVEL_FATAL: return "FATAL"; default: return "UNKNOWN"; } }
Métodos auxiliares (FormatMessage, LogLevelToString): Esses métodos auxiliares privados funcionam da mesma forma que seus equivalentes em ConsoleLogHandler, formatando as mensagens com base na string m_format e convertendo valores da enumeração LogLevel em strings legíveis.
//+------------------------------------------------------------------+ //| EnsureFileOpen | //+------------------------------------------------------------------+ bool FileLogHandler::EnsureFileOpen() { datetime current_time = TimeCurrent(); MqlDateTime time_struct; TimeToStruct(current_time, time_struct); // Create a datetime that represents just the current day (time set to 00:00:00) MqlDateTime day_struct; day_struct.year = time_struct.year; day_struct.mon = time_struct.mon; day_struct.day = time_struct.day; day_struct.hour = 0; day_struct.min = 0; day_struct.sec = 0; datetime current_day = StructToTime(day_struct); // Check if we need to open a new file (either first time or new day) if(m_file_handle == INVALID_HANDLE || m_current_day != current_day) { // Close existing file if open if(m_file_handle != INVALID_HANDLE) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; } // Update current day m_current_day = current_day; // Generate new file name string file_name = GenerateFileName(current_time); // Open file for writing (append if exists) m_file_handle = FileOpen(file_name, FILE_WRITE | FILE_READ | FILE_TXT); if(m_file_handle == INVALID_HANDLE) { Print("FileLogHandler: Failed to open log file: ", file_name, ", error: ", GetLastError()); return false; } // Move to end of file for appending FileSeek(m_file_handle, 0, SEEK_END); } return true; } //+------------------------------------------------------------------+ //| GenerateFileName | //+------------------------------------------------------------------+ string FileLogHandler::GenerateFileName(const datetime time) { MqlDateTime time_struct; TimeToStruct(time, time_struct); string date_str = StringFormat("%04d%02d%02d", time_struct.year, time_struct.mon, time_struct.day); return m_file_path + "\\" + m_file_prefix + "_" + date_str + ".log"; } //+------------------------------------------------------------------+ //| IsFileSizeExceeded | //+------------------------------------------------------------------+ bool FileLogHandler::IsFileSizeExceeded() { if(m_file_handle != INVALID_HANDLE) { // Get current position (file size) ulong size = FileSize(m_file_handle); // Check if size exceeds limit (convert KB to bytes) return (size > (ulong)m_max_size_kb * 1024); } return false; }
- Método auxiliar (EnsureFileOpen):
Este método auxiliar é responsável por abrir os arquivos de log e gerenciar sua rotação diária. Ele compara a data atual, obtida por TimeCurrent(), com o valor armazenado em m_current_day. Se o handle do arquivo for inválido ou a data tiver mudado, o método fecha o handle existente, caso haja um, atualiza m_current_day, gera um novo nome de arquivo por meio de GenerateFileName, incluindo a data, e abre esse novo arquivo nos modos de leitura e gravação (FILE_WRITE FILE_READ FILE_TXT). Em seguida, FileSeek posiciona o cursor no final do arquivo para que os novos registros sejam acrescentados ao conteúdo existente. O método retorna true se o arquivo já estiver aberto ou for aberto com sucesso, e false em caso de falha. - Método auxiliar (GenerateFileName):
Este método gera o caminho completo do arquivo de log com base no horário atual. Ele formata a parte referente à data no padrão AAAAMMDD e a combina com os valores configurados em m_file_path e m_file_prefix, acrescentando a extensão .log. - Método auxiliar (IsFileSizeExceeded):
Esta função verifica se o tamanho do arquivo de log atualmente aberto, identificado por m_file_handle, ultrapassou o limite definido em m_max_size_kb. Para isso, obtém o tamanho do arquivo por meio de FileSize e o compara com o limite convertido em bytes. A função retorna true quando o tamanho máximo é excedido e false caso contrário.
//+------------------------------------------------------------------+ //| RotateLogFiles | //+------------------------------------------------------------------+ void FileLogHandler::RotateLogFiles() { // Get list of log files string terminal_path = TerminalInfoString(TERMINAL_DATA_PATH); string full_path = terminal_path + "\\" + m_file_path; string file_pattern = m_file_prefix + "_*.log"; string files[]; int file_count = 0; long search_handle = FileFindFirst(full_path + "\\" + file_pattern, files[file_count]); if(search_handle != INVALID_HANDLE) { file_count++; // Find all matching files while(FileFindNext(search_handle, files[file_count])) { file_count++; ArrayResize(files, file_count + 1); } // Close search handle FileFindClose(search_handle); } // Resize array to actual number of found files before sorting ArrayResize(files, file_count); // Sort the string array using the custom sorter SortStringArray(files); // Delete oldest files if we have too many int files_to_delete = file_count - m_max_files + 1; // +1 for the new file we'll create if(files_to_delete > 0) { for(int i = 0; i < files_to_delete; i++) { if(!FileDelete(m_file_path + "\\" + files[i])) Print("FileLogHandler: Failed to delete old log file: ", files[i], ", error: ", GetLastError()); } } } //+------------------------------------------------------------------+ //| SortStringArray | //+------------------------------------------------------------------+ void FileLogHandler::SortStringArray(string &arr[]) { int n = ArraySize(arr); for(int i = 0; i < n - 1; i++) { for(int j = i + 1; j < n; j++) { if(arr[i] > arr[j]) { string temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } } //+------------------------------------------------------------------+ //| New implementation: CleanPath | //+------------------------------------------------------------------+ string FileLogHandler::CleanPath(const string path) { string result = path; // Replace all "/" with "\\" StringReplace(result, "/", "\\"); return result; } //+------------------------------------------------------------------+
- Método auxiliar (RotateLogFiles):
Este método implementa a política de retenção dos arquivos de log. Ele localiza todos os arquivos no diretório de logs que correspondem ao padrão (m_file_prefix_*.log) usando FileFindFirst e FileFindNext. Os nomes dos arquivos são armazenados em um array de strings e classificados em ordem alfabética com o método auxiliar SortStringArray. Como os nomes incluem a data, essa ordenação normalmente também corresponde à ordem cronológica. Em seguida, o método calcula quantos arquivos excedem o limite definido por m_max_files e remove os mais antigos, localizados no início da lista ordenada, por meio de FileDelete. - Método auxiliar (SortStringArray):
Este método implementa uma ordenação simples por bubble sort, desenvolvida especificamente para classificar o array de nomes de arquivos de log obtido em RotateLogFiles. Ele é utilizado porque a biblioteca padrão do MQL5 não oferece uma função integrada para ordenar arrays de strings. - Método auxiliar (CleanPath):
Este método garante que os caminhos de diretório utilizem a barra invertida (), esperada pelas funções de manipulação de arquivos do MQL5, substituindo eventuais barras normais (/) encontradas na string de entrada. - Métodos setters (SetFilePath, SetMinLevel etc.):
Esses métodos públicos permitem alterar os parâmetros de configuração do handler, como caminho, prefixo, nível mínimo, formato e limites de tamanho, mesmo após sua criação.
CLogger
Este arquivo de cabeçalho define a classe CLogger, que atua como o componente central de todo o mecanismo de logging. Ela é implementada com o padrão de projeto Singleton, garantindo que exista apenas uma instância do logger em toda a aplicação. Essa instância gerencia todos os handlers registrados e fornece a principal interface usada pelo código da aplicação para enviar mensagens de log.#property strict #include "LogLevels.mqh" #include "ILogHandler.mqh" //+------------------------------------------------------------------+ //| Class: CLogger | //| Description: Singleton class for managing and dispatching log | //| messages to registered handlers. | //+------------------------------------------------------------------+ class CLogger { private: static CLogger *s_instance; ILogHandler* m_handlers[]; LogLevel m_global_min_level; long m_expert_magic; string m_expert_name; //--- Private constructor for Singleton CLogger(); ~CLogger(); public: //--- Get the singleton instance static CLogger* Instance(); //--- Cleanup the singleton instance static void Release(); //--- Handler management bool AddHandler(ILogHandler *handler); void ClearHandlers(); //--- Configuration void SetGlobalMinLevel(const LogLevel level); void SetExpertInfo(const long magic, const string name); //--- Logging methods void Log(const LogLevel level, const string origin, const string message); void Debug(const string origin, const string message); void Info(const string origin, const string message); void Warn(const string origin, const string message); void Error(const string origin, const string message); void Fatal(const string origin, const string message); //--- Formatted logging methods void LogFormat(const LogLevel level, const string origin, const string formatted_message); void DebugFormat(const string origin, const string formatted_message); void InfoFormat(const string origin, const string formatted_message); void WarnFormat(const string origin, const string formatted_message); void ErrorFormat(const string origin, const string formatted_message); void FatalFormat(const string origin, const string formatted_message); };
A classe CLogger contém vários membros privados. s_instance é um ponteiro estático para a única instância da própria classe. m_handlers é um array dinâmico de ponteiros para ILogHandler que armazena referências a todos os handlers de log ativos, como os de console e arquivo. m_global_min_level define o limiar global de filtragem; mensagens abaixo desse nível são descartadas antes mesmo de serem encaminhadas aos handlers individuais. m_expert_magic e m_expert_name armazenam informações opcionais sobre o EA que utiliza o logger e podem ser incluídas nas mensagens de log para fornecer mais contexto.
O construtor e o destrutor são privados para impor o padrão Singleton. Os métodos públicos fornecem acesso à instância, gerenciamento dos handlers, configuração e diferentes funções de logging.
//+------------------------------------------------------------------+ //| Static instance initialization | //+------------------------------------------------------------------+ CLogger *CLogger::s_instance = NULL; //+------------------------------------------------------------------+ //| Get Singleton Instance | //+------------------------------------------------------------------+ CLogger* CLogger::Instance() { if(s_instance == NULL) { s_instance = new CLogger(); } return s_instance; } //+------------------------------------------------------------------+ //| Release Singleton Instance | //+------------------------------------------------------------------+ void CLogger::Release() { if(s_instance != NULL) { delete s_instance; s_instance = NULL; } } //+------------------------------------------------------------------+ //| Constructor (Private) | //+------------------------------------------------------------------+ CLogger::CLogger() { m_global_min_level = LOG_LEVEL_DEBUG; m_expert_magic = 0; m_expert_name = ""; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogger::~CLogger() { ClearHandlers(); }
- Implementação do Singleton (Instance, Release, construtor privado):
O padrão Singleton é implementado por meio do método estático Instance(), que cria o objeto CLogger na primeira chamada e retorna essa mesma instância nas chamadas seguintes. O construtor CLogger::CLogger é privado, impedindo que objetos da classe sejam criados diretamente fora dela. Ele inicializa os valores padrão do nível mínimo global e das informações do EA. O método estático Release() permite excluir explicitamente a instância singleton e liberar seus recursos, normalmente durante o encerramento da aplicação. - Destrutor (CLogger::~CLogger):
O destrutor é chamado quando a instância singleton é excluída por meio do método Release(). Sua principal responsabilidade é liberar os handlers gerenciados chamando ClearHandlers, chamando Shutdown para cada handler antes de excluir os respectivos objetos.
//+------------------------------------------------------------------+ //| AddHandler | //+------------------------------------------------------------------+ bool CLogger::AddHandler(ILogHandler *handler) { if(CheckPointer(handler) == POINTER_INVALID) { Print("CLogger::AddHandler - Error: Invalid handler pointer."); return false; } int size = ArraySize(m_handlers); ArrayResize(m_handlers, size + 1); m_handlers[size] = handler; return true; } //+------------------------------------------------------------------+ //| ClearHandlers | //+------------------------------------------------------------------+ void CLogger::ClearHandlers() { for(int i = 0; i < ArraySize(m_handlers); i++) { ILogHandler *handler = m_handlers[i]; if(CheckPointer(handler) != POINTER_INVALID) { handler.Shutdown(); delete handler; } } ArrayResize(m_handlers, 0); } //+------------------------------------------------------------------+ //| SetGlobalMinLevel | //+------------------------------------------------------------------+ void CLogger::SetGlobalMinLevel(const LogLevel level) { m_global_min_level = level; } //+------------------------------------------------------------------+ //| SetExpertInfo | //+------------------------------------------------------------------+ void CLogger::SetExpertInfo(const long magic, const string name) { m_expert_magic = magic; m_expert_name = name; }
- Gerenciamento dos handlers (AddHandler, ClearHandlers):
O método AddHandler permite adicionar um novo handler de log, ou seja, qualquer objeto que implemente a interface ILogHandler, à lista interna do logger armazenada em m_handlers. Ele verifica se o ponteiro é válido, redimensiona o array dinâmico e adiciona o novo handler. O método ClearHandlers percorre o array m_handlers, chama Shutdown para cada handler válido, exclui o próprio objeto, partindo do princípio de que o logger assume sua propriedade, e por fim limpa o array. Esse procedimento é essencial para liberar corretamente todos os recursos associados. - Configuração (SetGlobalMinLevel, SetExpertInfo):
Esses métodos permitem ajustar o comportamento do logger. SetGlobalMinLevel altera o limiar global de filtragem, armazenado em m_global_min_level, afetando todas as mensagens antes que elas sejam encaminhadas aos handlers. SetExpertInfo permite definir o número mágico e o nome do EA. Essas informações podem ser incluídas automaticamente nas mensagens de log para facilitar a identificação da origem, especialmente quando vários EAs estão registrando mensagens ao mesmo tempo.
//+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void CLogger::Log(const LogLevel level, const string origin, const string message) { // Check global level first if(level < m_global_min_level || level >= LOG_LEVEL_OFF) return; datetime current_time = TimeCurrent(); string effective_origin = origin; if(m_expert_name != "") effective_origin = m_expert_name + "::" + origin; // Dispatch to all registered handlers for(int i = 0; i < ArraySize(m_handlers); i++) { ILogHandler *handler = m_handlers[i]; if(CheckPointer(handler) != POINTER_INVALID) { handler.Log(current_time, level, effective_origin, message, m_expert_magic); } } } //+------------------------------------------------------------------+ //| Convenience Logging Methods | //+------------------------------------------------------------------+ void CLogger::Debug(const string origin, const string message) { Log(LOG_LEVEL_DEBUG, origin, message); } void CLogger::Info(const string origin, const string message) { Log(LOG_LEVEL_INFO, origin, message); } void CLogger::Warn(const string origin, const string message) { Log(LOG_LEVEL_WARN, origin, message); } void CLogger::Error(const string origin, const string message) { Log(LOG_LEVEL_ERROR, origin, message); } void CLogger::Fatal(const string origin, const string message) { Log(LOG_LEVEL_FATAL, origin, message); } //+------------------------------------------------------------------+ //| LogFormat | //+------------------------------------------------------------------+ void CLogger::LogFormat(const LogLevel level, const string origin, const string formatted_message) { // Check global level first if(level < m_global_min_level || level >= LOG_LEVEL_OFF) return; Log(level, origin, formatted_message); } //+------------------------------------------------------------------+ //| Convenience Formatted Logging Methods | //+------------------------------------------------------------------+ void CLogger::DebugFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_DEBUG, origin, formatted_message); } void CLogger::InfoFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_INFO, origin, formatted_message); } void CLogger::WarnFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_WARN, origin, formatted_message); } void CLogger::ErrorFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_ERROR, origin, formatted_message); } void CLogger::FatalFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_FATAL, origin, formatted_message); } //+------------------------------------------------------------------+
- Método principal de logging (Log):
Este é o método central que recebe as chamadas de logging. Primeiro, ele verifica se o nível da mensagem atende ao limite mínimo global definido por m_global_min_level. Se a mensagem passar por esse filtro, o método obtém o horário atual e monta a string effective_origin, podendo acrescentar no início o nome do EA armazenado em m_expert_name. Em seguida, percorre o array de handlers m_handlers e chama o método Log de cada handler válido, passando a marca de tempo, o nível, a origem, a mensagem e o Magic Number do EA. Dessa forma, a mesma mensagem de log é distribuída para todos os destinos de saída ativos. - Métodos de logging simplificados (Debug, Info, Warn, Error, Fatal):
Esses métodos públicos oferecem uma interface mais prática para registrar mensagens com níveis específicos de severidade. Cada método, como Debug ou Info, simplesmente chama o método principal Log com o valor correspondente da enumeração LogLevel, como LOG_LEVEL_DEBUG, LOG_LEVEL_INFO e assim por diante. Isso reduz a quantidade de código necessária na aplicação para registrar mensagens. - Métodos de logging com formatação (LogFormat, DebugFormat etc.):
Esses métodos oferecem uma forma alternativa de registrar mensagens já formatadas. LogFormat recebe uma string previamente formatada e chama o método principal Log. Os métodos auxiliares DebugFormat, InfoFormat e os demais apenas chamam LogFormat com o nível de severidade correspondente. Isso é útil quando a lógica de formatação da mensagem é mais complexa e é tratada em outro ponto do código antes da chamada ao logger.
Com a implementação do CLogger concluída, chegou a hora de vê-lo em funcionamento.
Uso da camada de logging
Este EA serve como uma demonstração prática de como integrar e utilizar a camada personalizada de logging em MQL5, composta por CLogger, ILogHandler, ConsoleLogHandler e FileLogHandler. O exemplo mostra como inicializar, configurar, utilizar durante a execução e limpar os componentes de logging dentro da estrutura padrão de um EA.
A seção inicial de LoggingExampleEA.mq5 define as propriedades padrão do EA e inclui os componentes necessários da camada personalizada de logging.
Após a definição das propriedades, as diretivas #include integram os componentes necessários de logging. CLogger.mqh inclui a definição da classe principal do logger. ConsoleLogHandler.mqh adiciona a classe responsável pelo logging no console do MetaTrader, ou seja, na guia "Experts". FileLogHandler.mqh inclui a classe responsável pela gravação em arquivos. Com essas inclusões, as classes e funções definidas nesses arquivos de cabeçalho passam a ficar disponíveis para uso no EA.
Parâmetros de entrada (input):
// Input parameters input int MagicNumber = 654321; // EA Magic Number input double LotSize = 0.01; // Fixed lot size input int StopLossPips = 50; // Stop Loss in pips input int TakeProfitPips = 100; // Take Profit in pips input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO; // Minimum level for console output input LogLevel FileLogLevel = LOG_LEVEL_DEBUG; // Minimum level for file output
Nesta seção são definidos os parâmetros externos que o usuário pode configurar ao anexar o EA a um gráfico. Esses parâmetros permitem ajustar tanto o comportamento de trading do EA quanto, Esses parâmetros permitem ajustar tanto o comportamento de trading do EA quanto suas configurações de logging, que são especialmente relevantes neste exemplo.
- input int MagicNumber = 654321; : este parâmetro define o Magic Number usado para identificar as ordens enviadas por essa instância específica do EA. Ele permite distinguir suas operações das realizadas por outros EAs ou manualmente.
- input double LotSize = 0.01; : define o volume fixo de negociação, ou tamanho do lote, usado nas ordens enviadas pelo EA.
- input int StopLossPips = 50; : define a distância do stop loss, em pips, para as ordens.
- input int TakeProfitPips = 100; : define a distância do take profit, em pips, para as ordens.
- input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO; : este parâmetro permite escolher o nível mínimo de severidade das mensagens que serão exibidas na guia "Experts" do MetaTrader, ou seja, no console. Ele usa o tipo enumerado LogLevel, definido em LogLevels.mqh. O valor padrão é LOG_LEVEL_INFO, o que significa que mensagens dos níveis INFO, WARN, ERROR e FATAL serão exibidas no console, enquanto mensagens DEBUG serão suprimidas.
- input LogLevel FileLogLevel = LOG_LEVEL_DEBUG; : de forma semelhante, este parâmetro define o nível mínimo de severidade das mensagens que serão gravadas no arquivo de log. Ele também utiliza a enumeração LogLevel. O valor padrão é LOG_LEVEL_DEBUG, o que significa que todas as mensagens, inclusive as informações detalhadas de depuração, serão registradas no arquivo. Assim, a saída do console pode permanecer menos detalhada durante a operação normal, enquanto logs completos ficam disponíveis para análise posterior ou diagnóstico de problemas.
Esses parâmetros específicos de logging mostram como a camada de logging pode ser configurada facilmente por meio de parâmetros externos, permitindo ajustar o nível de detalhamento dos logs sem modificar o código do EA.
// Global logger pointer (optional, can use CLogger::Instance() directly) CLogger *g_logger = NULL;
O EA declara uma única variável global: CLogger *g_logger = NULL; : esta linha declara um ponteiro chamado g_logger, capaz de apontar para um objeto da classe CLogger. Ele é inicializado com NULL, portanto inicialmente não referencia nenhum objeto válido. Esse ponteiro global será usado para armazenar a única instância de CLogger obtida por meio do padrão Singleton, usando CLogger::Instance().
Embora seja possível chamar diretamente o método estático CLogger::Instance() sempre que for necessário registrar uma mensagem, armazenar a instância nessa variável global após obtê-la em OnInit() oferece uma forma mais prática de acessar o logger a partir de diferentes funções, como OnTick, OnDeinit e OnChartEvent, sem repetir chamadas a CLogger::Instance(). Na prática, g_logger mantém uma referência à instância singleton do logger para facilitar seu acesso sem repetir chamadas a CLogger::Instance().
OnInit():
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Get the logger instance g_logger = CLogger::Instance(); if(CheckPointer(g_logger) == POINTER_INVALID) { Print("Critical Error: Failed to get Logger instance!"); return(INIT_FAILED); } //--- Set EA information for context in logs g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME)); //--- Configure Handlers --- // 1. Console Handler ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel); if(CheckPointer(console_handler) != POINTER_INVALID) { // Optionally customize format // console_handler.SetFormat("[{level}] {message}"); if(!g_logger.AddHandler(console_handler)) { Print("Warning: Failed to add ConsoleLogHandler."); delete console_handler; // Clean up if not added } } else { Print("Warning: Failed to create ConsoleLogHandler."); } // 2. File Handler string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber); FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", // Directory relative to MQL5/Files log_prefix, // File name prefix FileLogLevel, // Minimum level to log to file "[{time}] {level} ({origin}): {message}", // Format 2048, // Max file size in KB (e.g., 2MB) 10); // Max number of log files to keep if(CheckPointer(file_handler) != POINTER_INVALID) { if(!g_logger.AddHandler(file_handler)) { Print("Warning: Failed to add FileLogHandler."); delete file_handler; // Clean up if not added } } else { Print("Warning: Failed to create FileLogHandler."); } //--- Log initialization message g_logger.Info(__FUNCTION__, "Expert Advisor initialized successfully."); g_logger.Debug(__FUNCTION__, StringFormat("Settings: Lots=%.2f, SL=%d, TP=%d, ConsoleLevel=%s, FileLevel=%s", LotSize, StopLossPips, TakeProfitPips, EnumToString(ConsoleLogLevel), EnumToString(FileLogLevel))); //--- succeed return(INIT_SUCCEEDED); }
Neste exemplo, a função OnInit() desempenha um papel central na inicialização e configuração da camada personalizada de logging.
g_logger = CLogger::Instance();
O método estático CLogger::Instance() implementa o acesso à única instância de CLogger. O ponteiro retornado é armazenado na variável global g_logger para facilitar o acesso posteriormente. Em seguida, é realizada uma verificação básica com CheckPointer para confirmar que a instância foi obtida corretamente. Caso contrário, uma mensagem de erro crítico é enviada ao log padrão e a inicialização termina com falha, retornando INIT_FAILED.
g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME));
Na sequência, o logger recebe informações contextuais sobre o EA que está utilizando o sistema. São fornecidos o MagicNumber, definido nos parâmetros de entrada, e o nome do EA, obtido por meio de MQL5InfoString(MQL5_PROGRAM_NAME). Essas informações podem ser incorporadas automaticamente às mensagens pelos handlers, dependendo da string de formato configurada, facilitando a identificação da origem dos logs, especialmente quando vários EAs estão em execução ao mesmo tempo.
Em seguida, um ConsoleLogHandler é criado dinamicamente com new:
ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel);
Ele é configurado diretamente no construtor com o nível mínimo de logging definido pelo parâmetro de entrada ConsoleLogLevel. O código também inclui um exemplo comentado, console_handler.SetFormat("[{level}] {message}");, mostrando como o formato da saída pode ser alterado após a criação, caso seja necessário. Em seguida, o handler é adicionado ao logger principal:if(!g_logger.AddHandler(console_handler))
Se a adição do handler falhar, retornando false, é exibido um aviso e o objeto recém-criado é excluído com delete para evitar vazamento de memória. A criação inicial do handler com new também é acompanhada de verificação de erro.
De forma semelhante, é criado um FileLogHandler:// 2. File Handler string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber); FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", // Directory relative to MQL5/Files log_prefix, // File name prefix FileLogLevel, // Minimum level to log to file "[{time}] {level} ({origin}): {message}", // Format 2048, // Max file size in KB (e.g., 2MB) 10); // Max number of log files to keep
O prefixo do nome do arquivo é formado com o nome do EA e o Magic Number, facilitando a identificação exclusiva de cada instância. O construtor de FileLogHandler recebe vários argumentos: o caminho do diretório ("MQL5/Logs/EA_Logs", relativo ao diretório MQL5/Files do terminal), o prefixo gerado, o nível mínimo definido pelo parâmetro de entrada FileLogLevel, uma string de formato personalizada, o tamanho máximo de cada arquivo em KB (2048 KB = 2 MB) e o número máximo de arquivos de log mantidos (10). Assim como ocorre com o handler de console, ele é adicionado ao logger por meio de g_logger.AddHandler(), com tratamento de erros equivalente e uso de delete caso a criação ou a adição falhe.
Depois de configurar os handlers, o EA registra mensagens para confirmar a inicialização:g_logger.Info(__FUNCTION__, \"Expert Advisor initialized successfully.\"); g_logger.Debug(__FUNCTION__, StringFormat(\"Settings: ...\"));
Uma mensagem no nível Info registra que a inicialização foi concluída com sucesso. Já uma mensagem no nível Debug registra os principais parâmetros de entrada usando StringFormat. FUNCTION é usada como string de origem, fornecendo automaticamente o nome da função atual, neste caso OnInit. Essas mensagens serão processadas pelos handlers adicionados de acordo com os níveis mínimos configurados para cada um deles.
Por fim, se toda a inicialização for concluída corretamente, a função retorna INIT_SUCCEEDED, informando ao terminal que o EA está pronto para processar ticks. Se ocorrer algum erro crítico, como a impossibilidade de obter a instância do logger, a função retorna INIT_FAILED.
OnDeinit():
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Log deinitialization if(CheckPointer(g_logger) != POINTER_INVALID) { string reason_str = "Unknown reason"; switch(reason) { case REASON_ACCOUNT: reason_str = "Account change"; break; case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; break; case REASON_CHARTCLOSE: reason_str = "Chart closed"; break; case REASON_PARAMETERS: reason_str = "Input parameters changed"; break; case REASON_RECOMPILE: reason_str = "Recompiled"; break; case REASON_REMOVE: reason_str = "EA removed from chart"; break; case REASON_TEMPLATE: reason_str = "Template applied"; break; case REASON_CLOSE: reason_str = "Terminal closed"; break; } g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")"); // Release the logger instance (this calls Shutdown() on all handlers) CLogger::Release(); g_logger = NULL; // Set pointer to NULL after release } else { Print("Logger instance was already invalid during Deinit."); } //--- Print to standard log just in case logger failed Print(MQL5InfoString(MQL5_PROGRAM_NAME) + ": Deinitialized. Код причины: " + IntegerToString(reason)); }
Em LoggingExampleEA.mq5, a função OnDeinit concentra-se no encerramento correto da camada de logging:
if(CheckPointer(g_logger) != POINTER_INVALID)
Primeiro, a função verifica se o ponteiro global g_logger ainda é válido. Isso evita o acesso a um ponteiro inválido caso OnDeinit seja chamada depois que o logger já tiver sido liberado ou se a inicialização tiver falhado.
Dentro do bloco if, o código usa uma instrução switch para obter uma descrição legível correspondente ao código de motivo recebido por OnDeinit. Isso fornece contexto sobre a razão pela qual o EA está sendo encerrado. Em seguida, g_logger.Info() registra uma mensagem informativa que inclui tanto a descrição do motivo quanto o código original.
string reason_str = "Unknown reason"; switch(reason) { case REASON_ACCOUNT: reason_str = "Account change"; break; case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; break; ... ... case REASON_CLOSE: reason_str = "Terminal closed"; break; } g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")");
Assim, o encerramento do EA, inclusive seu motivo, fica registrado nos logs, tanto no console quanto em arquivo, de acordo com os níveis configurados.
Este é o passo mais importante para a liberação dos recursos do logger:
CLogger::Release();
A chamada ao método estático Release() da classe CLogger inicia a exclusão da instância singleton do logger. Durante esse processo, o destrutor de CLogger percorre todos os handlers adicionados, neste caso os handlers de console e arquivo, chama os respectivos métodos Shutdown() e, em seguida, exclui os próprios objetos. Para FileLogHandler, Shutdown() inclui o fechamento do arquivo de log que estiver aberto. Assim, todos os recursos são liberados corretamente e os arquivos são fechados de forma adequada.
Redefinição do ponteiro global:
g_logger = NULL;
Depois que a instância é liberada, o ponteiro global g_logger é explicitamente redefinido como NULL. Essa é uma boa prática, pois deixa claro que o ponteiro já não referencia um objeto válido.
O bloco else trata o caso em que g_logger já esteja inválido no momento em que OnDeinit é chamada, enviando uma mensagem ao log padrão da guia "Experts". Além disso, uma chamada final a Print, fora da lógica do logger personalizado, permite registrar a mensagem de desinicialização no log padrão mesmo se o sistema personalizado de logging falhar completamente.
Essa implementação demonstra o procedimento correto de limpeza da camada personalizada de logging, com o fechamento dos arquivos de log e a liberação dos recursos quando o EA é finalizado.
OnTick():
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) { // Attempt to re-initialize logger if it became invalid unexpectedly // This is defensive coding, ideally it shouldn't happen if OnInit succeeded. Print("Error: Logger instance invalid in OnTick! Attempting re-init..."); if(OnInit() != INIT_SUCCEEDED) { Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA."); ExpertRemove(); // Stop the EA return; } } //--- Log tick arrival MqlTick latest_tick; if(SymbolInfoTick(_Symbol, latest_tick)) { g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d", TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS), latest_tick.bid, latest_tick.ask, (int)latest_tick.volume_real)); } else { g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError())); } //--- Example Logic: Check for a simple crossover // Note: Use more robust indicator handling in a real EA double ma_fast[], ma_slow[]; int copied_fast = CopyBuffer(iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_fast); int copied_slow = CopyBuffer(iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_slow); if(copied_fast < 3 || copied_slow < 3) { g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data."); return; // Not enough data yet } // ArraySetAsSeries might be needed depending on how you access indices // ArraySetAsSeries(ma_fast, true); // ArraySetAsSeries(ma_slow, true); bool cross_up = ma_fast[1] > ma_slow[1] && ma_fast[2] <= ma_slow[2]; bool cross_down = ma_fast[1] < ma_slow[1] && ma_fast[2] >= ma_slow[2]; if(cross_up) { g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal."); // --- Add trading logic here --- // Example: SendBuyOrder(); } else if(cross_down) { g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal."); // --- Add trading logic here --- // Example: SendSellOrder(); } // Log account info periodically static datetime last_account_log = 0; if(TimeCurrent() - last_account_log >= 3600) // Log every hour { g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f", AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN), AccountInfoDouble(ACCOUNT_MARGIN_FREE))); last_account_log = TimeCurrent(); } }
Análise detalhada...
//--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) { // Attempt to re-initialize logger if it became invalid unexpectedly // This is defensive coding, ideally it shouldn't happen if OnInit succeeded. Print("Error: Logger instance invalid in OnTick! Attempting re-init..."); if(OnInit() != INIT_SUCCEEDED) { Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA."); ExpertRemove(); // Stop the EA return; } }
Assim como em OnDeinit, a função começa verificando a validade do ponteiro g_logger com CheckPointer. Como medida de proteção, se o logger for considerado inválido, algo que idealmente não deveria ocorrer após uma execução bem-sucedida de OnInit, o código tenta reinicializá-lo chamando OnInit() novamente. Se essa nova tentativa também falhar, uma mensagem de erro crítico é exibida com a função padrão Print e o EA é removido por meio de ExpertRemove().
Em seguida, o EA tenta obter as informações do tick mais recente usando SymbolInfoTick().//--- Log tick arrival MqlTick latest_tick; if(SymbolInfoTick(_Symbol, latest_tick)) { g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d", TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS), latest_tick.bid, latest_tick.ask, (int)latest_tick.volume_real)); } else { g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError())); }
Se a chamada for bem-sucedida, é registrada uma mensagem Debug contendo a marca de tempo do tick, o preço Bid, o preço Ask e o volume, todos formatados com StringFormat. Isso fornece um rastreamento detalhado dos dados de preço recebidos, o que é útil durante a depuração. Se SymbolInfoTick falhar, é registrada uma mensagem Warn contendo o código de erro obtido por GetLastError().
O código também inclui um exemplo simples de detecção de cruzamento de médias móveis (MA)://--- Example Logic: Check for a simple crossover // Note: Use more robust indicator handling in a real EA double ma_fast[], ma_slow[]; int copied_fast = CopyBuffer(iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_fast); int copied_slow = CopyBuffer(iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_slow); if(copied_fast < 3 || copied_slow < 3) { g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data."); return; // Not enough data yet } // ArraySetAsSeries might be needed depending on how you access indices // ArraySetAsSeries(ma_fast, true); // ArraySetAsSeries(ma_slow, true); bool cross_up = ma_fast[1] > ma_slow[1] && ma_fast[2] <= ma_slow[2]; bool cross_down = ma_fast[1] < ma_slow[1] && ma_fast[2] >= ma_slow[2]; if(cross_up) { g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal."); // --- Add trading logic here --- // Example: SendBuyOrder(); } else if(cross_down) { g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal."); // --- Add trading logic here --- // Example: SendSellOrder(); }
Primeiro, é feita uma tentativa de copiar os dados de dois indicadores iMA. Se o número de valores copiados for insuficiente, uma mensagem Warn é registrada e a função é encerrada. Se os dados estiverem disponíveis, o código verifica a ocorrência de um cruzamento entre as médias móveis rápida e lenta nas duas barras anteriores. Ao detectar um cruzamento, cross_up ou cross_down, é registrada uma mensagem Info indicando um possível sinal de trading. Isso demonstra como eventos relevantes da estratégia podem ser registrados no log.
Por fim, algumas informações são registradas periodicamente, em vez de a cada tick:
// Log account info periodically static datetime last_account_log = 0; if(TimeCurrent() - last_account_log >= 3600) // Log every hour { g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f", AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN), AccountInfoDouble(ACCOUNT_MARGIN_FREE))); last_account_log = TimeCurrent(); }
A variável estática last_account_log armazena o horário do último registro das informações da conta. O código verifica se o horário atual, obtido por TimeCurrent(), está pelo menos 3600 segundos, ou 1 hora, à frente do último registro. Em caso afirmativo, é registrada uma mensagem Info contendo o saldo atual da conta, o patrimônio líquido, a margem e a margem livre. Em seguida, last_account_log é atualizado. Isso evita que os logs sejam sobrecarregados com informações repetitivas, ao mesmo tempo que mantém atualizações periódicas sobre o estado da conta.
De modo geral, a função OnTick demonstra como usar o logger para diferentes finalidades durante a execução do EA: depuração detalhada, com Debug para os ticks; avisos sobre possíveis problemas, com Warn quando a cópia dos dados falha; mensagens informativas sobre eventos relevantes, com Info para os sinais; e atualizações periódicas sobre o estado da conta, também registradas com Info.
OnChartEvent():
A função OnChartEvent() é um handler de eventos do MQL5 responsável por processar diversos eventos que ocorrem diretamente no gráfico em que o EA está sendo executado. Esses eventos podem incluir interações do usuário, como pressionamento de teclas ou movimentação do mouse, cliques em objetos gráficos e também eventos personalizados gerados pelo próprio EA ou por outros programas MQL5.
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) return; //--- Log chart events string event_name = "Unknown Chart Event"; switch(id) { case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; break; case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; break; // Add other CHARTEVENT cases as needed case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; break; case CHARTEVENT_CUSTOM+1: event_name = "CustomEvent_1"; break; // Example custom event } g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d (%s), lparam=%d, dparam=%.5f, sparam='%s'", id, event_name, lparam, dparam, sparam)); } //+------------------------------------------------------------------+
Assim como em OnTick e OnDeinit, a função começa verificando a validade do ponteiro global g_logger:
if(CheckPointer(g_logger) == POINTER_INVALID) return;
Se o logger estiver inválido, a função simplesmente termina, evitando o processamento adicional com um ponteiro inválido.
A parte principal da função identifica o tipo de evento e registra seus detalhes:
//--- Log chart events string event_name = "Unknown Chart Event"; switch(id) { case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; break; case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; break; // Add other CHARTEVENT cases as needed case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; break; case CHARTEVENT_CUSTOM+1: event_name = "CustomEvent_1"; break; // Example custom event } g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d (%s), lparam=%d, dparam=%.5f, sparam='%s'", id, event_name, lparam, dparam, sparam));
Uma instrução switch converte cada identificador de evento recebido em um nome legível, armazenado em event_name, como CHARTEVENT_KEYDOWN, CHARTEVENT_MOUSE_MOVE ou CHARTEVENT_OBJECT_CLICK. O exemplo também mostra como tratar um evento personalizado, CHARTEVENT_CUSTOM + 1.
Em seguida, é registrada uma mensagem Debug por meio de g_logger.Debug(). Esse registro contém o identificador do evento, o nome correspondente e os valores dos parâmetros lparam, dparam e sparam, formatados com StringFormat. Registrar essas informações no nível Debug é extremamente útil durante o desenvolvimento e os testes, pois permite acompanhar as interações com o gráfico e rastrear o fluxo de eventos personalizados na aplicação.
Vantagens da camada personalizada de logging
Nosso sistema de logging personalizado oferece várias melhorias em relação à função básica Print():
- Filtragem por severidade: visualize apenas as mensagens relevantes, classificadas por nível de prioridade.
- Múltiplos destinos de saída: envie logs simultaneamente para o console, arquivos ou outros destinos.
- Contexto detalhado: marcas de tempo, origem e informações do EA são adicionadas automaticamente.
- Formatação flexível: personalize o formato das mensagens para facilitar a leitura e a análise.
- Rotação de arquivos: evite que os arquivos de log cresçam indefinidamente.
- Gerenciamento centralizado: ative ou desative o logging globalmente ou de forma individual para cada handler.
Esses recursos tornam a depuração de sistemas de trading complexos muito mais eficiente. Você pode identificar problemas rapidamente, acompanhar o comportamento do sistema ao longo do tempo e concentrar-se nos dados que realmente importam.
Conclusão
Depois de implementar essa camada personalizada de logging, você pode deixar para trás as chamadas dispersas a Print() e passar a trabalhar com um sistema em que o código produz mensagens claras, ricas em contexto e totalmente configuráveis. Erros críticos ganham destaque, rastreamentos detalhados ficam disponíveis para análise posterior e os arquivos de log permanecem sob controle. Melhor ainda, o sistema se adapta à sua forma de trabalhar: você pode trocar handlers, alterar formatos ou ajustar o nível de detalhamento sempre que necessário. No próximo artigo, adicionaremos ferramentas de profiling e testes unitários para detectar problemas de desempenho e erros de lógica muito antes que eles apareçam durante a execução do EA em um gráfico real. Isso é dominar MQL5 de verdade.
E lembre-se: esta é apenas a primeira etapa da jornada. Ainda veremos técnicas avançadas de depuração, profilers personalizados, um ambiente robusto de testes unitários e verificações automatizadas da qualidade do código. Ao final da série, você terá substituído a busca reativa por erros por um processo disciplinado e proativo de garantia de qualidade.
Por enquanto, bons trades e bom desenvolvimento!
Visão geral dos arquivos
| Nome do arquivo | Descrição do arquivo |
|---|---|
| LogLevels.mqh | Define a enumeração LogLevel com níveis de severidade que vão de DEBUG a OFF e são utilizados em toda a camada de logging. |
| ILogHandler.mqh | Declara a interface ILogHandler, com os métodos Setup, Log e Shutdown, implementada por todas as classes concretas responsáveis pelos destinos de saída dos logs. |
| ConsoleLogHandler.mqh | Implementa ILogHandler para exibir mensagens de log formatadas na guia "Experts" do MetaTrader, com filtragem por nível de severidade. |
| FileLogHandler.mqh | Implementa ILogHandler para gravar logs em arquivos, com rotação diária, limite de tamanho e retenção configurável do histórico de arquivos. |
| CLogger.mqh | Logger singleton que armazena os handlers, aplica a filtragem global por severidade e oferece métodos simplificados de logging. |
| LoggingExampleEA.mq5 | EA de exemplo que mostra, na prática, como configurar, utilizar e realizar a limpeza da camada personalizada de logging. |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17933
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.
Machine Learning e Data Science (Parte 38): Aplicação de Transfer Learning nos mercados de câmbio
Operando com o Calendário Econômico do MQL5 (Parte 7): Preparação para Testes de Estratégia com Análise de Eventos de Notícias por Recurso Incorporado
Criação de um painel administrativo de trading em MQL5 (Parte XI): Interface moderna de mensagens na plataforma (I)
Recursos do Assistente MQL5 que você precisa conhecer (Parte 64): Uso de padrões dos canais DeMarker e Envelopes com kernel de ruído branco
- 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