English Русский 中文 Deutsch 日本語
preview
Automatización de estrategias de trading en MQL5 (Parte 21): Mejora del trading basado en redes neuronales mediante tasas de aprendizaje adaptativas

Automatización de estrategias de trading en MQL5 (Parte 21): Mejora del trading basado en redes neuronales mediante tasas de aprendizaje adaptativas

MetaTrader 5Trading |
38 1
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En nuestro artículo anterior (Parte 20), desarrollamos la estrategia multisímbolo utilizando el Commodity Channel Index (CCI) y el Awesome Oscillator (AO), automatizando las operaciones de inversión de tendencia en múltiples pares de divisas con una generación de señales robusta y una gestión del riesgo en MetaQuotes Language 5 (MQL5). En la parte 21, pasamos a una estrategia de negociación basada en una red neuronal, que se ha mejorado con un mecanismo de tasa de aprendizaje adaptativa para optimizar la precisión de las predicciones sobre los movimientos del mercado. Trataremos los siguientes temas:

  1. Comprender la estrategia de la tasa de aprendizaje en las redes neuronales adaptativas
  2. Implementación en MetaQuotes Language 5 (MQL5)
  3. Pruebas y optimización de los ajustes de la tasa de aprendizaje
  4. Conclusión

Al final, tendrás un completo sistema de trading en MetaQuotes Language 5 (MQL5) que aprovecha las redes neuronales con ajustes dinámicos de la tasa de aprendizaje, listo para seguir perfeccionándolo. ¡Manos a la obra!


Comprender la estrategia de la tasa de aprendizaje en las redes neuronales adaptativas

En la parte 20, desarrollamos un sistema de trading multisímbolo que utiliza el Commodity Channel Index y el Awesome Oscillator, permitiendo automatizar operaciones de reversión de tendencia en múltiples pares de divisas. Ahora, en la parte 21, nos adentramos en una estrategia de negociación dinámica basada en redes neuronales, aprovechando el potencial de estas redes —modelos computacionales que imitan las neuronas interconectadas del cerebro humano— para predecir los movimientos de los precios del mercado con mayor precisión, mediante el procesamiento de diversos indicadores de mercado y la adaptación del proceso de aprendizaje a la volatilidad del mercado. Nuestro objetivo es desarrollar un sistema de negociación flexible y de alto rendimiento que aproveche las redes neuronales para analizar patrones de mercado complejos y ejecutar operaciones con una precisión optimizada mediante un mecanismo adaptativo de tasa de aprendizaje.

Las redes neuronales funcionan a través de capas de nodos, o neuronas, estructuradas en una capa de entrada que recoge los datos del mercado, capas ocultas que revelan patrones complejos y una capa de salida que genera señales de negociación, como la predicción de movimientos alcistas o bajistas de los precios. La propagación hacia adelante hace que los datos recorran estas capas, donde las neuronas aplican pesos y sesgos a las entradas, transformándolas en predicciones. Véase más abajo.

RED NEURAL CON CAPAS Y PESOS

Un componente fundamental, la función de activación, introduce no linealidad en estas transformaciones, lo que permite a la red modelar relaciones complejas; por ejemplo, la función de activación sigmoide asigna valores a un intervalo de 0 a 1, lo que la hace ideal para tareas de clasificación binaria, como las decisiones de compra o venta. La retropropagación (propagación hacia atrás) perfecciona este proceso partiendo de los errores de predicción y ajustando los pesos y los sesgos para mejorar la precisión con el tiempo. Véase más abajo.

PROPAGACIONES

Para dotar de dinamismo a la red, tenemos previsto implementar una estrategia de tasa de aprendizaje adaptativa que ajuste la rapidez con la que se actualizan los pesos durante la retropropagación: acelerando el aprendizaje cuando las predicciones coinciden con los resultados del mercado y ralentizándolo cuando se producen picos de error, con el fin de garantizar la estabilidad.

Tenemos previsto diseñar un sistema que introduzca indicadores de mercado, como medias móviles y métricas de impulso, en la capa de entrada de la red neuronal, los procese a través de las capas ocultas para detectar patrones y genere señales de negociación fiables a través de la capa de salida. Tenemos previsto utilizar la función de activación sigmoide para transformar las salidas de las neuronas, lo que garantizará predicciones fluidas e interpretables para la toma de decisiones de negociación. Hemos optado por esta función para disponer de dos opciones de salida. A continuación te mostramos un ejemplo de otras funciones que puedes utilizar.

