Automatização de estratégias de trading em MQL5 (Parte 21): Aprimorando o trading baseado em redes neurais com taxas de aprendizado adaptativas
Introdução
No artigo anterior (Parte 20), desenvolvemos uma estratégia multissímbolo baseada no Commodity Channel Index (CCI) e no Awesome Oscillator (AO), que permitiu automatizar operações de reversão de tendência em vários pares de moedas, com geração confiável de sinais e gestão de risco em MetaQuotes Language 5 (MQL5). Nesta Parte 21, vamos explorar uma estratégia de trading baseada em redes neurais, aprimorada com um mecanismo de taxa de aprendizado adaptativa (adaptive learning rate) para aumentar a precisão das previsões dos movimentos do mercado. Abordaremos os seguintes tópicos:
- Estratégia de taxa de aprendizado adaptativa para redes neurais
- Implementação em MetaQuotes Language 5 (MQL5)
- Testes e otimização dos parâmetros da taxa de aprendizado
- Conclusão
Ao final, teremos um sistema de trading completo, desenvolvido em MetaQuotes Language 5 (MQL5), que utiliza redes neurais com ajuste dinâmico da taxa de aprendizado e estará pronto para receber novos aprimoramentos.
Estratégia de taxa de aprendizado adaptativa para redes neurais
Na Parte 20, desenvolvemos um sistema de trading multissímbolo que utiliza o Commodity Channel Index e o Awesome Oscillator para automatizar operações de reversão de tendência em vários pares de moedas. Agora, vamos desenvolver uma estratégia de trading baseada em redes neurais, utilizando modelos computacionais inspirados na estrutura de neurônios interconectados do cérebro humano. Esses modelos permitem analisar diferentes indicadores de mercado e adaptar o processo de treinamento à volatilidade do mercado, buscando previsões mais precisas dos movimentos dos preços. Nosso objetivo é construir um sistema de trading flexível e de alto desempenho, capaz de identificar padrões complexos de mercado e executar operações com maior precisão graças ao mecanismo de ajuste da taxa de aprendizado.
As redes neurais são formadas por camadas de unidades, chamadas neurônios, organizadas em uma camada de entrada (que recebe os dados de mercado), camadas ocultas (que identificam padrões complexos) e uma camada de saída (que gera sinais de trading, como previsões de alta ou queda dos preços). Na propagação para frente (forward propagation), os dados percorrem essas camadas, enquanto os neurônios aplicam pesos e vieses às entradas para produzir as previsões. Observe a figura abaixo.

Um componente essencial desse mecanismo é a função de ativação, que introduz não linearidade nas transformações e permite que a rede modele relações complexas. Por exemplo, a função de ativação sigmoide mapeia os valores para o intervalo de 0 a 1, sendo especialmente adequada a tarefas de classificação binária, como a tomada de decisões de compra ou venda. A retropropagação (backpropagation) complementa esse mecanismo ao utilizar os erros das previsões para ajustar os pesos e vieses, percorrendo a rede no sentido inverso e melhorando gradualmente sua precisão. Observe a figura abaixo.

Para tornar a rede mais adaptável, vamos implementar uma estratégia de taxa de aprendizado adaptativa que ajuste a magnitude das atualizações dos pesos durante a retropropagação. Assim, a taxa de aprendizado será aumentada quando as previsões estiverem alinhadas aos resultados observados no mercado e reduzida quando houver um aumento acentuado dos erros, preservando a estabilidade do treinamento.
Vamos desenvolver um sistema que alimenta a camada de entrada da rede neural com indicadores de mercado, como médias móveis e indicadores de momentum, processa esses dados nas camadas ocultas para identificar padrões e gera sinais de trading confiáveis na camada de saída. Utilizaremos a função de ativação sigmoide para transformar as saídas dos neurônios em previsões contínuas e de fácil interpretação, que servirão de base para as decisões de trading. Escolhemos essa função por ser adequada a problemas com duas possíveis classes de saída. Veja abaixo alguns exemplos de outras funções de ativação que também podem ser utilizadas.

Também ajustaremos dinamicamente a taxa de aprendizado com base nos resultados do treinamento e adaptaremos a quantidade de neurônios da camada oculta à volatilidade do mercado. Com isso, teremos um sistema capaz de responder às mudanças nas condições de mercado, equilibrando complexidade e eficiência. Essa estratégia estabelece a base para uma implementação robusta e testes rigorosos. Para alimentar a rede, utilizaremos quatro indicadores: duas médias móveis, o Índice de Força Relativa (Relative Strength Index, RSI) e o Intervalo Verdadeiro Médio (Average True Range, ATR). Assim que os sinais forem gerados, abriremos as operações correspondentes. A figura abaixo ilustra o resultado que pretendemos alcançar com essa implementação.

Implementação em MQL5
Para criar um programa em MQL5, abra o MetaEditor, acesse o "Navegador", selecione a pasta Indicators, clique em "Criar" e siga as instruções para gerar o arquivo. Com o arquivo criado no ambiente de desenvolvimento, começaremos declarando os parâmetros de entrada, as estruturas e as classes necessárias, já que adotaremos uma abordagem de programação orientada a objetos (Object Oriented Programming, OOP).
//+------------------------------------------------------------------+ //| Neural Networks Propagation EA.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #include <Trade/Trade.mqh> CTrade tradeObject; //--- Instantiate trade object for executing trades // Input parameters with clear, meaningful names input double LotSize = 0.1; // Lot Size input int StopLossPoints = 100; // Stop Loss (points) input int TakeProfitPoints = 100; // Take Profit (points) input int MinHiddenNeurons = 10; // Minimum Hidden Neurons input int MaxHiddenNeurons = 50; // Maximum Hidden Neurons input int TrainingBarCount = 1000; // Training Bars input double MinPredictionAccuracy = 0.7; // Minimum Prediction Accuracy input double MinLearningRate = 0.01; // Minimum Learning Rate input double MaxLearningRate = 0.5; // Maximum Learning Rate input string InputToHiddenWeights = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Input-to-Hidden Weights input string HiddenToOutputWeights = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Hidden-to-Output Weights input string HiddenBiases = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Hidden Biases input string OutputBiases = "0.1,-0.1"; // Output Biases // Neural Network Structure Constants const int INPUT_NEURON_COUNT = 10; //--- Define number of input neurons const int OUTPUT_NEURON_COUNT = 2; //--- Define number of output neurons const int MAX_HISTORY_SIZE = 10; //--- Define maximum history size for accuracy and error tracking // Indicator handles int ma20IndicatorHandle; //--- Handle for 20-period moving average int ma50IndicatorHandle; //--- Handle for 50-period moving average int rsiIndicatorHandle; //--- Handle for RSI indicator int atrIndicatorHandle; //--- Handle for ATR indicator // Training related structures struct TrainingData { double inputValues[]; //--- Array to store input values for training double targetValues[]; //--- Array to store target values for training };
Nesta etapa, vamos estabelecer a base da nossa estratégia de trading com redes neurais e taxa de aprendizado adaptativa, inicializando os principais componentes responsáveis pela execução das operações e pelo processamento dos dados. Incluiremos a biblioteca Trade.mqh e criaremos uma instância da classe CTrade chamada tradeObject para gerenciar as operações de trading. Com ela, poderemos executar ordens de compra e venda com base nas previsões da rede neural.
Definiremos os parâmetros de entrada para configurar a estratégia: LotSize, para controlar o volume das operações; StopLossPoints e TakeProfitPoints, para a gestão de risco; e MinHiddenNeurons e MaxHiddenNeurons, para estabelecer os limites da quantidade de neurônios na camada oculta da rede neural. Também definiremos TrainingBarCount para determinar a quantidade de barras históricas utilizadas no treinamento, MinPredictionAccuracy como limite mínimo de precisão das previsões e MinLearningRate e MaxLearningRate para estabelecer os limites da taxa de aprendizado adaptativa. Além disso, disponibilizaremos InputToHiddenWeights, HiddenToOutputWeights, HiddenBiases e OutputBiases como parâmetros de entrada do tipo string para inicializar os pesos e vieses da rede neural, permitindo utilizar valores predefinidos ou os valores padrão.
Em seguida, definiremos as constantes que determinam a estrutura da rede neural: INPUT_NEURON_COUNT será definida como 10 para receber os dados de mercado, OUTPUT_NEURON_COUNT como 2 para gerar os sinais de compra e venda, e MAX_HISTORY_SIZE como 10 para acompanhar o histórico de precisão e erros durante o treinamento. Criaremos os handles ma20IndicatorHandle, ma50IndicatorHandle, rsiIndicatorHandle e atrIndicatorHandle para acessar, respectivamente, as médias móveis de 20 e 50 períodos, o RSI e o Average True Range (ATR), cujos dados serão utilizados para alimentar a rede neural. Por fim, definiremos a estrutura TrainingData, com os arrays inputValues e targetValues, para armazenar os atributos de entrada e as saídas esperadas durante o treinamento. Dessa forma, os dados de treinamento ficarão estruturados para serem utilizados pela rede neural. Após preenchê-los com os valores correspondentes, teremos a estrutura ilustrada abaixo.

