Русский Español Português
preview
Neural Networks in Trading: Generalizing Time Series Without Data-Specific Dependence (Conclusion)

Neural Networks in Trading: Generalizing Time Series Without Data-Specific Dependence (Conclusion)

MetaTrader 5Trading systems |
338 1
Dmitriy Gizlyk
Dmitriy Gizlyk

Introduction

Financial markets are a living organism. Their rhythm is set by millions of trades, hundreds of economic reports, and a constant stream of news. In such an environment, the ones who profit are those who can not only react quickly but also anticipate when trends will shift. That is exactly why Mamba4Cast was developed — a time series forecasting framework inspired by the latest developments in neural network architecture and tailored to the specifics of high-frequency sequences.

We have reached the final phase of our introduction to this framework. First, we examined the theoretical framework and the general approach to feature processing. In the second part, we delved deeper into the mechanics. Now let's bring everything together to show that the model not only exists on paper but also works in a real-world market environment.

The framework itself is structured as a chain of modules, each of which performs a specific function. The first block is responsible for feature extraction. Here, the model processes the raw data: opening price, closing price, high/low, and volumes. They all pass through a compact layer that identifies local patterns. You could say it is like a trader's trained eye, spotting patterns amid the chaos of the chart.

Next come the convolutional layers. These blocks act as market filters, extracting stable signals and filtering out noise. Volatility spikes, fading trends, and emerging consolidation phases — all of these are detected and processed. In this case, a multi-window architecture is used, where each convolution targets different horizons. Thus, the framework learns to capture both near-term fluctuations and longer oscillatory cycles simultaneously.

The key module — SSM (State Space Model) — provides long-term memory capabilities. This is especially important in financial data, where patterns often do not emerge immediately, but over horizons of dozens of candlesticks. For example, a series of false breakouts may culminate in a powerful impulse move — and the model must be prepared for such a scenario. It is precisely the SSM that allows the model to retain context and maintain well-founded forecasting even amid market uncertainty.

What makes Mamba4Cast particularly valuable is its forecasting mechanism over the entire planning horizon. This aligns with the real-world challenges traders face. This approach can be compared to the way a driver behaves: keeping an eye on the road immediately ahead while also looking far ahead and reading the flow of traffic. This hybrid mode produces a more stable and comprehensive behavior policy.

In this article, you will see the final form of the model: its architecture, training processes, and real-world results. We will show how theory and practice come together, and how an abstract model is transformed into a practical tool for market analysis.



Model Architecture

Today, we will start by building the architecture of a trainable model — a full-fledged trading Agent capable of making decisions and executing trades in real time. Just like a trader who carefully analyzes the current market situation, assesses price behavior, volume, and market sentiment — and only then decides whether to enter a position — our Agent must also be able to see and understand the market, rather than blindly following signals. That is why we do not limit ourselves to predicting a single next price — our goal is more ambitious: to create a model that can recognize patterns in market behavior, respond to rapidly changing conditions, and adapt to different phases of the market cycle.

In this context, the Mamba4Cast framework is implemented as one of the key components of the overall system — the Environment State Encoder. This is where the model’s market perception begins to take shape, transforming a set of numbers into a meaningful picture of what is happening. The Encoder will serve as a kind of trading eye for the Agent, trained to recognize significant movements, hidden patterns, and potential entry points long before they are confirmed on the chart.

In this article, we continue to follow the Actor-Director-Critic learning framework. The system we are training includes four key models, each of which is responsible for a specific aspect of trading decision-making:

  • Environment State Encoder — the agent’s eyes — generates market state embeddings;
  • Actor (Actor) — a model that proposes specific trading actions based on the received embedding;
  • Director (Director) — a model that classifies the actions proposed by the Actor as good or bad, guiding the learning process and preventing erroneous decisions;
  • Critic (Critic) — evaluates the value of the Actor's actions in the context of the market state and generates a feedback signal for strategy optimization.

The architecture of all models is defined using the CreateDescriptions method, whose parameters receive four pointers to dynamic arrays. It is into these arrays that the layer descriptions and parameters for each of the listed models — from the Encoder to the Critic — are written sequentially, allowing flexible control over the structure and making it easy to adapt the framework to new requirements.

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&actor,
                        CArrayObj *&director,
                        CArrayObj *&critic
                       )
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!director)
     {
      director = new CArrayObj();
      if(!director)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }

In the body of the CreateDescriptions method, the validity of the received pointers to the four dynamic arrays is checked first. If necessary, new objects are created, ensuring that the architecture description can be written correctly later without the risk of memory conflicts.