FUNCIONES DE ACTIVACIÓN

Además, ajustaremos dinámicamente la tasa de aprendizaje en función del rendimiento del entrenamiento y adaptaremos el número de neuronas de la capa oculta para que se ajuste a la volatilidad del mercado, creando así un sistema adaptable que equilibre la complejidad y la eficiencia. Esta estrategia sienta las bases para una implementación sólida y unas pruebas exhaustivas. Utilizaremos dos medias móviles, el Relative Strength Index (RSI) y el Average True Range (ATR) como datos de entrada y, una vez que tengamos señales, abriremos posiciones. A continuación se muestra una representación de lo que pretendemos conseguir tras la implementación.

IMAGEN DEL PLAN


Implementación en MQL5

Para crear el programa en MQL5, abre el MetaEditor, ve al Navegador, busca la carpeta «Indicators», haz clic en la pestaña «Nuevo» y sigue las instrucciones para crear el archivo. Una vez creado, en el entorno de programación, comenzaremos declarando algunas variables de entrada, estructuras y clases que utilizaremos, ya que queremos aplicar un enfoque de programación orientada a objetos (POO).

//+------------------------------------------------------------------+
//|                              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
};

En este punto, sentamos las bases de nuestra estrategia de negociación basada en redes neuronales con tasas de aprendizaje adaptativas, inicializando los componentes esenciales para la ejecución de operaciones y el procesamiento de datos. Incluimos la biblioteca «Trade.mqh» y creamos la instancia «tradeObject» de la clase «CTrade» para gestionar las operaciones de trading, lo que permite ejecutar órdenes de compra y venta basadas en las predicciones de una red neuronal.

Definimos los parámetros input para configurar la estrategia, estableciendo «LotSize» para controlar el volumen de la operación, «StopLossPoints» y «TakeProfitPoints» para la gestión del riesgo, y «MinHiddenNeurons» y «MaxHiddenNeurons» para definir el rango de neuronas ocultas en la red neuronal. Además, especificamos «TrainingBarCount» para indicar el número de barras históricas utilizadas en el entrenamiento, «MinPredictionAccuracy» como umbral de precisión, y «MinLearningRate» y «MaxLearningRate» para limitar la tasa de aprendizaje adaptativa. También proporcionamos «InputToHiddenWeights», «HiddenToOutputWeights», «HiddenBiases» y «OutputBiases» como cadenas de entrada para inicializar los pesos y sesgos de la red neuronal, lo que permite configuraciones preestablecidas o por defecto.

A continuación, establecemos las constantes para la estructura de la red neuronal, definiendo «INPUT_NEURON_COUNT» como 10 para las entradas de datos de mercado, «OUTPUT_NEURON_COUNT» como 2 para las salidas de señales de compra/venta y «MAX_HISTORY_SIZE» como 10 para realizar un seguimiento de la precisión y los errores del entrenamiento. Creamos los identificadores de indicadores «ma20IndicatorHandle», «ma50IndicatorHandle», «rsiIndicatorHandle» y «atrIndicatorHandle» para hacer referencia a las medias móviles de 20 y 50 períodos, al RSI y al Average True Range, respectivamente, con el fin de introducir los datos de mercado en la red neuronal. Por último, definimos la estructura «TrainingData» con las matrices «inputValues» y «targetValues» para almacenar las características de entrada y los resultados esperados para el entrenamiento, lo que garantiza una gestión organizada de los datos para el proceso de aprendizaje de la red neuronal. Al introducir los valores, almacenaremos los datos tal y como se muestra a continuación.

ESTRUCTURA DE DATOS A PARTIR DE LOS DATOS DE ENTRADA

A continuación, tendremos que definir una clase que contenga la mayoría de las variables miembro básicas que vamos a utilizar con frecuencia.

// 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
};