Em seguida, precisaremos definir uma classe para armazenar a maior parte das variáveis-membro essenciais que serão utilizadas com frequência.
// Neural Network Class class CNeuralNetwork { private: int inputNeuronCount; //--- Number of input neurons int hiddenNeuronCount; //--- Number of hidden neurons int outputNeuronCount; //--- Number of output neurons double inputLayer[]; //--- Array for input layer values double hiddenLayer[]; //--- Array for hidden layer values double outputLayer[]; //--- Array for output layer values double inputToHiddenWeights[]; //--- Weights between input and hidden layers double hiddenToOutputWeights[];//--- Weights between hidden and output layers double hiddenLayerBiases[]; //--- Biases for hidden layer double outputLayerBiases[]; //--- Biases for output layer double outputDeltas[]; //--- Delta values for output layer double hiddenDeltas[]; //--- Delta values for hidden layer double trainingError; //--- Current training error double currentLearningRate; //--- Current learning rate double accuracyHistory[]; //--- History of training accuracy double errorHistory[]; //--- History of training errors int historyRecordCount; //--- Number of recorded history entries };
Nesta etapa, vamos implementar a estrutura principal da rede neural por meio da classe CNeuralNetwork. Definiremos variáveis-membro privadas para gerenciar a arquitetura da rede e o treinamento, começando por inputNeuronCount, hiddenNeuronCount e outputNeuronCount, que estabelecerão a quantidade de neurônios nas camadas de entrada, oculta e de saída, respectivamente. Essa configuração seguirá a arquitetura da estratégia para processar os dados de mercado e gerar sinais de trading.
Criaremos arrays para armazenar os valores de cada camada: inputLayer receberá os dados dos indicadores, hiddenLayer armazenará os resultados intermediários do processamento dos padrões e outputLayer conterá as previsões de compra e venda. Para realizar os cálculos da rede neural, definiremos os arrays inputToHiddenWeights e hiddenToOutputWeights, responsáveis pelos pesos das conexões entre as camadas, além de hiddenLayerBiases e outputLayerBiases, que armazenarão os vieses das camadas oculta e de saída. Para a retropropagação, utilizaremos outputDeltas e hiddenDeltas para armazenar os termos de erro necessários ao cálculo dos gradientes, permitindo atualizar os pesos e vieses durante o treinamento.
Também incluiremos trainingError para acompanhar o erro atual de treinamento, currentLearningRate para controlar a taxa de aprendizado adaptativa, os arrays accuracyHistory e errorHistory para monitorar a evolução da precisão e dos erros ao longo do treinamento, e historyRecordCount para contabilizar os registros armazenados. Com esses componentes, a rede poderá ajustar dinamicamente seu treinamento conforme as tendências observadas no desempenho. Essa classe servirá de base para implementar a propagação para frente, a retropropagação e os ajustes da taxa de aprendizado adaptativa nas próximas funções. Na seção privada da classe, também poderemos definir um método para converter os parâmetros de entrada do tipo string em arrays, permitindo utilizar seus valores posteriormente.
// Parse comma-separated string to array bool ParseStringToArray(string inputString, double &output[], int expectedSize) { //--- Check if input string is empty if(inputString == "") return false; string values[]; //--- Initialize array for parsed values ArrayResize(values, 0); //--- Split input string by comma int count = StringSplit(inputString, 44, values); //--- Check if string splitting failed if(count <= 0) { Print("Error: StringSplit failed for input: ", inputString, ". Error code: ", GetLastError()); return false; } //--- Verify correct number of values if(count != expectedSize) { Print("Error: Invalid number of values in input string. Expected: ", expectedSize, ", Got: ", count); return false; } //--- Resize output array to expected size ArrayResize(output, expectedSize); //--- Convert string values to doubles and normalize for(int i = 0; i < count; i++) { output[i] = StringToDouble(values[i]); //--- Clamp values between -1.0 and 1.0 if(MathAbs(output[i]) > 1.0) output[i] = MathMax(-1.0, MathMin(1.0, output[i])); } return true; }
Vamos implementar o método ParseStringToArray na classe CNeuralNetwork para processar strings contendo os pesos e vieses da rede neural, separados por vírgulas. Primeiro, verificaremos se inputString está vazia e retornaremos false caso não contenha dados válidos. Em seguida, utilizaremos StringSplit com a vírgula como delimitador para separar os valores. Se a divisão falhar ou se count for diferente de expectedSize, registraremos o erro com Print e retornaremos false. Depois, ajustaremos o tamanho do array de saída com ArrayResize, converteremos os valores para o tipo double usando StringToDouble e os limitaremos ao intervalo de -1.0 a 1.0 com MathMax e MathMin. Se todo o processamento for concluído corretamente, retornaremos true. Os demais métodos auxiliares poderão ser declarados na seção pública da classe, conforme mostrado abaixo, e implementados posteriormente.
public: CNeuralNetwork(int inputs, int hidden, int outputs); //--- Constructor void InitializeWeights(); //--- Initialize network weights double Sigmoid(double x); //--- Apply sigmoid activation function void ForwardPropagate(); //--- Perform forward propagation void Backpropagate(const double &targets[]); //--- Perform backpropagation void SetInput(double &inputs[]); //--- Set input values void GetOutput(double &outputs[]); //--- Retrieve output values double TrainOnHistoricalData(TrainingData &data[]); //--- Train on historical data void UpdateNetworkWithRecentData(); //--- Update with recent data void InitializeTraining(); //--- Initialize training arrays void ResizeNetwork(int newHiddenNeurons); //--- Resize network void AdjustLearningRate(); //--- Adjust learning rate dynamically double GetRecentAccuracy(); //--- Get recent training accuracy double GetRecentError(); //--- Get recent training error bool ShouldRetrain(); //--- Check if retraining is needed double CalculateDynamicNeurons(); //--- Calculate dynamic neuron count int GetHiddenNeurons() { return hiddenNeuronCount; //--- Get current hidden neuron count }
Agora, vamos definir a interface pública da classe CNeuralNetwork. Criaremos o construtor CNeuralNetwork com os parâmetros inputs, hidden e outputs para configurar a estrutura da rede, além do método InitializeWeights para inicializar os pesos e vieses. Implementaremos Sigmoid como função de ativação, ForwardPropagate para a propagação para frente e Backpropagate para realizar a retropropagação e atualizar os parâmetros com base nos valores esperados (targets). Também definiremos SetInput e GetOutput para atribuir os dados de entrada e recuperar as saídas da rede.
Criaremos TrainOnHistoricalData para treinar a rede com os dados da estrutura TrainingData, UpdateNetworkWithRecentData para atualizá-la com dados recentes, InitializeTraining para inicializar os arrays necessários ao treinamento, ResizeNetwork para redimensionar a rede de acordo com newHiddenNeurons e AdjustLearningRate para ajustar dinamicamente a taxa de aprendizado. Também incluiremos GetRecentAccuracy, GetRecentError, ShouldRetrain, CalculateDynamicNeurons e GetHiddenNeurons para monitorar o desempenho da rede e adaptar a quantidade de neurônios da camada oculta, definida por hiddenNeuronCount. Com a interface estabelecida, podemos começar a implementar os métodos, conforme mostrado abaixo.
// Constructor CNeuralNetwork::CNeuralNetwork(int inputs, int hidden, int outputs) { //--- Set input neuron count inputNeuronCount = inputs; //--- Set output neuron count outputNeuronCount = outputs; //--- Initialize learning rate to minimum currentLearningRate = MinLearningRate; //--- Set hidden neuron count hiddenNeuronCount = hidden; //--- Ensure hidden neurons within bounds if(hiddenNeuronCount < MinHiddenNeurons) hiddenNeuronCount = MinHiddenNeurons; if(hiddenNeuronCount > MaxHiddenNeurons) hiddenNeuronCount = MaxHiddenNeurons; //--- Resize input layer array ArrayResize(inputLayer, inputs); //--- Resize hidden layer array ArrayResize(hiddenLayer, hiddenNeuronCount); //--- Resize output layer array ArrayResize(outputLayer, outputs); //--- Resize input-to-hidden weights array ArrayResize(inputToHiddenWeights, inputs * hiddenNeuronCount); //--- Resize hidden-to-output weights array ArrayResize(hiddenToOutputWeights, hiddenNeuronCount * outputs); //--- Resize hidden biases array ArrayResize(hiddenLayerBiases, hiddenNeuronCount); //--- Resize output biases array ArrayResize(outputLayerBiases, outputs); //--- Resize accuracy history array ArrayResize(accuracyHistory, MAX_HISTORY_SIZE); //--- Resize error history array ArrayResize(errorHistory, MAX_HISTORY_SIZE); //--- Initialize history record count historyRecordCount = 0; //--- Initialize training error trainingError = 0.0; //--- Initialize network weights InitializeWeights(); //--- Initialize training arrays InitializeTraining(); } // Initialize training arrays void CNeuralNetwork::InitializeTraining() { //--- Resize output deltas array ArrayResize(outputDeltas, outputNeuronCount); //--- Resize hidden deltas array ArrayResize(hiddenDeltas, hiddenNeuronCount); } // Initialize weights void CNeuralNetwork::InitializeWeights() { //--- Track if weights and biases are set bool isInputToHiddenWeightsSet = false; bool isHiddenToOutputWeightsSet = false; bool isHiddenBiasesSet = false; bool isOutputBiasesSet = false; double tempInputToHiddenWeights[]; double tempHiddenToOutputWeights[]; double tempHiddenBiases[]; double tempOutputBiases[]; //--- Parse and set input-to-hidden weights if provided if(InputToHiddenWeights != "" && ParseStringToArray(InputToHiddenWeights, tempInputToHiddenWeights, inputNeuronCount * hiddenNeuronCount)) { //--- Copy parsed weights to main array ArrayCopy(inputToHiddenWeights, tempInputToHiddenWeights); isInputToHiddenWeightsSet = true; //--- Log weight initialization Print("Initialized input-to-hidden weights from input: ", InputToHiddenWeights); } //--- Parse and set hidden-to-output weights if provided if(HiddenToOutputWeights != "" && ParseStringToArray(HiddenToOutputWeights, tempHiddenToOutputWeights, hiddenNeuronCount * outputNeuronCount)) { //--- Copy parsed weights to main array ArrayCopy(hiddenToOutputWeights, tempHiddenToOutputWeights); isHiddenToOutputWeightsSet = true; //--- Log weight initialization Print("Initialized hidden-to-output weights from input: ", HiddenToOutputWeights); } //--- Parse and set hidden biases if provided if(HiddenBiases != "" && ParseStringToArray(HiddenBiases, tempHiddenBiases, hiddenNeuronCount)) { //--- Copy parsed biases to main array ArrayCopy(hiddenLayerBiases, tempHiddenBiases); isHiddenBiasesSet = true; //--- Log bias initialization Print("Initialized hidden biases from input: ", HiddenBiases); } //--- Parse and set output biases if provided if(OutputBiases != "" && ParseStringToArray(OutputBiases, tempOutputBiases, outputNeuronCount)) { //--- Copy parsed biases to main array ArrayCopy(outputLayerBiases, tempOutputBiases); isOutputBiasesSet = true; //--- Log bias initialization Print("Initialized output biases from input: ", OutputBiases); } //--- Initialize input-to-hidden weights randomly if not set if(!isInputToHiddenWeightsSet) { for(int i = 0; i < ArraySize(inputToHiddenWeights); i++) inputToHiddenWeights[i] = (MathRand() / 32767.0) * 2 - 1; } //--- Initialize hidden-to-output weights randomly if not set if(!isHiddenToOutputWeightsSet) { for(int i = 0; i < ArraySize(hiddenToOutputWeights); i++) hiddenToOutputWeights[i] = (MathRand() / 32767.0) * 2 - 1; } //--- Initialize hidden biases randomly if not set if(!isHiddenBiasesSet) { for(int i = 0; i < ArraySize(hiddenLayerBiases); i++) hiddenLayerBiases[i] = (MathRand() / 32767.0) * 2 - 1; } //--- Initialize output biases randomly if not set if(!isOutputBiasesSet) { for(int i = 0; i < ArraySize(outputLayerBiases); i++) outputLayerBiases[i] = (MathRand() / 32767.0) * 2 - 1; } } // Sigmoid activation function double CNeuralNetwork::Sigmoid(double x) { //--- Compute and return sigmoid value return 1.0 / (1.0 + MathExp(-x)); } // Set input void CNeuralNetwork::SetInput(double &inputs[]) { //--- Check for input array size mismatch if(ArraySize(inputs) != inputNeuronCount) { Print("Error: Input array size mismatch. Expected: ", inputNeuronCount, ", Got: ", ArraySize(inputs)); return; } //--- Copy inputs to input layer ArrayCopy(inputLayer, inputs); } // Forward propagation void CNeuralNetwork::ForwardPropagate() { //--- Compute hidden layer values for(int j = 0; j < hiddenNeuronCount; j++) { double sum = 0; //--- Calculate weighted sum for hidden neuron for(int i = 0; i < inputNeuronCount; i++) sum += inputLayer[i] * inputToHiddenWeights[i * hiddenNeuronCount + j]; //--- Apply sigmoid activation hiddenLayer[j] = Sigmoid(sum + hiddenLayerBiases[j]); } //--- Compute output layer values for(int j = 0; j < outputNeuronCount; j++) { double sum = 0; //--- Calculate weighted sum for output neuron for(int i = 0; i < hiddenNeuronCount; i++) sum += hiddenLayer[i] * hiddenToOutputWeights[i * outputNeuronCount + j]; //--- Apply sigmoid activation outputLayer[j] = Sigmoid(sum + outputLayerBiases[j]); } } // Get output void CNeuralNetwork::GetOutput(double &outputs[]) { //--- Resize output array ArrayResize(outputs, outputNeuronCount); //--- Copy output layer to outputs ArrayCopy(outputs, outputLayer); } // Backpropagation void CNeuralNetwork::Backpropagate(const double &targets[]) { //--- Calculate output layer deltas for(int i = 0; i < outputNeuronCount; i++) { double output = outputLayer[i]; //--- Compute delta for output neuron outputDeltas[i] = output * (1 - output) * (targets[i] - output); } //--- Calculate hidden layer deltas for(int i = 0; i < hiddenNeuronCount; i++) { double error = 0; //--- Sum weighted errors from output layer for(int j = 0; j < outputNeuronCount; j++) error += outputDeltas[j] * hiddenToOutputWeights[i * outputNeuronCount + j]; double output = hiddenLayer[i]; //--- Compute delta for hidden neuron hiddenDeltas[i] = output * (1 - output) * error; } //--- Update hidden-to-output weights for(int i = 0; i < hiddenNeuronCount; i++) { for(int j = 0; j < outputNeuronCount; j++) { int idx = i * outputNeuronCount + j; //--- Adjust weight based on learning rate and delta hiddenToOutputWeights[idx] += currentLearningRate * outputDeltas[j] * hiddenLayer[i]; } } //--- Update input-to-hidden weights for(int i = 0; i < inputNeuronCount; i++) { for(int j = 0; j < hiddenNeuronCount; j++) { int idx = i * hiddenNeuronCount + j; //--- Adjust weight based on learning rate and delta inputToHiddenWeights[idx] += currentLearningRate * hiddenDeltas[j] * inputLayer[i]; } } //--- Update hidden biases for(int i = 0; i < hiddenNeuronCount; i++) //--- Adjust bias based on learning rate and delta hiddenLayerBiases[i] += currentLearningRate * hiddenDeltas[i]; //--- Update output biases for(int i = 0; i < outputNeuronCount; i++) //--- Adjust bias based on learning rate and delta outputLayerBiases[i] += currentLearningRate * outputDeltas[i]; } // Resize network (adjust hidden neurons) void CNeuralNetwork::ResizeNetwork(int newHiddenNeurons) { //--- Clamp new neuron count within bounds newHiddenNeurons = MathMax(MinHiddenNeurons, MathMin(newHiddenNeurons, MaxHiddenNeurons)); //--- Check if resizing is necessary if(newHiddenNeurons == hiddenNeuronCount) return; //--- Log resizing information Print("Resizing network. New hidden neurons: ", newHiddenNeurons, ", Previous: ", hiddenNeuronCount); //--- Update hidden neuron count hiddenNeuronCount = newHiddenNeurons; //--- Resize hidden layer array ArrayResize(hiddenLayer, hiddenNeuronCount); //--- Resize input-to-hidden weights array ArrayResize(inputToHiddenWeights, inputNeuronCount * hiddenNeuronCount); //--- Resize hidden-to-output weights array ArrayResize(hiddenToOutputWeights, hiddenNeuronCount * outputNeuronCount); //--- Resize hidden biases array ArrayResize(hiddenLayerBiases, hiddenNeuronCount); //--- Resize hidden deltas array ArrayResize(hiddenDeltas, hiddenNeuronCount); //--- Reinitialize weights InitializeWeights(); }
Nesta etapa, vamos implementar os componentes essenciais da rede neural. Começaremos pelo construtor CNeuralNetwork, que receberá os parâmetros inputs, hidden e outputs para definir inputNeuronCount, outputNeuronCount e hiddenNeuronCount. A quantidade de neurônios da camada oculta será limitada ao intervalo estabelecido por MinHiddenNeurons e MaxHiddenNeurons. Não precisamos nos aprofundar na explicação da estrutura da classe, pois já utilizamos uma abordagem semelhante na parte anterior.
Em seguida, inicializaremos currentLearningRate com o valor de MinLearningRate e utilizaremos ArrayResize para dimensionar os arrays inputLayer, hiddenLayer, outputLayer, inputToHiddenWeights, hiddenToOutputWeights, hiddenLayerBiases, outputLayerBiases, accuracyHistory e errorHistory. Também chamaremos InitializeWeights e InitializeTraining para concluir a configuração inicial da rede.
Implementaremos o método InitializeTraining para dimensionar os arrays outputDeltas e hiddenDeltas, utilizados na retropropagação, garantindo o armazenamento correto dos valores de erro utilizados na retropropagação. O método InitializeWeights será responsável por definir os pesos e vieses da rede. Para isso, utilizará ParseStringToArray para carregar os valores de inputToHiddenWeights, hiddenToOutputWeights, hiddenLayerBiases e outputLayerBiases a partir dos parâmetros de entrada correspondentes, como InputToHiddenWeights, quando estiverem disponíveis. Caso contrário, os valores serão inicializados aleatoriamente no intervalo de -1 a 1 por meio de MathRand. As operações de inicialização serão registradas com Print. Por fim, implementaremos Sigmoid para calcular a ativação sigmoide de um determinado valor x, utilizando MathExp.
O método SetInput copiará os dados de entrada para inputLayer após verificar com ArraySize se o tamanho dos dados de entrada corresponde ao tamanho esperado. Caso haja alguma incompatibilidade, o erro será registrado com Print. Em seguida, implementaremos ForwardPropagate para calcular os valores de hiddenLayer e outputLayer, calculando as somas ponderadas com os vieses armazenados em hiddenLayerBiases e outputLayerBiases e, em seguida, aplicando a função de ativação Sigmoid. O método GetOutput utilizará ArrayResize para dimensionar o array de saída e ArrayCopy para copiar os valores de outputLayer.
Implementaremos Backpropagate para calcular outputDeltas e hiddenDeltas com base nos valores esperados (targets), atualizando hiddenToOutputWeights, inputToHiddenWeights, hiddenLayerBiases e outputLayerBiases de acordo com currentLearningRate. Por fim, ResizeNetwork ajustará hiddenNeuronCount dentro dos limites estabelecidos, redimensionará arrays como hiddenLayer e hiddenDeltas e reinicializará os pesos por meio de InitializeWeights, registrando as alterações com Print. Depois disso, precisaremos ajustar a taxa de aprendizado conforme a evolução dos erros.
// Adjust learning rate based on error trend void CNeuralNetwork::AdjustLearningRate() { //--- Check if enough history exists if(historyRecordCount < 2) return; //--- Get last and previous errors double lastError = errorHistory[historyRecordCount - 1]; double prevError = errorHistory[historyRecordCount - 2]; //--- Calculate error difference double errorDiff = lastError - prevError; //--- Increase learning rate if error decreased if(lastError < prevError) currentLearningRate = MathMin(currentLearningRate * 1.05, MaxLearningRate); //--- Decrease learning rate if error increased significantly else if(lastError > prevError * 1.2) currentLearningRate = MathMax(currentLearningRate * 0.9, MinLearningRate); //--- Slightly decrease learning rate otherwise else currentLearningRate = MathMax(currentLearningRate * 0.99, MinLearningRate); //--- Log learning rate adjustment Print("Adjusted learning rate to: ", currentLearningRate, ", Last Error: ", lastError, ", Prev Error: ", prevError, ", Error Diff: ", errorDiff); }
Para implementar o mecanismo de taxa de aprendizado adaptativa, criaremos o método AdjustLearningRate na classe CNeuralNetwork. Primeiro, verificaremos se historyRecordCount é maior ou igual a 2, garantindo que errorHistory contenha pelo menos dois registros. Caso contrário, encerraremos a execução do método para evitar ajustes sem dados suficientes. Em seguida, recuperaremos o erro mais recente em lastError e o erro anterior em prevError, ambos de errorHistory, e calcularemos a diferença entre eles em errorDiff para avaliar a evolução do treinamento.
Utilizaremos MathMin para aumentar currentLearningRate em 5% quando lastError for menor que prevError, respeitando o limite MaxLearningRate. Se lastError superar prevError em mais de 20%, reduziremos a taxa em 10% com MathMax, sem permitir que fique abaixo de MinLearningRate. Nos demais casos, aplicaremos uma pequena redução de 1%, também utilizando MathMax. Por fim, registraremos com Print os valores de currentLearningRate após o ajuste, lastError, prevError e errorDiff, permitindo acompanhar a adaptação da taxa de aprendizado ao desempenho da rede neural. Em seguida, utilizaremos o indicador ATR para calcular dinamicamente a quantidade de neurônios da camada oculta, ajustando-a conforme a volatilidade do mercado.
// Calculate dynamic number of hidden neurons based on ATR double CNeuralNetwork::CalculateDynamicNeurons() { double atrValues[]; //--- Set ATR array as series ArraySetAsSeries(atrValues, true); //--- Copy ATR buffer if(CopyBuffer(atrIndicatorHandle, 0, 0, 10, atrValues) < 10) { Print("Error: Failed to copy ATR for dynamic neurons. Using default: ", hiddenNeuronCount); return hiddenNeuronCount; } //--- Calculate average ATR double avgATR = 0; for(int i = 0; i < 10; i++) avgATR += atrValues[i]; avgATR /= 10; //--- Get current close price double closePrice = iClose(_Symbol, PERIOD_CURRENT, 0); //--- Check for valid close price if(MathAbs(closePrice) < 0.000001) { Print("Error: Invalid close price for ATR ratio. Using default: ", hiddenNeuronCount); return hiddenNeuronCount; } //--- Calculate ATR ratio double atrRatio = atrValues[0] / closePrice; //--- Compute new neuron count int newNeurons = MinHiddenNeurons + (int)((MaxHiddenNeurons - MinHiddenNeurons) * MathMin(atrRatio * 100, 1.0)); //--- Return clamped neuron count return MathMax(MinHiddenNeurons, MathMin(newNeurons, MaxHiddenNeurons)); }
Para adaptar dinamicamente a quantidade de neurônios da camada oculta à volatilidade do mercado, declararemos o array atrValues e utilizaremos ArraySetAsSeries para configurá-lo como uma série temporal. Depois, chamaremos CopyBuffer para copiar os valores do ATR correspondentes a 10 barras, obtidos por meio de atrIndicatorHandle, para atrValues. Se CopyBuffer retornar menos de 10 valores, registraremos o erro com Print e manteremos a quantidade atual de neurônios, definida por hiddenNeuronCount. É fundamental configurar o array como uma série temporal para que os dados mais recentes correspondam aos primeiros índices e possam ser acessados a partir deles.