Next, we move on to describing the architecture of the Environment State Encoder. A sufficiently large fully connected layer is used to receive the input data. We feed raw data into it — without any preprocessing — directly from the trading terminal: open/close prices, high/low prices, volumes, and technical indicator readings.

Since these data have different statistical characteristics and scales, their distributions need to be aligned to stabilize the model training process. This is where the batch normalization layer comes in. It transforms the input vectors so that each feature has a mean close to zero and a variance close to 1, which promotes faster convergence and improves training stability. Instead of the standard implementation, we use a modified version: a normalization layer with added noise. This approach helps improve the model's generalization ability by artificially increasing the diversity of the training data.

At the output of this stack, the Encoder receives standardized features that are ready for further processing.
//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- Layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Next, we generate time-step embeddings using the CMamba4CastEmbedding module. It is here that the feature vectors are enriched with harmonics of two key time intervals — H1 (hourly) and D1 (daily). By adding sinusoidal and cosine components, the model receives information about typical hourly fluctuations and recurring daily rhythms. This allows the Agent to account for typical market cycles — morning warm-up phases, daytime trends, and evening lulls.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defMamba4CastEmbeding;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   int prev_out = descr.window_out = NSkills;
     {
      int temp[] = {PeriodSeconds(PERIOD_H1), PeriodSeconds(PERIOD_D1)};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Using a multi-window convolution block with three convolution windows will make the embeddings richer. It is important to note that the convolution is performed not along the time axis but horizontally, within a single bar, where the relationships between the features are analyzed.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiWindowsConvWPadOCL;
   descr.step = 3;
   descr.count = (prev_out + descr.step - 1) / descr.step;
   descr.window_out = 5;
     {
      int temp[] = {3, 5, 7};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out = int(descr.count * descr.window_out * descr.windows.Size());

At the final stage of encoding the input signal, we will add a normalization layer. Its purpose is to remove skew in the distribution of features, make the analyzed data more homogeneous, and ensure stable model operation during training. This step helps avoid gradient bias and accelerates convergence without any loss of quality.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count*prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Next, we move directly to building the Encoder architecture. And here we plan to work with univariate time series of individual features. Therefore, let’s first transpose our feature tensor.

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   prev_out = descr.count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

It is worth noting, however, that the features we are dealing with here are no longer the same features we previously received from the terminal. At this stage, a completely different set of enriched features has formed, each representing a specific slice of the bar description previously received from the terminal.

The Mamba4Cast Encoder block consists of a stack of convolutions and an SSM module. A normalization layer is included between the modules to align the features. In the convolutional stack, we use multi-window convolution modules. Here, each filter focuses on its own time window and identifies the corresponding market patterns. To preserve the dimensionality of the data, a max-pooling layer is applied after each multi-window convolution module; this layer selects the maximum filter value in each window, thereby reducing the spatial dimensions without altering the feature depth.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiWindowsConvWPadOCL;
   descr.step = 3;
   descr.count = (prev_out + descr.step - 1) / descr.step;
   int filt=descr.window_out = 5;
     {
      int temp[] = {3, 5, 7};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out = int(descr.count * descr.windows.Size());
//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count * prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiWindowsConvWPadOCL;
   descr.step = 3;
   descr.count = (prev_out + descr.step - 1) / descr.step;
   filt=descr.window_out = 5;
     {
      int temp[] = {3, 5, 7};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out = int(descr.count * descr.windows.Size());
//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count * prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count*prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

In SSM, we decided against the original Mamba2 proposed by the authors of the Mamba4Cast framework and chose the Chimera module, which provides data analysis in a two-dimensional plane and allows us to account for cross-dependencies between temporal and spatial components.

//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronChimera;
//--- Window
     {
      int temp[] = {prev_out, prev_out/2}; //In, Out
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
//--- Units
     {
      int temp[] = {prev_count, prev_count*2}; //In, Out
      if(ArrayCopy(descr.units, temp) < int(temp.Size()))
         return false;
     }
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out=descr.windows[1];
   prev_count=descr.units[1];   
//--- layer 12
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count*prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

The module concludes with a batch normalization layer. We have already discussed the advantages of this approach above.

The architecture of our Encoder includes two sequential blocks, each consisting of a multi-window convolution stack, a max-pooling layer, and a SSM module based on Chimera, ensuring step-by-step feature enrichment and context preservation.

//--- layer 13
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiWindowsConvWPadOCL;
   descr.step = 3;
   descr.count = (prev_out + descr.step - 1) / descr.step;
   filt=descr.window_out = 5;
     {
      int temp[] = {3, 5, 7};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out = int(descr.count * descr.windows.Size());
//--- layer 14
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count * prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 15
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiWindowsConvWPadOCL;
   descr.step = 3;
   descr.count = (prev_out + descr.step - 1) / descr.step;
   filt=descr.window_out = 5;
     {
      int temp[] = {3, 5, 7};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out = int(descr.count * descr.windows.Size());
//--- layer 16
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count * prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 17
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count*prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 18
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronChimera;
//--- Window
     {
      int temp[] = {prev_out, prev_out/2}; //In, Out
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
//--- Units
     {
      int temp[] = {prev_count, prev_count*2}; //In, Out
      if(ArrayCopy(descr.units, temp) < int(temp.Size()))
         return false;
     }
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out=descr.windows[1];
   prev_count=descr.units[1];   
//--- layer 19
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count*prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

As a decoder for independently forecasting univariate time series over the entire forecast horizon, we use two consecutive convolutional layers. SoftPlus is placed between them to provide the necessary nonlinearity. The hyperbolic tangent (tanh) is applied at the decoder's output because its range of values corresponds to the normalized data scale, which helps maintain consistency between the model's input and output.

//--- layer 20
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = 4 * NForecast;
   descr.layers = prev_count;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 21
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = NForecast;
   descr.layers = prev_count;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

However, it is worth noting here that, in order to switch to working with univariate time series, we previously transposed the tensor of the analyzed features. Therefore, before passing the decoder results further, it is necessary to restore them to their original form by performing an inverse transposition.

//--- layer 22
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   prev_out = descr.count;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

The next step in restoring the data structure is to reduce the dimensionality that was increased during embedding generation. This is necessary to transform the results tensor into a form compatible with the original data.

//--- layer 23
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = BarDescr;
   descr.layers = 1;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

The final stage in the operation of the encoder is denormalization. At this step, the values obtained after all transformations are returned to the scale of the original data, which allows the model's output to be interpreted correctly.

//--- layer 24
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = prev_count * prev_out;
   descr.layers = 1;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Next, we move on to a description of the Actor architecture. Its primary objective is to assess the current account state and open positions in the context of the analyzed market environment state. Based on the information received, the Actor formulates a trading decision — a trade that can potentially deliver maximum returns with minimal risk.

In this context, the Actor is fed a tensor representing the current account state. This tensor contains aggregated information about the balance, open position volumes, trade direction, and other key parameters that reflect the financial state of the trading Agent.

   CLayerDescription *latent = encoder.At(LatentLayer-1);
//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

These inputs are then processed using a batch normalization layer, which stabilizes the distribution of features and speeds up the model training process.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = AccountDescr;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Next, a cross-attention layer is applied, allowing the current account state to be matched with the market situation. In this process, a latent representation of the environment, previously generated by the Encoder, is used as context.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
     {
      int temp[] = {AccountDescr,     // Inputs window
                    latent.windows[1// Cross window
                   };
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
     {
      int temp[] = {1,              // Inputs units
                    latent.units[1// Cross units
                   };
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.step = 4;                  // Heads
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.layers = 2;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

In this experiment, we used a stack of two cross-attention modules arranged in series. This configuration allows for deeper alignment between the internal account state and market dynamics, enhancing the model’s ability to identify cause-and-effect relationships between the current position and external conditions.

It is important to understand that, for the cross-attention layer, we do not use a general representation of the current environment state as context, but rather the Encoder’s latent representation, which was formed after processing the input signal as embeddings of individual univariate feature time series. Simply put, the encoder (a block within the Environment State Encoder model) converts each feature into its own compact sensitivity vector, and it is these vectors that are fed into the cross-attention module as context.

Imagine that the cross-attention module is a conductor. It has melodies from each instrument (feature embeddings) and the score of the current account balance. The conductor determines which instruments should be louder at that moment — that is, which features are most important for making a decision — and emphasizes those in particular.

As a result, the cross-attention mechanism compares hidden market patterns with the current position and selects signals that will help make an effective trading decision.

The results of the contextual analysis pass through three fully connected layers (MLP), each of which successively refines the representation of the target action. At the output of the final layer, a trading decision is formed — a specific recommendation to open, hold, or close a position, taking into account the current account state and market conditions.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SoftPlus;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = SIGMOID;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

The Director and Critic models share a similar architecture: they analyze the tensor of actions proposed by the Actor in the context of the current market situation. These models generate a corresponding assessment — approval or rejection of the proposed action from a strategic and risk perspective.

I suggest leaving the detailed implementation of these components' architectures for you to explore on your own. The complete source code describing the architecture of all the models being trained, including the Director and the Critic, is provided in the attachment.



Model Training

Now that we have reviewed the architecture of the models, let's move directly on to the training phase. It is important to note here that the authors of the Mamba4Cast framework used synthetic time series to test and train their models. This approach offers a number of advantages, particularly when it comes to developing and debugging deep learning architectures.

First, synthetic data provides complete control over data parameters: you can specify the amplitude, frequency, trends, seasonality, and noise level in advance, and even include rare or anomalous events. This allows targeted testing of how the model responds to various characteristics of time series and helps identify its weaknesses in a strictly controlled environment.

Second, artificially generated time series eliminate the influence of dirty or incomplete data, which is especially important in the early stages of training. Unlike real-world market data, synthetic data does not contain gaps, collection artifacts, or distortions that could mask true model errors.

The third advantage is scalability. Generating synthetic data eliminates the costs of collecting and storing historical data and makes it possible to quickly create training datasets of the required size for problems of any complexity. This is particularly relevant when using resource-intensive models, which require a rich and balanced training environment.

Finally, synthetic data is a reliable tool for stress testing. We can simulate extreme market situations without having to wait for them to occur in real life. Such scenarios make it possible to test the model's robustness to unexpected fluctuations.

On the other hand, the real market isn't a sterile laboratory, but rather a stormy sea where the rules are often made on the fly. That is precisely why, despite all the advantages of synthetic data, training and validating a model exclusively on synthetic data is a one-sided and potentially dangerous path.

First, real-world market data always contains noise, gaps, non-obvious correlations, and dirty areas — elements that do not exist in an artificially created environment. A model that has not been trained on such features may falter at its first encounter with reality — especially on low-liquidity assets or during periods of high volatility.

Second, the market is subject to the surprise effect: news, sanctions, mergers, geopolitics, and the behavior of major players — all of these factors influence prices, but it is virtually impossible to model them reliably using synthetic data. Here, it is especially important that the model be able to adapt and function under conditions of incomplete information.

Third, the behavioral patterns of market participants (ranging from fear to greed) create a unique dynamic that is difficult to replicate using generators. A model that has not been exposed to such patterns runs the risk of overfitting to a clean environment and failing to recognize important signals in real-world trading.

That is precisely why a hybrid approach is considered the most effective: synthetic data is used in the early stages — for calibrating the architecture, selecting hyperparameters, and debugging training. Then real-world data is introduced to train the model to operate in the field, teaching it to make mistakes, adapt, and make decisions in an unstable environment.

At present, we do not have a full-fledged generator for synthetic financial sequences at our disposal. However, as the saying goes, if the mountain will not come to Muhammad…

For the first stage of training, we will attempt to approximate the properties of synthetic data by preprocessing real historical data. As mentioned earlier, synthetic series typically do not contain artifacts, gaps, or other market noise. To achieve a similar level of cleanliness in the actual data, we will apply a simple moving average with a short window to each of the features being analyzed. This will allow us to:

  • smooth out local anomalies and sharp spikes,
  • mitigate the impact of isolated outliers,
  • improve the model's robustness during the training phase.

It is important to note that we deliberately choose a small averaging window to preserve the signal's dynamics and shape. Our goal is not to smooth everything out into a flat line, but simply to dampen the noise that could mislead the model. We will implement similar logic in the Expert Advisor "…\MQL5\Experts\Mamba4Cast\StudyMA.mq5". In this article, we will focus solely on the Train method, which implements the model training process.

The algorithm begins by creating a probability distribution vector for selecting individual trajectories from the experience replay buffer.

void Train(void)
  {
//---
   vector<float> probability = vector<float>::Full(Buffer.Size(), 1.0f / Buffer.Size());

At the initial stage, all trajectories are assigned equal probabilities, enabling a more comprehensive exploration of the entire history.

Next, we initialize the local variables that we will use to temporarily store data while training the models.

   vector<float> result, target, state;
   matrix<float> fstate = matrix<float>::Zeros(1, NForecast * BarDescr);
   matrix<float> hstate = matrix<float>::Zeros(1, HistoryBars * BarDescr);
   bool Stop = false;
   int average = 5;
//---
   uint ticks = GetTickCount();

Once the preparatory work is complete, we move on to setting up the model training loop structure. The outer loop controls the total number of training iterations.

for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
  {
   int tr = SampleTrajectory(probability);
   int start = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast - Batch));
   if(start <= 0)
     {
      iter -= Batch;
      continue;
     }
   if(
      !cEncoder.Clear()
      || !cActor.Clear()
      || !cDirector.Clear()
      || !cCritic.Clear()
   )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
   result = vector<float>::Zeros(NActions);

Here, we sample a single trajectory from the experience replay buffer and the starting state of the training batch. We immediately reset the internal state of all models to eliminate the influence of irrelevant memory on the data for the new trajectory. Next, we initialize a nested model training loop within the training batch.

for(int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++)
  {
   if(!hstate.Assign(Buffer[tr].States[i].state) ||
      MathAbs(hstate).Sum() == 0 ||
      !hstate.Reshape(HistoryBars, BarDescr))
     {
      iter -= Batch + start - i;
      break;
     }

In the body of the nested loop, we load historical data describing the analyzed environment state from the experience replay buffer and set up a loop to smooth it using a moving average.

for(int h = HistoryBars - 1; h > 0; h--)
  {
   state = vector<float>::Zeros(BarDescr);
   for(int a = MathMax(h - average + 1, 0); a <= h; a++)
      state += hstate.Row(a);
   if(!hstate.Row(state / MathMin(average, h + 1), h))
     {
      iter -= Batch + start - i;
      break;
     }
  }

We transfer the smoothed values to the data buffer describing the analyzed environment state.

if(!hstate.Reshape(1, HistoryBars * BarDescr) ||
   !bState.AssignArray(hstate.Row(0)))
  {
   iter -= Batch + start - i;
   break;
  }

Next, keep in mind that for the Mamba4Cast framework to work properly, we need the timestamps for each bar. However, in the experience replay buffer structure we created earlier, only one timestamp is retained for each environment state, corresponding to the last bar. To create the necessary timestamp buffer, we iterate backward through the environment states from the experience replay buffer, starting from the current state and going back to the specified analysis depth, and collect the timestamps.

bTime.Clear();
bTime.Reserve(HistoryBars);
double time = (double)Buffer[tr].States[i].account[7];
for(int t = i; t >= MathMax(0, i - HistoryBars + 1); t--)
   if(!bTime.Add((float)(double)Buffer[tr].States[t].account[7]))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
if(bTime.Total() < HistoryBars)
  {
   float period = MathMin(Buffer[tr].States[i + 1].account[7] - Buffer[tr].States[i].account[7],
                          Buffer[tr].States[i + 2].account[7] - Buffer[tr].States[i + 1].account[7]);
   do
     {
      if(!bTime.Add(bTime[-1] - period))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
     }
   while(bTime.Total() < HistoryBars);
  }
if(bTime.GetIndex() >= 0)
   if(!bTime.BufferWrite())
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }

We will port the algorithm for filling the account state description buffer from similar programs without making any changes.

//--- Account
float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
float profit = float(bState[0] / _Point * (result[0] - result[3]));
bAccount.Clear();
bAccount.Add(1);
bAccount.Add((PrevEquity + profit) / PrevEquity);
bAccount.Add(profit / PrevEquity);
bAccount.Add(MathMax(result[0] - result[3], 0));
bAccount.Add(MathMax(result[3] - result[0], 0));
bAccount.Add((bAccount[3] > 0 ? profit / PrevEquity : 0));
bAccount.Add((bAccount[4] > 0 ? profit / PrevEquity : 0));
bAccount.Add(0);
double x = time / (double)(D'2024.01.01' - D'2023.01.01');
bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_MN1);
bAccount.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_W1);
bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_D1);
bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
if(bAccount.GetIndex() >= 0)
   if(!bAccount.BufferWrite())
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }

After preparing the necessary input data, we proceed to perform the forward pass for all models. The Environment State Encoder is the first to perform a forward pass. It uses smoothed market state description data and a timestamp buffer.

//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Next comes the Actor. It analyzes the account state buffer and the environment context from the latent state of the Encoder.

if(!cActor.feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

We train the Environment State Encoder to predict subsequent states. It is critically important to understand that we do not generate the target manually, nor do we apply moving average smoothing, as is done during the input data preparation stage. Instead, we simply load the prepared environment state that is already in the experience replay buffer, shifted forward by the specified planning horizon.

//--- Look for target
target = vector<float>::Zeros(NActions);
bActions.AssignArray(target);
if(!state.Assign(Buffer[tr].States[i + NForecast].state) ||
   !state.Resize(NForecast * BarDescr) ||
   MathAbs(state).Sum() == 0)
  {
   iter -= Batch + start - i;
   break;
  }
if(!fstate.Resize(1, NForecast * BarDescr) ||
   !fstate.Row(state, 0) ||
   !fstate.Reshape(NForecast, BarDescr))
  {
   iter -= Batch + start - i;
   break;
  }
for(int j = 0; j < NForecast / 2; j++)
  {
   if(!fstate.SwapRows(j, NForecast - j - 1))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
  }

After forming the target values, we can adjust the Encoder parameters by calling the backward pass method.

//--- State Encoder
Result.AssignArray(fstate);
if(!cEncoder.backProp(Result, (CBufferFloat*)NULL, NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Next, based on the available actual data on future price movement, we can form an "almost perfect" trading decision.

target = fstate.Col(0).CumSum();
if(result[0] > result[3])
  {
   float tp = 0;
   float sl = 0;
   float cur_sl = float(-(result[2] > 0 ? result[2] : 1) * MaxSL * Point());
   int pos = 0;
   for(int j = 0; j < NForecast; j++)
     {
      tp = MathMax(tp, target[j] + fstate[j, 1] - fstate[j, 0]);
      pos = j;
      if(cur_sl >= target[j] + fstate[j, 2] - fstate[j, 0])
         break;
      sl = MathMin(sl, target[j] + fstate[j, 2] - fstate[j, 0]);
     }
   if(pos > 0 && tp > 0)
     {
      sl = (float)MathMax(MathMin(MathAbs(sl) / (MaxSL * Point()), 1), 0.01);
      tp = float(MathMin(tp / (MaxTP * Point()), 1));
      result[0] = MathMax(result[0] - result[3], 0.011f);
      result[5] = result[1] = tp;
      result[4] = result[2] = sl;
      result[3] = 0;
      bActions.AssignArray(result);
     }
  }
else
  {
   if(result[0] < result[3])
     {
      float tp = 0;
      float sl = 0;
      float cur_sl = float((result[5] > 0 ? result[5] : 1) * MaxSL * Point());
      int pos = 0;
      for(int j = 0; j < NForecast; j++)
        {
         tp = MathMin(tp, target[j] + fstate[j, 2] - fstate[j, 0]);
         pos = j;
         if(cur_sl <= target[j] + fstate[j, 1] - fstate[j, 0])
            break;
         sl = MathMax(sl, target[j] + fstate[j, 1] - fstate[j, 0]);
        }
      if(pos > 0 && tp < 0)
        {
         sl = (float)MathMax(MathMin(MathAbs(sl) / (MaxSL * Point()), 1), 0.01);
         tp = float(MathMin(-tp / (MaxTP * Point()), 1));
         result[3] = MathMax(result[3] - result[0], 0.011f);
         result[2] = result[4] = tp;
         result[1] = result[5] = sl;
         result[0] = 0;
         bActions.AssignArray(result);
        }
     }
   else
     {
      ulong argmin = target.ArgMin();
      ulong argmax = target.ArgMax();
      float max_sl = float(MaxSL * Point());
      while(argmax > 0 && argmin > 0)
        {
         if(argmax < argmin && target[argmax] / 2 > MathAbs(target[argmin]) &&
                               MathAbs(target[argmin]) < max_sl)
            break;
         if(argmax > argmin && target[argmax] < MathAbs(target[argmin] / 2) && target[argmax] < max_sl)
            break;
         target.Resize(MathMin(argmax, argmin));
         argmin = target.ArgMin();
         argmax = target.ArgMax();
        }
      if(argmin == 0 || (argmax < argmin && argmax > 0))
        {
         float tp = 0;
         float sl = 0;
         float cur_sl = - float(MaxSL * Point());
         ulong pos = 0;
         for(ulong j = 0; j < argmax; j++)
           {
            tp = MathMax(tp, target[j] + fstate[j, 1] - fstate[j, 0]);
            pos = j;
            if(cur_sl >= target[j] + fstate[j, 2] - fstate[j, 0])
               break;
            sl = MathMin(sl, target[j] + fstate[j, 2] - fstate[j, 0]);
           }
         if(pos > 0 && tp > 0)
           {
            sl = (float)MathMax(MathMin(MathAbs(sl) / (MaxSL * Point()), 1), 0.01);
            tp = (float)MathMin(tp / (MaxTP * Point()), 1);
            result[0] = float(MathMax(Buffer[tr].States[i].account[0] / 100 * 0.01, 0.011));
            result[5] = result[1] = tp;
            result[4] = result[2] = sl;
            result[3] = 0;
            bActions.AssignArray(result);
           }
        }
      else
        {
         if(argmax == 0 || argmax > argmin)
           {
            float tp = 0;
            float sl = 0;
            float cur_sl = float(MaxSL * Point());
            ulong pos = 0;
            for(ulong j = 0; j < argmin; j++)
              {
               tp = MathMin(tp, target[j] + fstate[j, 2] - fstate[j, 0]);
               pos = j;
               if(cur_sl <= target[j] + fstate[j, 1] - fstate[j, 0])
                  break;
               sl = MathMax(sl, target[j] + fstate[j, 1] - fstate[j, 0]);
              }
            if(pos > 0 && tp < 0)
              {
               sl = (float)MathMax(MathMin(MathAbs(sl) / (MaxSL * Point()), 1), 0.01);
               tp = (float)MathMin(-tp / (MaxTP * Point()), 1);
               result[3] = float(MathMax(Buffer[tr].States[i].account[0] / 100 * 0.01, 0.011));
               result[2] = result[4] = tp;
               result[1] = result[5] = sl;
               result[0] = 0;
               bActions.AssignArray(result);
              }
           }
        }
     }
  }

It should be noted that this trading decision is based on the trade executed in the previous step. The Agent does not work with isolated signals in a vacuum, but rather builds a sequence of actions. And every subsequent decision is based on a trade that has already been made. Thanks to this approach, we end up not with a collection of disparate orders, but with a full-fledged strategy in which each decision logically follows from the previous one. It is precisely these almost perfect trades that we use to train the Actor.

//--- Actor Policy
bActions.GetData(result);
if(!cActor.backProp(GetPointer(bActions), (CNet*)GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

We use these same trades to train the Critic. The goal is to create an action evaluation function that closely mirrors the actual policy of the Actor. We pass the Critic the same almost perfect sequence of trades, and determine the reward based on the price change on the next bar.

//--- Critic
if(!cCritic.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
float reward = float((bActions[0] - bActions[3]) * fstate[0, 0] / Point());
Result.Clear();
if(!Result.Add(reward)
   || !cCritic.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

In this way, the Critic learns to accurately evaluate the actions of the Actor based on actual price changes and helps develop a more precise and robust strategy.

Things are a little different when it comes to training the Director. You cannot simply feed it only positive examples—otherwise, it will never learn to distinguish bad actions from good ones. Therefore, at each step, we randomly select what the training example will look like:

  • Positive. We feed in an almost perfect action that we have just calculated from the actual data, and assign it the label “1” (success).
  • Negative. We generate a vector of random values with the same dimensionality as the action space and assign it the label “0” (failure).

After that, we call the forward and backward pass methods of the Director.

//--- Director
Result.Clear();
if((MathRand() / 32767.0) > 0.5)
   Result.Add(1);
else
  {
   target = vector<float>::Zeros(NActions);
   for(int i = 0; i < NActions; i++)
      target[i] = float(MathRand() / 32767.0);
      bActions.AssignArray(target);
      Result.Add(0);
  }
if(!cDirector.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cDirector.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

This approach ensures that the Director learns not only to encourage good decisions but also to recognize the flaws in bad ones, helping the Actor avoid ineffective actions.

Now all that remains is to inform the user about the training progress and move on to the next iteration of the loop system.

    if(GetTickCount() - ticks > 500)
      {
       double percent = double(iter + i - start) * 100.0 / (Iterations);
       string str = StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Encoder",
                                     percent, cEncoder.getRecentAverageError());
       str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Actor", percent,
                                                cActor.getRecentAverageError());
       str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Director", 
                                    percent, cDirector.getRecentAverageError());
       str += StringFormat("%-16s %6.2f%% -> Error %15.8f\n", "Critic", percent,
                                               cCritic.getRecentAverageError());
       Comment(str);
       ticks = GetTickCount();
      }
   }
}

After the model training process is complete, we log the results and initiate the shutdown of the Expert Advisor.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Encoder", cEncoder.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", cActor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Director", cDirector.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Critic", cCritic.getRecentAverageError());
   ExpertRemove();
//---
  }

Only minor, targeted changes were made to the offline and online model training programs on real historical data, specifically regarding the creation of a timestamp buffer. We will not go into a detailed study of them right now. Their complete code is included in the attachment, and you can review it on your own. The environment interaction programs are provided there as well.

Our system's training is structured in three stages, each of which gradually prepares the model for real-world market conditions.

First, we perform initial offline training on real historical data using the smoothing method described above. This stage is performed without updating the training set. The batch normalization layer with added noise that we use in the Environment State Encoder will provide sufficient augmentation of the input data and significantly expand the training set from the model's perspective.

Imagine that every candlestick and every indicator passes through a filter that slightly distorts them — this creates many variations of the same situation and prevents the model from simply memorizing the same patterns over and over again. As a result, the Encoder learns to recognize the essence of movement despite minor distortions.

In the second offline stage, historical data is used without smoothing: the model is exposed to the true face of the market — sharp spikes, drops, and noisy fluctuations. This transition from an idealized market to raw data helps the Agent adapt to real-world market fluctuations, learn to maintain the stability of its forecasts, and avoid being thrown off by sudden anomalies. We closely monitor the dynamics of the forecasting error and stop training as soon as the metric remains within a narrow range for several consecutive passes — this is a sign that the model has attuned itself to the data.

Finally, in the third stage, the Agent enters the Strategy Tester for online learning. Here, we observe the behavior of the balance curve. If, after several consecutive passes, the balance stalls and does not show the expected growth, we gently return to offline training: we adjust the Actor’s policy using a nearly ideal trajectory and launch fine-tuning again.

This phased approach ensures both high forecast accuracy and the robustness of trading decisions under any market conditions.



Testing

We have done a tremendous amount of work to adapt and implement the approaches proposed by the authors of the Mamba4Cast framework. Now comes the moment of truth — testing the effectiveness of the implemented solutions on real data.

We used one-minute EURUSD quotes for the entire year of 2024 as the training set. To keep the experiment clean, final testing was conducted on historical data from January through March 2025 — a period that was not used in training. All other parameters remained unchanged to ensure that the evaluation of the strategy was objective and fair.

The test results are presented below.

It must be admitted that we are seeing a fairly high frequency of trades here. The average position holding time is just over 3 minutes. Overall, during the testing period, the model executed 2,677 trades, 1,240 of which were closed at a profit. Although the number of losing trades was slightly higher, the model managed to generate a profit over the testing period, and we can see fairly steady growth in the balance line. This can be partly explained by opening positions with a fairly tight stop-loss and then managing them. This assumption is supported by the small gap between the average losing trade and the largest losing trade. At the same time, the largest winning trade is nearly seven times greater than the average profit per trade.



Conclusion

We have covered the entire path — from the idea and architecture of the Mamba4Cast framework to its practical implementation, training, and rigorous testing on real historical data. We taught the Encoder to sense the market, the Actor to make decisions while taking risks into account, the Director to filter the best and worst signals, and the Critic to evaluate actions based on actual results.

Testing on EURUSD M1 for January–March 2025 showed that Mamba4Cast can not only forecast, but also protect itself from noise, adapt to unexpected events, and remain profitable over the long term.

However, all of the programs presented in this article are for demonstration purposes only and serve to illustrate the capabilities of the Mamba4Cast framework. Before applying the proposed solutions in real trading, you must train the models on a truly representative dataset and conduct comprehensive testing — this is the only way to guarantee the reliability and safety of your trading strategy.


Links


Programs used in this article

# Name Type Description
1 Research.mq5 Expert Advisor Example collection Expert Advisor
2 ResearchRealORL.mq5
Expert Advisor
Example collection Expert Advisor using the Real-ORL method
3 Study.mq5 Expert Advisor Expert Advisor for offline model training
4 StudyMA.mq5 Expert Advisor Expert Advisor for offline model training on averaged data
5 StudyOnline.mq5
Expert Advisor
Expert Advisor for online model training
6 Test.mq5 Expert Advisor Expert Advisor for model testing
7 Trajectory.mqh Class Library Structure for describing the system state and model architectures
8 NeuroNet.mqh Class Library A class library for creating a neural network
9 NeuroNet.cl Library OpenCL program code library


Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/18219

Attached files |
MQL5.zip (2759.84 KB)
Last comments | Go to discussion (1)
fxsaber
fxsaber | 22 May 2025 at 13:05
Результаты тестирования представлены ниже.
Please could you add the relevant tst file to the screenshot? Thank you.
From Basic to Intermediate: Classes (II) From Basic to Intermediate: Classes (II)
This article is intended to be as educational as possible, since the topic we will be discussing often causes considerable confusion in itself. Therefore, dear reader, please try to put what is explained here into practice. If you have any questions, be sure to leave a comment—after all, understanding destructors is no easy task.
Automating Chart Patterns in MQL5 (Part 2): The Double Top and Double Bottom Automating Chart Patterns in MQL5 (Part 2): The Double Top and Double Bottom
We build a robust MQL5 detector for double tops and double bottoms that first confirms the H4 trend, then validates six conditions (point equality, neckline placement, ordering, width, height, and ATR‑based tolerances). The neckline break is timed on the chart's timeframe, and a three-state machine ensures each pattern trades once. The measured‑move target translates structure into clear exits.
Eco-inspired Evolutionary Algorithm (ECO) Eco-inspired Evolutionary Algorithm (ECO)
The article discusses the ECO optimization algorithm, which is based on ecological concepts: populations are grouped into habitats based on territorial proximity, exchange genetic material within habitats, and migrate between them. Despite its wide range of operators and elegant biological metaphor, the algorithm produced a certain result discussed below.
Making Custom Indicators for Beginners (Part 2): Fisher-style Indicator Making Custom Indicators for Beginners (Part 2): Fisher-style Indicator
This article develops a Fisher‑style Indicator in MQL5 from first principles: normalize price within a recent high/low window, smooth and clamp the value, then apply a logarithmic transform. We cover buffer wiring, calculation‑buffer state management across bars, and seeding for stable starts. An accompanying EA implements threshold and reversal confirmation to show how to act on the signal.