Aquí implementamos la estructura básica de nuestra red neuronal creando la clase «CNeuralNetwork». Definimos variables de miembro privadas para gestionar la arquitectura de la red y el proceso de entrenamiento, empezando por «inputNeuronCount», «hiddenNeuronCount» y «outputNeuronCount» para establecer el número de neuronas en las capas de entrada, oculta y de salida, respectivamente, de acuerdo con el diseño de la estrategia para procesar datos de mercado y generar señales de negociación.

Creamos matrices para almacenar los valores de las capas, entre ellas «inputLayer» para los datos de entrada de los indicadores de mercado, «hiddenLayer» para el procesamiento de los patrones intermedios y «outputLayer» para generar predicciones de compra o venta. Para gestionar los cálculos de la red neuronal, creamos las matrices «inputToHiddenWeights» y «hiddenToOutputWeights» para las conexiones de pesos entre capas, y «hiddenLayerBiases» y «outputLayerBiases» para los ajustes de sesgo en las capas ocultas y de salida. Para la retropropagación, definimos «outputDeltas» y «hiddenDeltas» para almacenar los gradientes de error, lo que permite actualizar los pesos y el sesgo durante el entrenamiento.

Además, incluimos «trainingError» para realizar un seguimiento del error actual, «currentLearningRate» para gestionar la tasa de aprendizaje adaptativa, las matrices «accuracyHistory» y «errorHistory» para supervisar el rendimiento del entrenamiento a lo largo del tiempo, y «historyRecordCount» para contar las entradas registradas, lo que garantiza que la red pueda ajustar dinámicamente su proceso de aprendizaje en función de las tendencias de rendimiento. Esta clase constituye la base para implementar la propagación hacia adelante, la retropropagación y los ajustes adaptativos de la tasa de aprendizaje en las funciones posteriores. Dentro del modificador de acceso privado, podemos definir un método para procesar cadenas de entrada y convertirlas en un array para su uso.

// 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;
}

Implementamos la función «ParseStringToArray» en la clase «CNeuralNetwork» para procesar cadenas separadas por comas que contienen los pesos y sesgos de una red neuronal. Comprobamos si «inputString» está vacío, devolviendo «false» si no es válido, y utilizamos StringSplit con una coma como separador para dividirlo en «values». Si la división falla o el valor de «count» no coincide con «expectedSize», registramos los errores con Print y devolvemos «false». Cambiamos el tamaño de «output» con ArrayResize, convertimos «values» a valores de tipo doble utilizando StringToDouble, los normalizamos entre -1,0 y 1,0 con MathMax y MathMin, y devolvemos «true» si el análisis se ha realizado correctamente. El resto de las funciones auxiliares se pueden declarar con el modificador de acceso «public», tal y como se muestra a continuación, y luego las podemos definir más adelante. 

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
   }

Aquí definimos la interfaz pública de la clase «CNeuralNetwork». Creamos el constructor «CNeuralNetwork» con «inputs», «hidden» y «outputs» para configurar la red, y «InitializeWeights» para configurar los pesos y los sesgos. Implementamos «Sigmoid» para la activación, «ForwardPropagate» y «Backpropagate» para el procesamiento y la actualización en función de los «targets», y «SetInput» y «GetOutput» para gestionar las «inputs» y las «outputs».

Desarrollamos «TrainOnHistoricalData» utilizando «TrainingData», «UpdateNetworkWithRecentData» para los datos recientes, «InitializeTraining» para las matrices, «ResizeNetwork» para «newHiddenNeurons», «AdjustLearningRate» para el aprendizaje dinámico, y «GetRecentAccuracy», «GetRecentError», «ShouldRetrain», «CalculateDynamicNeurons» y «GetHiddenNeurons» para supervisar y adaptar «hiddenNeuronCount». Ahora podemos comenzar con la implementación y la definición de la función tal y como se indica a continuación.

// 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();
}

Aquí implementamos los componentes fundamentales de la red neuronal. Creamos el constructor «CNeuralNetwork», que toma como parámetros «inputs», «hidden» y «outputs» para establecer «inputNeuronCount», «outputNeuronCount» y «hiddenNeuronCount», limitando este último valor entre «MinHiddenNeurons» y «MaxHiddenNeurons». En realidad, no hace falta dedicar mucho tiempo a explicar el prototipo de clase, ya que ya habíamos hecho algo similar en la parte anterior.