Calcularemos o ATR médio somando os valores de atrValues correspondentes às 10 barras e dividindo o resultado por 10. O valor obtido será armazenado em avgATR. Em seguida, utilizaremos iClose para obter o preço de fechamento em closePrice, considerando _Symbol e PERIOD_CURRENT. Se closePrice estiver próximo de zero, registraremos o erro com Print e retornaremos hiddenNeuronCount. Calcularemos a razão atrRatio dividindo atrValues[0] por closePrice e, com base em atrRatio, calcularemos newNeurons escalando o intervalo entre MinHiddenNeurons e MaxHiddenNeurons e utilizando MathMin para aplicar o limite correspondente. Por fim, aplicaremos MathMax e MathMin para garantir que a quantidade calculada permaneça dentro dos limites estabelecidos e retornaremos o valor ajustado. Dessa forma, a rede poderá adaptar dinamicamente sua quantidade de neurônios às condições de mercado. Agora, podemos implementar os demais métodos de treinamento e atualização, conforme mostrado abaixo.
// Train network on historical data double CNeuralNetwork::TrainOnHistoricalData(TrainingData &data[]) { const int maxEpochs = 100; //--- Maximum training epochs const double targetError = 0.01; //--- Target error threshold double accuracy = 0; //--- Training accuracy //--- Reset learning rate currentLearningRate = MinLearningRate; //--- Iterate through epochs for(int epoch = 0; epoch < maxEpochs; epoch++) { double totalError = 0; //--- Total error for epoch int correctPredictions = 0; //--- Count of correct predictions //--- Process each training sample for(int i = 0; i < ArraySize(data); i++) { //--- Check target array size if(ArraySize(data[i].targetValues) != outputNeuronCount) { Print("Error: Mismatch in targets size for training data at index ", i); continue; } //--- Set input values SetInput(data[i].inputValues); //--- Perform forward propagation ForwardPropagate(); double error = 0; //--- Calculate error for(int j = 0; j < outputNeuronCount; j++) error += MathPow(data[i].targetValues[j] - outputLayer[j], 2); totalError += error; //--- Check prediction correctness if((outputLayer[0] > outputLayer[1] && data[i].targetValues[0] > data[i].targetValues[1]) || (outputLayer[0] < outputLayer[1] && data[i].targetValues[0] < data[i].targetValues[1])) correctPredictions++; //--- Perform backpropagation Backpropagate(data[i].targetValues); } //--- Calculate accuracy accuracy = (double)correctPredictions / ArraySize(data); //--- Update training error trainingError = totalError / ArraySize(data); //--- Update history if(historyRecordCount < MAX_HISTORY_SIZE) { accuracyHistory[historyRecordCount] = accuracy; errorHistory[historyRecordCount] = trainingError; historyRecordCount++; } else { //--- Shift history arrays for(int i = 1; i < MAX_HISTORY_SIZE; i++) { accuracyHistory[i - 1] = accuracyHistory[i]; errorHistory[i - 1] = errorHistory[i]; } //--- Add new values accuracyHistory[MAX_HISTORY_SIZE - 1] = accuracy; errorHistory[MAX_HISTORY_SIZE - 1] = trainingError; } //--- Log error history update Print("Error history updated: ", errorHistory[historyRecordCount - 1]); //--- Adjust learning rate AdjustLearningRate(); //--- Log progress every 10 epochs if(epoch % 10 == 0) Print("Epoch ", epoch, ": Error = ", trainingError, ", Accuracy = ", accuracy); //--- Check for early stopping if(trainingError < targetError && accuracy >= MinPredictionAccuracy) break; } //--- Return final accuracy return accuracy; } // Update network with recent data void CNeuralNetwork::UpdateNetworkWithRecentData() { const int recentBarCount = 10; //--- Number of recent bars to process TrainingData recentData[]; //--- Collect recent training data if(!CollectTrainingData(recentData, recentBarCount)) return; //--- Process each recent data sample for(int i = 0; i < ArraySize(recentData); i++) { //--- Set input values SetInput(recentData[i].inputValues); //--- Perform forward propagation ForwardPropagate(); //--- Perform backpropagation Backpropagate(recentData[i].targetValues); } } // Get recent accuracy double CNeuralNetwork::GetRecentAccuracy() { //--- Check if history exists if(historyRecordCount == 0) return 0.0; //--- Return most recent accuracy return accuracyHistory[historyRecordCount - 1]; } // Get recent error double CNeuralNetwork::GetRecentError() { //--- Check if history exists if(historyRecordCount == 0) return 0.0; //--- Return most recent error return errorHistory[historyRecordCount - 1]; } // Check if retraining is needed bool CNeuralNetwork::ShouldRetrain() { //--- Check if enough history exists if(historyRecordCount < 2) return false; //--- Get recent metrics double recentAccuracy = GetRecentAccuracy(); double recentError = GetRecentError(); double prevError = errorHistory[historyRecordCount - 2]; //--- Determine if retraining is needed return (recentAccuracy < MinPredictionAccuracy || recentError > prevError * 1.5); }
Nesta etapa, vamos implementar os principais mecanismos de treinamento e atualização da rede neural. Criaremos o método TrainOnHistoricalData para treinar a rede com um array de estruturas TrainingData, definindo maxEpochs como 100 e targetError como 0.01, além de reinicializar currentLearningRate com MinLearningRate. Em cada época, percorreremos as amostras de treinamento, verificaremos se o tamanho de data[i].targetValues corresponde a outputNeuronCount e utilizaremos SetInput para carregar data[i].inputValues. Em seguida, chamaremos ForwardPropagate para calcular as previsões. Utilizaremos MathPow para calcular o erro a partir da diferença entre outputLayer e data[i].targetValues, contabilizaremos as previsões corretas em correctPredictions e aplicaremos Backpropagate com data[i].targetValues.
Atualizaremos accuracy e trainingError e armazenaremos seus valores em accuracyHistory e errorHistory, utilizando historyRecordCount para controlar a quantidade de registros. Quando os arrays estiverem cheios, deslocaremos os valores para liberar espaço para novos registros. Também registraremos as atualizações com Print e chamaremos AdjustLearningRate para adaptar a taxa de aprendizado ao desempenho do treinamento. O treinamento será encerrado antecipadamente se trainingError e accuracy atenderem aos critérios definidos por targetError e MinPredictionAccuracy.
Desenvolveremos o método UpdateNetworkWithRecentData para atualizar a rede com dados recentes. Definiremos recentBarCount como 10 e utilizaremos CollectTrainingData para preencher recentData. Em seguida, percorreremos cada amostra, aplicando SetInput, ForwardPropagate e Backpropagate para atualizar os parâmetros da rede. O método GetRecentAccuracy retornará o valor mais recente de accuracyHistory ou 0.0 se historyRecordCount for igual a zero. Da mesma forma, GetRecentError retornará o último valor de errorHistory ou 0.0 caso não haja registros.
Criaremos o método ShouldRetrain para determinar se a rede precisa passar por um novo treinamento. Primeiro, verificaremos se historyRecordCount é maior ou igual a 2. Depois, utilizaremos GetRecentAccuracy e GetRecentError para obter recentAccuracy e recentError e comparar recentAccuracy com MinPredictionAccuracy e recentError com um limite equivalente a 1,5 vezes o erro anterior armazenado em errorHistory. Se os critérios indicarem a necessidade de retreinar a rede, o método retornará true. Com esses métodos definidos, podemos criar a instância da classe que será utilizada na implementação da estratégia.
// Global neural network instance CNeuralNetwork *neuralNetwork; //--- Global neural network object
Criaremos o ponteiro global neuralNetwork, que fará referência a uma instância da classe CNeuralNetwork. Esse ponteiro permitirá acessar os recursos da rede neural de forma centralizada, incluindo treinamento, propagação para frente, retropropagação e ajustes da taxa de aprendizado adaptativa. Assim, poderemos integrar esses mecanismos às diferentes rotinas do EA.
Ao definir neuralNetwork globalmente, facilitamos seu acesso nas funções de inicialização, processamento de ticks e desinicialização, permitindo gerenciar as funcionalidades de previsão e trading da estratégia. Para manter o código modular, também podemos criar funções específicas para preparar os dados de entrada e coletar as amostras de treinamento.
// Prepare inputs from market data void PrepareInputs(double &inputs[]) { //--- Resize inputs array if necessary if(ArraySize(inputs) != INPUT_NEURON_COUNT) ArrayResize(inputs, INPUT_NEURON_COUNT); double ma20Values[], ma50Values[], rsiValues[], atrValues[]; //--- Set arrays as series ArraySetAsSeries(ma20Values, true); ArraySetAsSeries(ma50Values, true); ArraySetAsSeries(rsiValues, true); ArraySetAsSeries(atrValues, true); //--- Copy MA20 buffer if(CopyBuffer(ma20IndicatorHandle, 0, 0, 2, ma20Values) <= 0) { Print("Error: Failed to copy MA20 buffer. Error code: ", GetLastError()); return; } //--- Copy MA50 buffer if(CopyBuffer(ma50IndicatorHandle, 0, 0, 2, ma50Values) <= 0) { Print("Error: Failed to copy MA50 buffer. Error code: ", GetLastError()); return; } //--- Copy RSI buffer if(CopyBuffer(rsiIndicatorHandle, 0, 0, 2, rsiValues) <= 0) { Print("Error: Failed to copy RSI buffer. Error code: ", GetLastError()); return; } //--- Copy ATR buffer if(CopyBuffer(atrIndicatorHandle, 0, 0, 2, atrValues) <= 0) { Print("Error: Failed to copy ATR buffer. Error code: ", GetLastError()); return; } //--- Check array sizes if(ArraySize(ma20Values) < 2 || ArraySize(ma50Values) < 2 || ArraySize(rsiValues) < 2 || ArraySize(atrValues) < 2) { Print("Error: Insufficient data in indicator arrays"); return; } //--- Get current market prices double closePrice = iClose(_Symbol, PERIOD_CURRENT, 0); double openPrice = iOpen(_Symbol, PERIOD_CURRENT, 0); double highPrice = iHigh(_Symbol, PERIOD_CURRENT, 0); double lowPrice = iLow(_Symbol, PERIOD_CURRENT, 0); //--- Calculate input features inputs[0] = (MathAbs(openPrice) > 0.000001) ? (closePrice - openPrice) / openPrice : 0; inputs[1] = (MathAbs(lowPrice) > 0.000001) ? (highPrice - lowPrice) / lowPrice : 0; inputs[2] = (MathAbs(ma20Values[0]) > 0.000001) ? (closePrice - ma20Values[0]) / ma20Values[0] : 0; inputs[3] = (MathAbs(ma50Values[0]) > 0.000001) ? (ma20Values[0] - ma50Values[0]) / ma50Values[0] : 0; inputs[4] = rsiValues[0] / 100.0; double highLowRange = highPrice - lowPrice; if(MathAbs(highLowRange) > 0.000001) { inputs[5] = (closePrice - lowPrice) / highLowRange; inputs[7] = MathAbs(closePrice - openPrice) / highLowRange; inputs[8] = (highPrice - closePrice) / highLowRange; inputs[9] = (closePrice - lowPrice) / highLowRange; } else { inputs[5] = 0; inputs[7] = 0; inputs[8] = 0; inputs[9] = 0; } inputs[6] = (MathAbs(closePrice) > 0.000001) ? atrValues[0] / closePrice : 0; //--- Log input preparation Print("Prepared inputs. Size: ", ArraySize(inputs)); } // Collect training data bool CollectTrainingData(TrainingData &data[], int barCount) { //--- Check output neuron count if(OUTPUT_NEURON_COUNT != 2) { Print("Error: OUTPUT_NEURON_COUNT must be 2 for binary classification."); return false; } //--- Resize data array ArrayResize(data, barCount); double ma20Values[], ma50Values[], rsiValues[], atrValues[]; //--- Set arrays as series ArraySetAsSeries(ma20Values, true); ArraySetAsSeries(ma50Values, true); ArraySetAsSeries(rsiValues, true); ArraySetAsSeries(atrValues, true); //--- Copy MA20 buffer if(CopyBuffer(ma20IndicatorHandle, 0, 0, barCount + 1, ma20Values) < barCount + 1) { Print("Error: Failed to copy MA20 buffer for training. Error code: ", GetLastError()); return false; } //--- Copy MA50 buffer if(CopyBuffer(ma50IndicatorHandle, 0, 0, barCount + 1, ma50Values) < barCount + 1) { Print("Error: Failed to copy MA50 buffer for training. Error code: ", GetLastError()); return false; } //--- Copy RSI buffer if(CopyBuffer(rsiIndicatorHandle, 0, 0, barCount + 1, rsiValues) < barCount + 1) { Print("Error: Failed to copy RSI buffer for training. Error code: ", GetLastError()); return false; } //--- Copy ATR buffer if(CopyBuffer(atrIndicatorHandle, 0, 0, barCount + 1, atrValues) < barCount + 1) { Print("Error: Failed to copy ATR buffer for training. Error code: ", GetLastError()); return false; } MqlRates priceData[]; //--- Set rates array as series ArraySetAsSeries(priceData, true); //--- Copy price data if(CopyRates(_Symbol, PERIOD_CURRENT, 0, barCount + 1, priceData) < barCount + 1) { Print("Error: Failed to copy rates for training. Error code: ", GetLastError()); return false; } //--- Process each bar for(int i = 0; i < barCount; i++) { //--- Resize input and target arrays ArrayResize(data[i].inputValues, INPUT_NEURON_COUNT); ArrayResize(data[i].targetValues, OUTPUT_NEURON_COUNT); //--- Get price data double closePrice = priceData[i].close; double openPrice = priceData[i].open; double highPrice = priceData[i].high; double lowPrice = priceData[i].low; double highLowRange = highPrice - lowPrice; //--- Calculate input features data[i].inputValues[0] = (MathAbs(openPrice) > 0.000001) ? (closePrice - openPrice) / openPrice : 0; data[i].inputValues[1] = (MathAbs(lowPrice) > 0.000001) ? (highPrice - lowPrice) / lowPrice : 0; data[i].inputValues[2] = (MathAbs(ma20Values[i]) > 0.000001) ? (closePrice - ma20Values[i]) / ma20Values[i] : 0; data[i].inputValues[3] = (MathAbs(ma50Values[i]) > 0.000001) ? (ma20Values[i] - ma50Values[i]) / ma50Values[i] : 0; data[i].inputValues[4] = rsiValues[i] / 100.0; if(MathAbs(highLowRange) > 0.000001) { data[i].inputValues[5] = (closePrice - lowPrice) / highLowRange; data[i].inputValues[7] = MathAbs(closePrice - openPrice) / highLowRange; data[i].inputValues[8] = (highPrice - closePrice) / highLowRange; data[i].inputValues[9] = (closePrice - lowPrice) / highLowRange; } data[i].inputValues[6] = (MathAbs(closePrice) > 0.000001) ? atrValues[i] / closePrice : 0; //--- Set target values based on price movement if(i < barCount - 1) { double futureClose = priceData[i + 1].close; double priceChange = futureClose - closePrice; if(priceChange > 0) { data[i].targetValues[0] = 1; data[i].targetValues[1] = 0; } else { data[i].targetValues[0] = 0; data[i].targetValues[1] = 1; } } else { data[i].targetValues[0] = 0; data[i].targetValues[1] = 0; } } //--- Return success return true; }
Nesta etapa, vamos implementar a preparação dos dados por meio da função PrepareInputs, responsável por gerar os atributos de entrada da rede neural. Utilizaremos ArrayResize para dimensionar o array inputs de acordo com INPUT_NEURON_COUNT. Em seguida, declararemos os arrays ma20Values, ma50Values, rsiValues e atrValues e os configuraremos como séries temporais com ArraySetAsSeries. Depois, utilizaremos CopyBuffer para obter os valores correspondentes a duas barras de cada indicador, por meio de ma20IndicatorHandle, ma50IndicatorHandle, rsiIndicatorHandle e atrIndicatorHandle. Se alguma dessas chamadas falhar, registraremos o erro com Print e encerraremos a execução da função.
Verificaremos também o tamanho dos arrays com ArraySize e registraremos eventuais erros com Print caso não haja dados suficientes. Em seguida, utilizaremos iClose, iOpen, iHigh e iLow para obter os preços da barra atual em closePrice, openPrice, highPrice e lowPrice, considerando _Symbol e PERIOD_CURRENT. Com esses valores, calcularemos os atributos de entrada em inputs, incluindo diferenças normalizadas entre preços, desvios em relação às médias móveis, o RSI convertido para a escala de 0 a 1 e o ATR normalizado pelo preço de fechamento armazenado em closePrice. Utilizaremos verificações com MathAbs para evitar divisões por zero e calcularemos também os atributos derivados do intervalo entre máxima e mínima representado por highLowRange. Por fim, registraremos a conclusão da preparação dos dados com Print, incluindo o tamanho do array obtido por ArraySize.
Criaremos a função CollectTrainingData para preparar as amostras de treinamento em um array de estruturas TrainingData, considerando a quantidade de barras definida por barCount. Primeiro, verificaremos se OUTPUT_NEURON_COUNT é igual a 2 e utilizaremos ArrayResize para dimensionar o array data. Em seguida, chamaremos CopyBuffer para obter os valores dos indicadores por meio de seus respectivos handles e CopyRates para carregar os dados de preços em priceData. Se alguma dessas operações falhar, registraremos o erro com Print. Para cada barra, utilizaremos ArrayResize para dimensionar data[i].inputValues e data[i].targetValues. Calcularemos os atributos de entrada seguindo a mesma lógica de PrepareInputs e definiremos os valores esperados em data[i].targetValues com base na variação dos preços registrada em priceData. Se todas as etapas forem concluídas corretamente, retornaremos true. Com as amostras preparadas, poderemos iniciar o treinamento da rede neural.
// Train the neural network bool TrainNetwork() { //--- Log training start Print("Starting neural network training..."); TrainingData trainingData[]; //--- Collect training data if(!CollectTrainingData(trainingData, TrainingBarCount)) { Print("Failed to collect training data"); return false; } //--- Train network double accuracy = neuralNetwork.TrainOnHistoricalData(trainingData); //--- Log training completion Print("Training completed. Final accuracy: ", accuracy); //--- Return training success return (accuracy >= MinPredictionAccuracy); }
Criaremos a função TrainNetwork e utilizaremos Print para registrar o início do treinamento. Em seguida, declararemos o array trainingData, composto por estruturas TrainingData, para armazenar os atributos de entrada e os valores esperados. Chamaremos CollectTrainingData para preencher trainingData com a quantidade de barras definida por TrainingBarCount. Se a coleta falhar, registraremos o erro com Print e retornaremos false.
Depois, utilizaremos o método TrainOnHistoricalData do objeto apontado por neuralNetwork para treinar a rede, armazenando a precisão obtida na variável accuracy. Ao concluir o treinamento, registraremos o resultado com Print, incluindo o valor de accuracy. A função retornará true se accuracy for maior ou igual a MinPredictionAccuracy, indicando que a rede atingiu o nível mínimo de precisão exigido para operar. Por fim, podemos implementar a função responsável por verificar os sinais de trading, conforme mostrado abaixo.
// Validate Stop Loss and Take Profit levels bool CheckStopLossTakeprofit(ENUM_ORDER_TYPE orderType, double price, double stopLoss, double takeProfit) { //--- Get minimum stop level double stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point; //--- Validate buy order if(orderType == ORDER_TYPE_BUY) { //--- Check stop loss distance if(MathAbs(price - stopLoss) < stopLevel) { Print("Buy Stop Loss too close. Minimum distance: ", stopLevel); return false; } //--- Check take profit distance if(MathAbs(takeProfit - price) < stopLevel) { Print("Buy Take Profit too close. Minimum distance: ", stopLevel); return false; } } //--- Validate sell order else if(orderType == ORDER_TYPE_SELL) { //--- Check stop loss distance if(MathAbs(stopLoss - price) < stopLevel) { Print("Sell Stop Loss too close. Minimum distance: ", stopLevel); return false; } //--- Check take profit distance if(MathAbs(price - takeProfit) < stopLevel) { Print("Sell Take Profit too close. Minimum distance: ", stopLevel); return false; } } //--- Return validation success return true; }
Nesta etapa, criaremos uma função booleana para validar os níveis de Stop Loss e Take Profit, evitando possíveis erros decorrentes das restrições impostas pela corretora. Com essa verificação implementada, podemos inicializar o programa no manipulador de eventos OnInit, inicializando os indicadores e a instância da classe CNeuralNetwork, responsável pelas tarefas de maior carga computacional, conforme mostrado abaixo.
// Expert initialization function int OnInit() { //--- Initialize 20-period MA indicator ma20IndicatorHandle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_SMA, PRICE_CLOSE); //--- Initialize 50-period MA indicator ma50IndicatorHandle = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_SMA, PRICE_CLOSE); //--- Initialize RSI indicator rsiIndicatorHandle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE); //--- Initialize ATR indicator atrIndicatorHandle = iATR(_Symbol, PERIOD_CURRENT, 14); //--- Check MA20 handle if(ma20IndicatorHandle == INVALID_HANDLE) Print("Error: Failed to initialize MA20 handle. Error code: ", GetLastError()); //--- Check MA50 handle if(ma50IndicatorHandle == INVALID_HANDLE) Print("Error: Failed to initialize MA50 handle. Error code: ", GetLastError()); //--- Check RSI handle if(rsiIndicatorHandle == INVALID_HANDLE) Print("Error: Failed to initialize RSI handle. Error code: ", GetLastError()); //--- Check ATR handle if(atrIndicatorHandle == INVALID_HANDLE) Print("Error: Failed to initialize ATR handle. Error code: ", GetLastError()); //--- Check for any invalid handles if(ma20IndicatorHandle == INVALID_HANDLE || ma50IndicatorHandle == INVALID_HANDLE || rsiIndicatorHandle == INVALID_HANDLE || atrIndicatorHandle == INVALID_HANDLE) { Print("Error initializing indicators"); return INIT_FAILED; } //--- Create neural network instance neuralNetwork = new CNeuralNetwork(INPUT_NEURON_COUNT, MinHiddenNeurons, OUTPUT_NEURON_COUNT); //--- Check neural network creation if(neuralNetwork == NULL) { Print("Failed to create neural network"); return INIT_FAILED; } //--- Log initialization Print("Initializing neural network..."); //--- Return success return(INIT_SUCCEEDED); }
Vamos implementar a lógica de inicialização na função OnInit. Utilizaremos iMA para inicializar ma20IndicatorHandle e ma50IndicatorHandle, correspondentes às médias móveis simples de 20 e 50 períodos, configuradas para _Symbol e PERIOD_CURRENT com PRICE_CLOSE. Também utilizaremos iRSI e iATR para inicializar rsiIndicatorHandle e atrIndicatorHandle, ambos com período 14. Verificaremos se algum dos handles retornou INVALID_HANDLE e, em caso de falha, registraremos o erro com Print e GetLastError. Se a inicialização de algum dos indicadores falhar, retornaremos INIT_FAILED.
Em seguida, criaremos uma instância da classe CNeuralNetwork e atribuiremos a neuralNetwork o ponteiro para essa instância, utilizando INPUT_NEURON_COUNT, MinHiddenNeurons e OUTPUT_NEURON_COUNT para configurar a estrutura da rede. Se neuralNetwork for igual a NULL, registraremos o erro com Print e retornaremos INIT_FAILED. Por fim, registraremos a inicialização bem-sucedida com Print e retornaremos INIT_SUCCEEDED, indicando que o EA foi configurado corretamente para operar. Ao executar o programa, teremos o seguinte resultado.