A continuación, inicializamos «currentLearningRate» con el valor de «MinLearningRate», redimensionamos matrices como «inputLayer», «hiddenLayer», «outputLayer», «inputToHiddenWeights», «hiddenToOutputWeights», «hiddenLayerBiases», «outputLayerBiases», «accuracyHistory» y «errorHistory» utilizando ArrayResize, y llamamos a «InitializeWeights» y «InitializeTraining» para configurar la red.

Desarrollamos la función «InitializeTraining» para redimensionar las matrices «outputDeltas» y «hiddenDeltas» con vistas a la retropropagación, garantizando así un almacenamiento adecuado del gradiente de error. La función «InitializeWeights» configura los pesos y los sesgos, utilizando «ParseStringToArray» para cargar «inputToHiddenWeights», «hiddenToOutputWeights», «hiddenLayerBiases» y «outputLayerBiases» a partir de entradas como «InputToHiddenWeights», si se proporcionan; de lo contrario, los aleatoriza con MathRand entre -1 y 1, registrando las acciones con «Print». Implementamos la función «Sigmoid» para calcular el valor de activación sigmoide de un «x» dado utilizando la función MathExp.

La función «SetInput» copia «inputs» a «inputLayer» tras comprobar el tamaño con ArraySize y, en caso de discrepancia, registra los errores mediante «Print». Creamos la función «ForwardPropagate» para calcular los valores de «hiddenLayer» y «outputLayer» aplicando sumas ponderadas y la función de activación «Sigmoid» con «hiddenLayerBiases» y «outputLayerBiases». La función «GetOutput» redimensiona las «outputs» con «ArrayResize» y copia los valores de «outputLayer» utilizando ArrayCopy.

Implementamos la función «Backpropagate» para calcular «outputDeltas» y «hiddenDeltas» a partir de «targets», actualizando «hiddenToOutputWeights», «inputToHiddenWeights», «hiddenLayerBiases» y «outputLayerBiases» utilizando «currentLearningRate». Por último, la función «ResizeNetwork» ajusta «hiddenNeuronCount» dentro de unos límites, redimensiona matrices como «hiddenLayer» y «hiddenDeltas», y reinicializa los pesos con «InitializeWeights», registrando los cambios con «Print». A continuación, tenemos que ajustar la tasa de aprendizaje en función de la tendencia del error, así que analicémoslo con más detalle.

// 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);
}

Implementamos el mecanismo de tasa de aprendizaje adaptativa creando la función «AdjustLearningRate» dentro de la clase «CNeuralNetwork». Comprobamos si «historyRecordCount» es al menos 2 para asegurarnos de que hay datos suficientes en el array «errorHistory»; si no es así, salimos de la función para evitar ajustes no válidos. Recuperamos el último error como «lastError» y el error anterior como «prevError» de «errorHistory»; a continuación, calculamos su diferencia en «errorDiff» para evaluar el rendimiento del entrenamiento.

Utilizamos la función MathMin para aumentar «currentLearningRate» en un 5 % si «lastError» es menor que «prevError», limitándolo a «MaxLearningRate», o utilizamos la función MathMax para reducirla un 10 % si «lastError» supera a «prevError» en un 20 %, asegurándonos de que se mantenga por encima de «MinLearningRate». De lo contrario, reducimos ligeramente «currentLearningRate» en un 1 % utilizando «MathMax». Por último, utilizamos la función «Print» para registrar los valores ajustados de «currentLearningRate», «lastError», «prevError» y «errorDiff», lo que permite una optimización dinámica del proceso de aprendizaje de la red neuronal. A continuación, utilizamos el indicador ATR para calcular dinámicamente el número de neuronas ocultas, de modo que podamos ajustar el número de neuronas ocultas en función de la volatilidad.

// 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 ajustar dinámicamente el número de neuronas ocultas en función de la volatilidad del mercado, declaramos la matriz «atrValues» y utilizamos la función ArraySetAsSeries para configurarla como una matriz de series temporales; a continuación, empleamos la función CopyBuffer para recuperar 10 barras de datos del rango verdadero medio (Average True Range) desde «atrIndicatorHandle» y almacenarlas en «atrValues». Si «CopyBuffer» recupera menos de 10 valores, utilizamos la función «Print» para registrar un error y devolvemos «hiddenNeuronCount» como valor por defecto. Es imprescindible que configuremos el array como una serie temporal, de modo que los datos más recientes recuperados se puedan asignar a los índices iniciales y se utilicen como los primeros. Aquí tienes una ilustración.

SERIE TEMPORAL DE DATOS DE BARRAS

Calculamos el ATR medio sumando los valores de «atrValues» de las últimas 10 barras y dividiendo el resultado entre 10, y almacenamos el resultado en «avgATR». Utilizamos la función iClose para obtener el precio de cierre actual en «closePrice» para el _Symbol y «PERIOD_CURRENT». Si «closePrice» es cercano a cero, utilizamos la función «Print» para registrar un error y devolvemos «hiddenNeuronCount». Calculamos el ratio ATR dividiendo «atrValues[0]» entre «closePrice»; a continuación, calculamos «newNeurons» escalando el rango comprendido entre «MinHiddenNeurons» y «MaxHiddenNeurons» mediante la función MathMin aplicada al «atrRatio» escalado. Por último, utilizamos las funciones MathMax y «MathMin» para limitar y devolver el recuento de neuronas ajustado, lo que garantiza una adaptación dinámica a las condiciones del mercado. Ahora podemos definir el resto de las funciones de entrenamiento y actualización tal y como se indica a continuación.

// 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);
}

Aquí implementamos los mecanismos clave de entrenamiento y actualización de la red neuronal. Creamos la función «TrainOnHistoricalData» para entrenar la red utilizando la matriz de estructuras «TrainingData», estableciendo «maxEpochs» en 100 y «targetError» en 0,01, y restableciendo «currentLearningRate» a «MinLearningRate». En cada época, recorremos «data», comprobamos que el tamaño de «data[i].targetValues» coincida con «outputNeuronCount» y utilizamos la función «SetInput» para cargar «data[i].inputValues», tras lo cual aplicamos la función «ForwardPropagate» para calcular las predicciones. Calculamos el error utilizando MathPow para comparar «outputLayer» con «data[i].targetValues», realizamos el seguimiento de «correctPredictions» y aplicamos la función «Backpropagate» con «data[i].targetValues».

Actualizamos «accuracy» y «trainingError», los almacenamos en «accuracyHistory» y «errorHistory» utilizando «historyRecordCount», desplazamos los arrays si están llenos, registramos las actualizaciones con la función «Print» y utilizamos la función «AdjustLearningRate» para optimizar el aprendizaje, deteniéndonos antes de tiempo si «trainingError» y «accuracy» alcanzan «targetError» y «MinPredictionAccuracy».

Desarrollamos la función «UpdateNetworkWithRecentData» para refinar la red con «recentBarCount» establecido en 10, utilizando la función «CollectTrainingData» para rellenar «recentData» e iterando para aplicar «SetInput», «ForwardPropagate» y «Backpropagate» a cada muestra. La función «GetRecentAccuracy» devuelve el último valor de «accuracyHistory» o 0,0 si «historyRecordCount» es cero, y la función «GetRecentError» hace lo mismo con «errorHistory».

Creamos la función «ShouldRetrain» para comprobar si «historyRecordCount» es al menos 2, utilizando «GetRecentAccuracy» y «GetRecentError» para comparar «recentAccuracy» y «recentError» con «MinPredictionAccuracy» y 1,5 veces el valor anterior de «errorHistory», y devolviendo «true» si es necesario volver a entrenar el modelo. Ahora podemos crear una instancia de la clase que utilizaremos para la implementación propiamente dicha.

// Global neural network instance
CNeuralNetwork *neuralNetwork; //--- Global neural network object

Definimos el ámbito global de la red neuronal creando el puntero «neuralNetwork» a una instancia de la clase «CNeuralNetwork». Este objeto global permitirá un acceso centralizado a las funciones de la red neuronal, incluyendo el entrenamiento, la propagación hacia adelante, la retropropagación y los ajustes adaptativos de la tasa de aprendizaje, lo que garantizará una integración perfecta en todas las operaciones del Asesor Experto.

Al definir «neuralNetwork» de forma global, facilitamos su uso en las funciones de inicialización, procesamiento de ticks y desinicialización para gestionar las capacidades predictivas y de negociación de la estrategia. Podemos definir funciones para inicializar las entradas y recopilar datos de entrenamiento con el fin de lograr la modularidad.