Conseguimos inicializar o programa. Agora, como criamos uma instância da rede neural, precisamos garantir que ela seja excluída quando o programa for encerrado. A lógica necessária é apresentada abaixo.
// Expert deinitialization function void OnDeinit(const int reason) { //--- Release MA20 indicator handle if(ma20IndicatorHandle != INVALID_HANDLE) IndicatorRelease(ma20IndicatorHandle); //--- Release MA50 indicator handle if(ma50IndicatorHandle != INVALID_HANDLE) IndicatorRelease(ma50IndicatorHandle); //--- Release RSI indicator handle if(rsiIndicatorHandle != INVALID_HANDLE) IndicatorRelease(rsiIndicatorHandle); //--- Release ATR indicator handle if(atrIndicatorHandle != INVALID_HANDLE) IndicatorRelease(atrIndicatorHandle); //--- Delete neural network instance if(neuralNetwork != NULL) delete neuralNetwork; //--- Log deinitialization Print("Expert Advisor deinitialized - ", EnumToString((ENUM_INIT_RETCODE)reason)); }
Implementaremos a liberação dos recursos na função OnDeinit. Utilizaremos IndicatorRelease para liberar os recursos associados a ma20IndicatorHandle, ma50IndicatorHandle, rsiIndicatorHandle e atrIndicatorHandle, desde que cada handle seja diferente de INVALID_HANDLE. Dessa forma, garantiremos a liberação correta dos recursos dos indicadores de médias móveis, RSI e ATR. Também verificaremos se neuralNetwork é diferente de NULL e utilizaremos o operador delete para excluir a instância da classe CNeuralNetwork e liberar a memória ocupada pela rede neural. Por fim, utilizaremos EnumToString para converter o motivo da desinicialização em texto e registrá-lo com Print, permitindo acompanhar o encerramento do EA e a liberação de seus recursos. Se não excluirmos a instância da classe, ocorrerá um vazamento de memória, conforme ilustrado abaixo.

Com a prevenção do vazamento de memória implementada, podemos implementar as principais rotinas de coleta de dados, treinamento e utilização da rede neural no manipulador de eventos OnTick.
// Expert tick function void OnTick() { static datetime lastBarTime = 0; //--- Track last processed bar time //--- Get current bar time datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Skip if same bar if(lastBarTime == currentBarTime) return; //--- Update last bar time lastBarTime = currentBarTime; //--- Calculate dynamic neuron count int newNeuronCount = (int)neuralNetwork.CalculateDynamicNeurons(); //--- Resize network if necessary if(newNeuronCount != neuralNetwork.GetHiddenNeurons()) neuralNetwork.ResizeNetwork(newNeuronCount); //--- Check if retraining is needed if(TimeCurrent() - iTime(_Symbol, PERIOD_CURRENT, TrainingBarCount) >= 12 * 3600 || neuralNetwork.ShouldRetrain()) { //--- Log training start Print("Starting network training..."); //--- Train network if(!TrainNetwork()) { Print("Training failed or insufficient accuracy"); return; } } //--- Update network with recent data neuralNetwork.UpdateNetworkWithRecentData(); //--- Check for open positions if(PositionsTotal() > 0) { //--- Iterate through positions for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Skip if position is for current symbol if(PositionGetSymbol(i) == _Symbol) return; } } //--- Prepare input data double currentInputs[]; ArrayResize(currentInputs, INPUT_NEURON_COUNT); PrepareInputs(currentInputs); //--- Verify input array size if(ArraySize(currentInputs) != INPUT_NEURON_COUNT) { Print("Error: Inputs array not properly initialized. Size: ", ArraySize(currentInputs)); return; } //--- Set network inputs neuralNetwork.SetInput(currentInputs); //--- Perform forward propagation neuralNetwork.ForwardPropagate(); double outputValues[]; //--- Resize output array ArrayResize(outputValues, OUTPUT_NEURON_COUNT); //--- Get network outputs neuralNetwork.GetOutput(outputValues); //--- Verify output array size if(ArraySize(outputValues) != OUTPUT_NEURON_COUNT) { Print("Error: Outputs array not properly initialized. Size: ", ArraySize(outputValues)); return; } //--- Get market prices double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Calculate stop loss and take profit levels double buyStopLoss = NormalizeDouble(askPrice - StopLossPoints * _Point, _Digits); double buyTakeProfit = NormalizeDouble(askPrice + TakeProfitPoints * _Point, _Digits); double sellStopLoss = NormalizeDouble(bidPrice + StopLossPoints * _Point, _Digits); double sellTakeProfit = NormalizeDouble(bidPrice - TakeProfitPoints * _Point, _Digits); //--- Validate stop loss and take profit if(!CheckStopLossTakeprofit(ORDER_TYPE_BUY, askPrice, buyStopLoss, buyTakeProfit) || !CheckStopLossTakeprofit(ORDER_TYPE_SELL, bidPrice, sellStopLoss, sellTakeProfit)) { return; } // Trading logic const double CONFIDENCE_THRESHOLD = 0.8; //--- Confidence threshold for trading //--- Check for buy signal if(outputValues[0] > CONFIDENCE_THRESHOLD && outputValues[1] < (1 - CONFIDENCE_THRESHOLD)) { //--- Set trade magic number tradeObject.SetExpertMagicNumber(123456); //--- Place buy order if(tradeObject.Buy(LotSize, _Symbol, askPrice, buyStopLoss, buyTakeProfit, "Neural Buy")) { //--- Log successful buy order Print("Buy order placed - Signal Strength: ", outputValues[0]); } else { //--- Log buy order failure Print("Buy order failed. Error: ", GetLastError()); } } //--- Check for sell signal else if(outputValues[0] < (1 - CONFIDENCE_THRESHOLD) && outputValues[1] > CONFIDENCE_THRESHOLD) { //--- Set trade magic number tradeObject.SetExpertMagicNumber(123456); //--- Place sell order if(tradeObject.Sell(LotSize, _Symbol, bidPrice, sellStopLoss, sellTakeProfit, "Neural Sell")) { //--- Log successful sell order Print("Sell order placed - Signal Strength: ", outputValues[1]); } else { //--- Log sell order failure Print("Sell order failed. Error: ", GetLastError()); } } } //+------------------------------------------------------------------+
Nesta etapa, vamos implementar a lógica principal de trading da estratégia baseada em redes neurais na função OnTick. Declararemos a variável lastBarTime para acompanhar a última barra processada e utilizaremos iTime para obter o horário da barra atual em currentBarTime, considerando _Symbol e PERIOD_CURRENT. Se esse horário não tiver mudado, encerraremos a execução da função, garantindo que o processamento ocorra apenas na abertura de uma nova barra. Em seguida, atualizaremos lastBarTime e chamaremos CalculateDynamicNeurons para calcular a quantidade de neurônios em newNeuronCount. Se o valor calculado for diferente daquele retornado por GetHiddenNeurons, chamaremos ResizeNetwork para ajustar a estrutura da rede neural.
Verificaremos se é necessário retreinar a rede, comparando com 12 horas a diferença entre TimeCurrent e o horário retornado por iTime para o deslocamento definido por TrainingBarCount, ou utilizando o método ShouldRetrain. Se algum desses critérios indicar a necessidade de um novo treinamento, chamaremos TrainNetwork e encerraremos a execução da função caso o treinamento falhe. Em seguida, utilizaremos UpdateNetworkWithRecentData para atualizar a rede com os dados mais recentes. Se PositionsTotal indicar que existem posições abertas, utilizaremos PositionGetSymbol para verificar se já há uma posição em _Symbol e, nesse caso, evitaremos abrir outra operação. Depois, declararemos o array currentInputs, utilizaremos ArrayResize para dimensioná-lo de acordo com INPUT_NEURON_COUNT e chamaremos PrepareInputs para preenchê-lo. Também verificaremos seu tamanho com ArraySize e registraremos eventuais erros com Print.
Utilizaremos SetInput para carregar currentInputs na rede neural e chamaremos ForwardPropagate para gerar as previsões. Em seguida, utilizaremos ArrayResize para dimensionar outputValues de acordo com OUTPUT_NEURON_COUNT e GetOutput para recuperar os valores previstos, registrando eventuais erros com Print caso os dados sejam inválidos. Obteremos askPrice e bidPrice com SymbolInfoDouble e calcularemos buyStopLoss, buyTakeProfit, sellStopLoss e sellTakeProfit utilizando NormalizeDouble, com base em StopLossPoints, TakeProfitPoints, _Point e _Digits. Depois, validaremos esses níveis por meio da função CheckStopLossTakeprofit.
Para executar as operações, definiremos CONFIDENCE_THRESHOLD como 0.8. Se outputValues[0] ultrapassar esse limite e outputValues[1] ficar abaixo de seu complemento, utilizaremos tradeObject.Buy para abrir uma posição de compra, após configurar o número mágico com SetExpertMagicNumber. Registraremos o sucesso ou a falha da operação utilizando Print e GetLastError. Da mesma forma, utilizaremos tradeObject.Sell quando as previsões indicarem um sinal de venda, aplicando as verificações necessárias para executar as operações corretamente. Após a compilação, teremos o seguinte resultado.