// 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;
}

En este punto, llevamos a cabo la preparación de datos creando la función «PrepareInputs» para generar características de entrada para la red neuronal. Utilizamos la función ArrayResize para asegurarnos de que el array «inputs» coincida con «INPUT_NEURON_COUNT»; a continuación, declaramos los arrays «ma20Values», «ma50Values», «rsiValues» y «atrValues», y los configuramos como series temporales mediante la función ArraySetAsSeries. Utilizamos la función CopyBuffer para recuperar dos barras de datos de «ma20IndicatorHandle», «ma50IndicatorHandle», «rsiIndicatorHandle» y «atrIndicatorHandle», registrando los errores con la función «Print» y saliendo del programa si se produce algún fallo.

Verificamos los tamaños de los arrays con ArraySize, registrando los errores con «Print» si son insuficientes, y utilizamos iClose, «iOpen», «iHigh» y «iLow» para obtener los precios actuales y almacenarlos en «closePrice», «openPrice», «highPrice» y «lowPrice» para _Symbol y PERIOD_CURRENT. Calculamos las características de entrada para «inputs», entre las que se incluyen las diferencias de precio normalizadas, las desviaciones de la media móvil, el RSI escalado a un intervalo de 0 a 1 y el ATR en relación con «closePrice», gestionando la división por cero mediante comprobaciones con MathAbs, y calculamos características basadas en el rango para «highLowRange». Registramos el resultado en «Print» y «ArraySize».

Además, creamos la función «CollectTrainingData» para preparar los datos de entrenamiento en la matriz de estructuras «TrainingData» para «barCount» barras. Comprobamos que «OUTPUT_NEURON_COUNT» sea 2, utilizamos «ArrayResize» para ajustar el tamaño de «data», y utilizamos «CopyBuffer» para obtener datos de los identificadores de los indicadores y «CopyRates» para los datos de precios en «priceData», registrando los errores con «Print» si se produce algún fallo. Para cada barra, utilizamos «ArrayResize» para establecer «data[i].inputValues» y «data[i].targetValues», calculamos las características de entrada de forma similar a «PrepareInputs» y fijamos los valores objetivo en «data[i].targetValues» en función de la evolución del precio en «priceData», devolviendo «true» si la operación se realiza con éxito. A continuación, debemos entrenar la red a partir de los datos recibidos.

// 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);
}

Creamos la función «TrainNetwork» y utilizamos la función «Print» para registrar el inicio del entrenamiento; a continuación, declaramos la matriz «trainingData» de la estructura «TrainingData» para almacenar los valores de entrada y los valores objetivo. Llamamos a la función «CollectTrainingData» para rellenar «trainingData» con «TrainingBarCount» barras, registrando un error con «Print» y devolviendo «false» si falla la recopilación de datos.

Utilizamos la función «TrainOnHistoricalData» del objeto «neuralNetwork» para entrenar la red, almacenamos el resultado en «accuracy», registramos la finalización en el registro con «Print» incluyendo «accuracy» y devolvemos «true» si «accuracy» alcanza o supera «MinPredictionAccuracy», lo que garantiza que la red esté adecuadamente entrenada para la negociación. Por último, podemos crear una función para validar las señales de trading, tal y como se muestra a continuación.

// 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;
}

Aquí simplemente creamos una función booleana para validar los puntos de negociación y evitar posibles errores relacionados con las restricciones del bróker. Ahora podemos inicializar el programa en el controlador de eventos OnInit, inicializando técnicamente los indicadores y las instancias de la clase neuronal para que se encarguen del trabajo pesado, tal y como se muestra a continuación.

// 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);
}

Implementamos la lógica de inicialización dentro de la función OnInit. Utilizamos la función iMA para inicializar «ma20IndicatorHandle» y «ma50IndicatorHandle» para las medias móviles simples de 20 y 50 períodos sobre «_Symbol» y PERIOD_CURRENT con «PRICE_CLOSE», y las funciones «iRSI» e iATR para configurar «rsiIndicatorHandle» y «atrIndicatorHandle» para los indicadores RSI y ATR de 14 períodos. Comprobamos cada identificador con «INVALID_HANDLE», registrando los errores mediante la función «Print» y «GetLastError» si alguno falla, y devolvemos INIT_FAILED si falla la inicialización de algún indicador.