Na imagem, podemos observar que, durante o treinamento da rede neural, os erros são calculados e propagados ao longo das épocas e, em seguida, a taxa de aprendizado é ajustada de acordo com sua evolução. Em seguida, a taxa de aprendizado é ajustada de acordo com a evolução desses erros. Agora, precisamos apenas testar o programa com dados históricos.
Testes e otimização dos parâmetros da taxa de aprendizado
Após realizar testes históricos detalhados, obtivemos os seguintes resultados.
Gráfico do teste histórico:

Relatório do teste histórico:

Conclusão
Desenvolvemos um programa em MQL5 que implementa uma estratégia de trading baseada em redes neurais com taxa de aprendizado adaptativa. A solução utiliza a classe CNeuralNetwork para processar indicadores de mercado e executar operações, ajustando dinamicamente a taxa de aprendizado e a quantidade de neurônios da rede para otimizar seu desempenho. Graças à estrutura modular, composta por elementos como TrainingData e pelos métodos AdjustLearningRate e TrainNetwork, o sistema oferece uma base flexível para novos aprimoramentos. Você pode expandi-lo refinando os parâmetros ou integrando outros indicadores de mercado, de acordo com suas preferências e necessidades de trading.
Aviso: Este artigo tem finalidade exclusivamente educacional. O trading envolve riscos financeiros significativos, e a volatilidade do mercado pode provocar perdas. Antes de utilizar este programa em condições reais de mercado, é essencial realizar testes históricos rigorosos e adotar uma gestão de risco cuidadosa.
Com base nos conceitos e na implementação apresentados, você pode aprimorar este sistema de trading baseado em redes neurais ou adaptar sua arquitetura para desenvolver novas estratégias, ampliando suas possibilidades no trading algorítmico. Boas operações!
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/18660
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.
Recursos do Assistente MQL5 que você precisa conhecer (Parte 72): Uso de padrões MACD e OBV com aprendizado supervisionado
Ferramentas de Trading em MQL5 (Parte 3): Construindo um Painel de Scanner Multitemporal para Negociação Estratégica
Desenvolvimento de ferramentas para análise de Price Action (Parte 30): EA CCI Zero Line
Ciência de Dados e ML (Parte 42): Previsão de Séries Temporais no Mercado Forex com ARIMA em Python
- 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