Creamos una nueva instancia de la clase «CNeuralNetwork» para «neuralNetwork» utilizando «INPUT_NEURON_COUNT», «MinHiddenNeurons» y «OUTPUT_NEURON_COUNT» para configurar la estructura de la red. Si «neuralNetwork» es «NULL», utilizamos la función «Print» para registrar el error y devolvemos «INIT_FAILED». Por último, registramos que la inicialización se ha realizado correctamente con «Print» y devolvemos INIT_SUCCEEDED, lo que garantiza que el asesor experto esté correctamente configurado para operar. Al ejecutar el programa, obtenemos el siguiente resultado.

INICIALIZACIÓN

En la imagen podemos ver que inicializamos el programa correctamente. Ahora que hemos creado una instancia de la red neuronal, debemos eliminarla una vez que retiremos el programa del gráfico. Esta es la lógica que utilizamos para lograrlo.

// 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));
}

Llevamos a cabo el proceso de limpieza dentro de la función OnDeinit, donde utilizamos la función IndicatorRelease para liberar los recursos de «ma20IndicatorHandle», «ma50IndicatorHandle», «rsiIndicatorHandle», y «atrIndicatorHandle», siempre que ninguno de ellos sea «INVALID_HANDLE», garantizando así la liberación correcta de los identificadores de los indicadores de media móvil, RSI y ATR. Comprobamos que «neuralNetwork» no sea «NULL» y utilizamos el operador «delete» para desasignar la instancia de la clase «CNeuralNetwork», liberando así la red neuronal. Por último, utilizamos la función «Print» junto con EnumToString para registrar el motivo de la desinicialización, lo que confirma que los recursos del asesor experto se liberan correctamente. Si no se elimina la instancia de la clase, se producirán fugas de memoria, tal y como se muestra a continuación.

FUGA DE MEMORIA

Una vez solucionado el problema de la fuga de memoria, podemos implementar la lógica principal de adquisición de datos, entrenamiento y uso del modelo dentro del controlador 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());
      }
   }
}
//+------------------------------------------------------------------+

Aquí implementamos la lógica de negociación principal de la estrategia de red neuronal dentro de la función OnTick. Declaramos «lastBarTime» para realizar un seguimiento de la última barra procesada y utilizamos la función iTime para obtener «currentBarTime» para «_Symbol» y PERIOD_CURRENT, saliendo del bucle si no ha cambiado, con el fin de procesar únicamente las nuevas barras. Actualizamos «lastBarTime» y utilizamos la función «CalculateDynamicNeurons» para calcular «newNeuronCount»; si este valor difiere del resultado de la función «GetHiddenNeurons», llamamos a la función «ResizeNetwork» para ajustar la red.

Comprobamos si es necesario volver a entrenar la red comparando TimeCurrent menos «iTime» por «TrainingBarCount» con 12 horas, o bien utilizando la función «ShouldRetrain»; a continuación, utilizamos la función «TrainNetwork» para volver a entrenar la red, y salimos si falla. Llamamos a la función «UpdateNetworkWithRecentData» para refinar la red. Si PositionsTotal indica que hay posiciones abiertas, utilizamos la función «PositionGetSymbol» para omitir la operación si alguna de ellas corresponde a _Symbol. Declaramos «currentInputs», le cambiamos el tamaño con «ArrayResize» a «INPUT_NEURON_COUNT» y utilizamos la función «PrepareInputs» para rellenarlo, verificando el tamaño con ArraySize y registrando los errores con «Print».

Utilizamos la función «SetInput» para cargar «currentInputs», llamamos a la función «ForwardPropagate» para generar predicciones y utilizamos la función «GetOutput» para recuperar «outputValues» tras redimensionar con «ArrayResize» a «OUTPUT_NEURON_COUNT», registrando los errores con «Print» si no son válidos. Obtenemos «askPrice» y «bidPrice» con SymbolInfoDouble, calculamos «buyStopLoss», «buyTakeProfit», «sellStopLoss» y «sellTakeProfit» utilizando NormalizeDouble con «StopLossPoints», «TakeProfitPoints», «_Point» y «_Digits», y los validamos con la función «CheckStopLossTakeprofit».

Para la negociación, establecemos «CONFIDENCE_THRESHOLD» en 0,8; si «outputValues[0]» lo supera y «outputValues[1]» está por debajo de su complemento, utilizamos la función «tradeObject.Buy» tras establecer el número mágico con «SetExpertMagicNumber», y registramos el éxito o el fracaso con «Print» y «GetLastError». Del mismo modo, utilizamos la función «tradeObject.Sell» para las señales de venta, lo que garantiza una ejecución sólida de las operaciones. Tras la compilación, obtenemos el siguiente resultado.

RESULTADO FINAL

En la imagen podemos ver que entrenamos la red neuronal para obtener errores, los propagamos a lo largo de las épocas y, a continuación, ajustamos la tasa de aprendizaje en función de la precisión de los errores. Lo que queda por hacer es realizar un backtesting del programa, y eso se trata en la siguiente sección.


Pruebas y optimización de los ajustes de la tasa de aprendizaje

Tras realizar un exhaustivo backtesting, hemos obtenido los siguientes resultados.

Gráfico de backtest:

GRÁFICO

Informe de backtesting:

INFORME


Conclusión

En conclusión, hemos desarrollado un programa MQL5 que implementa una estrategia de trading basada en una red neuronal con tasas de aprendizaje adaptativas, aprovechando la clase «CNeuralNetwork» para procesar los indicadores de mercado y ejecutar operaciones con ajustes dinámicos de la velocidad de aprendizaje y el tamaño de la red, con el fin de obtener un rendimiento óptimo. Gracias a componentes modulares como la estructura «TrainingData» y funciones como «AdjustLearningRate» y «TrainNetwork», este sistema ofrece un marco flexible que puedes mejorar ajustando los parámetros o integrando indicadores de mercado adicionales para adaptarlo a tus preferencias de negociación.

Descargo de responsabilidad: Este artículo tiene fines exclusivamente educativos. Operar en los mercados financieros conlleva riesgos financieros significativos, y la volatilidad del mercado puede ocasionar pérdidas. Es fundamental realizar pruebas retrospectivas exhaustivas y una gestión de riesgos cuidadosa antes de implementar este programa en mercados reales.

Aprovechando los conceptos y la implementación presentados, podrá mejorar este sistema de negociación basado en redes neuronales o adaptar su arquitectura para crear nuevas estrategias, impulsando así su trayectoria en el trading algorítmico. ¡Buen trading!

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18660

murugan
murugan | 28 mar 2026 en 13:36
¿Podríamos crear ideas de trading basadas en redes neuronales que integren modelos ICT o CRT? Por ejemplo, utilizando configuraciones de tipo SMC con estructuras fractales en los marcos temporales superior, medio e inferior, ¿podríamos aplicar estas ideas para mejorar una red neuronal?
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Aprendizaje automático y Data Science (Parte 45): Predicción de series temporales en Forex con el modelo Prophet de Facebook Aprendizaje automático y Data Science (Parte 45): Predicción de series temporales en Forex con el modelo Prophet de Facebook
El modelo «Prophet», desarrollado por Facebook, es una herramienta robusta de previsión de series temporales diseñada para captar tendencias, la estacionalidad y los efectos de días festivos con un ajuste manual mínimo. Se ha generalizado su uso para la previsión de la demanda y la planificación empresarial. En este artículo, analizamos la eficacia de Prophet a la hora de predecir la volatilidad de los instrumentos del mercado forex, y mostramos cómo puede aplicarse más allá de los casos de uso empresariales tradicionales.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 29): Boom and Crash Interceptor EA Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 29): Boom and Crash Interceptor EA
Descubre cómo el EA «Boom & Crash Interceptor» transforma tus gráficos en un sistema de alertas proactivo, capaz de detectar movimientos explosivos mediante análisis ultrarrápidos, detección de picos de volatilidad, confirmación de tendencias y filtros de zonas de pivote. Con las flechas verdes «Boom» y rojas «Crash», que te guían en cada decisión, esta herramienta elimina el ruido y te permite sacar partido de los picos del mercado como nunca antes. Descúbrelo y comprueba cómo funciona y por qué puede convertirse en tu próxima ventaja imprescindible.