Русский Português
preview
Neural Networks in Trading: Adaptive Perception of Market Dynamics (Conclusion)

Neural Networks in Trading: Adaptive Perception of Market Dynamics (Conclusion)

MetaTrader 5 — Trading systems |
276 1
Dmitriy Gizlyk
Dmitriy Gizlyk

Introduction

The financial market is not an easy-to-read table. It is a stream. Continuous, sparse, and at times explosive. Market events do not occur on a schedule. They arise where liquidity shifts, orders break down, and volume spikes flicker past. Understanding this stream is essential for building a reliable, responsive, and adaptive forecasting system. STE-FlowNet was born precisely from this observation: viewing the market as a stream of events and training the model to sense its movement.

That perspective changes everything. Basic models that rely solely on a sequence of candlesticks or aggregated features inherently lose some information about the structure of price movement — specifically, where and when an imbalance between supply and demand arose. Event-driven analysis allows us to capture highly significant signals the moment they appear. This results in low response latency. This increases resistance to averaging effects, where important information is lost in the aggregation process. And one more thing. The event stream naturally combines local sensitivity and global stability. The model detects both the instantaneous spike and the underlying trend. And without any artificial separation into short and long horizons.

STE-FlowNet has brought into finance the idea of spatiotemporal perception, proven in other fields as a method for extracting dynamics from event-based data. It is based not on magic, but on sound engineering. It is designed as a tool for accurate and interpretable modeling of short-term market dynamics and for directly extracting price micro-movements and their local interrelationships over time.

The STE-FlowNet framework can be thought of as a team of experienced financial market analysts, each of whom plays a specific role in processing the stream of events. The encoder is the chief strategist, who records every trade and every change in price and volume, accumulating knowledge about current and past market dynamics. Its ConvGRU memory works like an experienced trader who remembers the entire history of recent price movements and instantly assesses the significance of new signals.

Intermediate movement maps and skip connections form an analytical network of assistants that retain details and instantly pass critical information onward. They enable the model to detect both rapid spikes in activity and slow but significant trends, much like a team that simultaneously monitors microscopic fluctuations in prices and broad market trends.

The correlation layer and iterative refinement act as a group of experts who adjust forecasts based on previous estimates. Each new step takes into account both current data and the entire accumulated history, creating a dynamic, adaptive strategy for predicting market movements.

The final decoder with the Projection Head is a portfolio manager that translates all internal assessments and forecasts into concrete actions: entry or exit signals, a probabilistic movement trajectory, risk assessment, and an assessment of market momentum strength. It brings all parts of the team together and produces a practical solution that is ready for real-time use.

Self-supervised learning and the balance between capturing local anomalies and maintaining global smoothness of the stream make STE-FlowNet robust to noise, spikes, and rare events. In a financial context, this makes it possible to accurately track sharp fluctuations in prices and volume, identify critical points, and support decision-making in highly volatile conditions.

An illustration of the STE-FlowNet framework, created by the author, is shown below.

In previous articles, we completed the implementation of the basic Encoder pipeline and laid the foundation for stream-based data processing. Today we will continue building the framework — we will add residual blocks, implement a complete Flow Decoder, and create a top-level object responsible for the end-to-end processing of the market signal.



Residual Block

Today we will start by building a Residual block — a kind of mini-structure within the model. Before we pick up our tools — that is, before we start writing code — let's take a look at the diagram provided by the framework's authors. Inside the block, we see two consecutive pairs of convolutional blocks with residual connections, and the outputs of these elements are then carefully concatenated with the original input data.

Special attention should be paid to residual connections. When we introduced SEW-ResNet, we already discussed the challenges of using similar architectures in spiking neural networks. The Spike-Element-Wise block immediately comes to mind — a proven component that preserves the signal structure even under complex load conditions.

Why reinvent the wheel when there is a ready-made, reliable solution? I propose building our Residual block using SEW approaches, just as an experienced architect uses proven elements to create a reliable building. This approach will ensure stability, preserve the shape of local features, and speed up the development process, making our block a robust and functional part of the entire model.

We will implement the proposed solution within the new CNeuronSTEFlowNetResidualBlock class, which inherits functionality from the base CNeuronBaseOCL class. The new object fully encapsulates all the logic of the Residual block and its interaction with OpenCL.

class CNeuronSTEFlowNetResidualBlock :  public CNeuronBaseOCL
  {
protected:
   CLayer            cBlock;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSTEFlowNetResidualBlock(void) {};
                    ~CNeuronSTEFlowNetResidualBlock(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint chanels, uint units, uint group_size, uint groups,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //--
   virtual int       Type(void)   override const   {  return defNeuronSTEFlowNetResidualBlock;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
   virtual void      TrainMode(bool flag) override;
   virtual bool      Clear(void) override;
  };

In the structure shown, only one internal component, cBlock, is declared — a dynamic array of objects that we populate during initialization.

The Init method can be thought of as assembling a full-fledged team of experts to analyze the market.

bool CNeuronSTEFlowNetResidualBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                          uint chanels, uint units, uint group_size, uint groups,
                                          ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, 2 * chanels * units, optimization_type, batch))
      return false;
   activation = None;

First, we check whether the parent class is ready to work. It is like making sure the office and infrastructure are ready to welcome employees. If the basic setup fails, the block does not start.

Next, we clear the workstations and connect the OpenCL computing supercomputer so that the entire team can process information efficiently.

   CNeuronSpikeResNeXtBottleneck* conv = NULL;
   CNeuronSpikeResNeXtBlock*      residual = NULL;
//---
   cBlock.Clear();
   cBlock.SetOpenCL(OpenCL);

The first to join the team is Bottleneck — the chief analyst, who filters and compresses the data being analyzed, identifying key features. It receives its tools — channels, groups, and batches — and is ready to perform the analysis. If, for any reason, it is not ready, the team launch is aborted — there is no point in continuing without the lead analyst.

   uint index = 0;
   conv = new CNeuronSpikeResNeXtBottleneck();
   if(!conv ||
      !conv.Init(0, index, OpenCL, chanels, chanels, units, units, group_size, groups, optimization, iBatch) ||
      !cBlock.Add(conv))
     {
      DeleteObj(conv)
      return false;
     }

Next, two blocks with residual connections (Residual Blocks) are added to the team. Their role is to verify and refine the analysis, support the strategy, and preserve important information. Every new analyst is initialized, gains access to resources, and is integrated into the overall workflow. If even one block fails to be created, the team stops to avoid running an incomplete strategy.

   for(int i = 0; i < 2; i++)
     {
      index++;
      residual = new CNeuronSpikeResNeXtBlock();
      if(!residual ||
         !residual.Init(0, index, OpenCL, chanels, chanels, units, units,
                             group_size, groups, optimization, iBatch) ||
         !cBlock.Add(residual))
        {
         DeleteObj(residual)
         return false;
        }
     }
//---
   return true;
  }

Once all internal components have been successfully added, the team is fully ready to begin work. A block can receive signals, process data, and transmit the results to the next stage. As a result, cBlock becomes a close-knit team where every employee knows their role, collaborates with colleagues, and ensures the accuracy of forecasts even in a highly dynamic market.

The forward pass algorithm is implemented in the feedForward method, which can be thought of as the work of a well-coordinated team of analysts. Each of them processes the information stream in turn.

bool CNeuronSTEFlowNetResidualBlock::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(cBlock.Total() <= 0)
      return false;

First, we check to see if there are any members on our team. Without them, nothing will get done. The data then goes to the first analyst in the internal cBlock array.

   CNeuronBaseOCL* prev = NeuronOCL;
   CNeuronBaseOCL* current = NULL;
   for(int i = 0; i < cBlock.Total(); i++)
     {
      current = cBlock[i];
      if(!current ||
         !current.FeedForward(prev))
         return false;
      prev = current;
     }

Each subsequent team member receives the results from the previous one, processes them according to their own rules, and passes them on, as if a series of experts were refining the forecast and filtering out market noise. If anyone on the team fails to cope, the entire process stops to avoid distorting the results.

When the signal passes through all the analysts, their combined work is merged with the block's original data. It is as if the chief analyst had gathered all the reports, added the source data to them, and put together a comprehensive, balanced overview of the current situation.

   if(!Concat(prev.getOutput(), NeuronOCL.getOutput(), Output, 1, 1, Neurons() / 2))
      return false;
//---
   return true;
  }

After that, the Residual block returns a result that is ready for further use in the model's computational path.

The linear structure of the forward pass makes the propagation of error gradients fairly simple and transparent. Therefore, the backward pass methods are left for the reader to study on their own. The complete source code for the class and all of its methods is included as an attachment to this article, allowing you to examine the implementation of the Residual block and its internal logic in detail.


Decoder

The next step in our work is to build the Decoder, a key component of the computational path that transforms the Encoder's hidden representations into information useful for analysis. Particular attention should be paid here to two internal information streams, which enable a more in-depth and accurate reconstruction of the dynamics of the source data. In addition, the use of the latent states of the Encoder allows the Decoder not merely to reconstruct the data, but to form a comprehensive representation of the system’s dynamics by integrating short-term fluctuations and long-term trends. In a financial context, this can be interpreted as the model's ability to account for instantaneous market impulses, individual trades, and overall trends, which is critical for developing forecasts and strategies.

The architecture of the Decoder provides for sequential processing of each feature level while simultaneously accounting for the latent state, which enhances prediction accuracy and the model's robustness to noise and rare anomalies. Each decoding stage includes transposed convolutions and skip connections, allowing spatial and temporal characteristics to be reconstructed in high detail. This approach ensures that critical information about local micro-movements and global phases is preserved, providing a comprehensive foundation for further analysis and decision-making.

Ultimately, the Decoder serves as a bridge between the Encoder’s internal representations and the model’s practical applications, transforming hidden dynamics into a formalizable predictive framework that can be used to assess risks, identify key market points, and develop adaptive strategies.

We build the Decoder as a separate new class, CNeuronSTEFlowNetDecoder, which plays a key role in transforming the hidden states of the Encoder into predictive flow maps. The Decoder is organized around two internal components, cMainLine and cFlow, which process the main features and intermediate streams, respectively. In addition, it stores a pointer to the Encoder, cEncoder, which allows latent states to be used to accurately reconstruct the dynamics.

class CNeuronSTEFlowNetDecoder   :  public CNeuronSpikeActivation
  {
protected:
   CLayer                        cMainLine;
   CLayer                        cFlow;
   CNeuronSTEFlowNetEncoder*     cEncoder;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSTEFlowNetDecoder(void) {};
                    ~CNeuronSTEFlowNetDecoder(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint &chanels[], uint &units[], uint group_size,
                          uint groups, uint heads, uint dimension_k,
                          CNeuronSTEFlowNetEncoder* encoder,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //--
   virtual int       Type(void)   override const   {  return defNeuronSTEFlowNetDecoder;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      SetEncoder(CNeuronSTEFlowNetEncoder* encoder) { cEncoder = encoder; }
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      TrainMode(bool flag) override;
   virtual bool      Clear(void) override;
  };

Initializing the Decoder object is a thorough, step-by-step process in which each decoder element is created, configured, and added to the corresponding information streams — cMainLine and cFlow.

bool CNeuronSTEFlowNetDecoder::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                    uint &chanels[], uint &units[], uint group_size, uint groups,
                                    uint heads, uint dimension_k,
                                    CNeuronSTEFlowNetEncoder *encoder,
                                    ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(chanels.Size() != units.Size())
      return false;
//---
   const uint layers = chanels.Size() - 1;
   if(layers < 1)
      return false;

First, the method verifies the validity of the received parameters: the number of elements in the channel description array must match the size of the array containing the number of elements in the sequences being analyzed, and the total number of layers must be at least one.

Next, the parent class is initialized to prepare the block for operation by specifying the dimensions of the result tensor and the parameter optimization method.

   if(!CNeuronSpikeActivation::Init(numOutputs, myIndex, open_cl, units[layers]*chanels[layers],
                                                                      optimization_type, batch))
      return false;
   cEncoder = encoder;
   uint enc_layers = (!cEncoder ? 0 : cEncoder.Layers());

After that, a pointer to the Encoder is saved so that the Decoder can access the latent states, and the number of Encoder layers is determined to ensure proper interaction.

The internal arrays cMainLine and cFlow are cleared and connected to the OpenCL context, after which the blocks are constructed in stages.

  CNeuronSSAMResNeXtBlock*   conv_res = NULL;
   CNeuronConvOCL*            conv = NULL;
   CNeuronBatchNormOCL*       norm = NULL;
   CNeuronSpikeActivation*    sa = NULL;
   CNeuronSpikeConvGRU*       gru = NULL;
   CNeuronBaseOCL*            concat = NULL;
//---
   cMainLine.Clear();
   cFlow.Clear();
   cMainLine.SetOpenCL(OpenCL);
   cFlow.SetOpenCL(OpenCL);

In the first step, the main Residual block CNeuronSSAMResNeXtBlock is created, initiating the main feature-processing stream.

   uint index = 0;
//--- Main Line
   conv_res = new CNeuronSSAMResNeXtBlock();
   if(!conv_res ||
      !conv_res.Init(0, index, OpenCL, chanels[0], chanels[1],
                     units[0], units[1], group_size, groups, heads,
                     dimension_k, optimization, iBatch) ||
      !cMainLine.Add(conv_res))
     {
      DeleteObj(conv_res)
      return false;
     }

Next, a standard CNeuronConvOCL convolutional layer is added, activated by the SoftPlus function. A CNeuronBatchNormOCL batch normalization layer is also added, ensuring data stability and consistency.

   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, chanels[1], chanels[1], chanels[1], units[1], 1, optimization, iBatch) ||
      !cMainLine.Add(conv))
     {
      DeleteObj(conv)
      return false;
     }
   conv.SetActivationFunction(SoftPlus);
   index++;
   norm = new CNeuronBatchNormOCL();
   if(!norm ||
      !norm.Init(0, index, OpenCL, conv.Neurons(), iBatch, optimization) ||
      !cMainLine.Add(norm))
     {
      DeleteObj(norm)
      return false;
     }
   norm.SetActivationFunction(None);

Subsequent layers form alternating blocks: Spike Activation in the main stream for nonlinear activation, and SSAM ResNeXt in the Flow stream, which processes combined local and global features using the latent states of the Encoder.

   uint ch_in = chanels[0];
//---
   for(uint i = 1; i < layers; i++)
     {
      //--- Main Line
      index++;
      sa = new CNeuronSpikeActivation();
      if(!sa ||
         !sa.Init(0, index, OpenCL, norm.Neurons(), optimization, iBatch) ||
         !cMainLine.Add(sa))
        {
         DeleteObj(sa)
         return false;
        }
      //--- Flow
      index++;
      conv_res = new CNeuronSSAMResNeXtBlock();
      if(!conv_res ||
         !conv_res.Init(0, index, OpenCL, ch_in, chanels[i],
                        units[i - 1], units[i], group_size, groups, heads,
                        dimension_k, optimization, iBatch) ||
         !cFlow.Add(conv_res))
        {
         DeleteObj(conv_res)
         return false;
        }

Each block is thoroughly checked for correctness and is added to the appropriate information stream only after it has been successfully initialized. At each stage, intermediate objects are created to concatenate features and align channel sizes, ensuring that data from the Encoder and the current object are integrated correctly.

      index++;
      ch_in = 2 * chanels[i];
      if(enc_layers - i - 1 >= 0)
        {
         gru = cEncoder.GetLayer(enc_layers - i - 1);
         if(!gru)
            return false;
         if(gru.GetUnits() != units[i])
            return false;
         ch_in += gru.GetChanels();
        }
      concat = new CNeuronBaseOCL();
      if(!concat ||
         !concat.Init(0, index, OpenCL, ch_in * units[i], optimization, iBatch) ||
         !cFlow.Add(concat))
        {
         DeleteObj(concat)
         return false;
        }
      concat.SetActivationFunction(None);

At the end of each loop, additional Residual blocks and convolutional layers with the SoftPlus function and normalization are added, completing the formation of the Main Line structure at this level.

      //--- Main Line
      conv_res = new CNeuronSSAMResNeXtBlock();
      if(!conv_res ||
         !conv_res.Init(0, index, OpenCL, ch_in, ch_in,
                        units[i], units[i + 1], group_size, groups, heads,
                        dimension_k, optimization, iBatch) ||
         !cMainLine.Add(conv_res))
        {
         DeleteObj(conv_res)
         return false;
        }
      index++;
      conv = new CNeuronConvOCL();
      if(!conv ||
         !conv.Init(0, index, OpenCL, chanels[i + 1], chanels[i + 1], chanels[i + 1], units[i + 1], 1,
                                                                              optimization, iBatch) ||
         !cMainLine.Add(conv))
        {
         DeleteObj(conv)
         return false;
        }
      conv.SetActivationFunction(SoftPlus);
      index++;
      norm = new CNeuronBatchNormOCL();
      if(!norm ||
         !norm.Init(0, index, OpenCL, conv.Neurons(), iBatch, optimization) ||
         !cMainLine.Add(norm))
        {
         DeleteObj(norm)
         return false;
        }
      norm.SetActivationFunction(None);
     }
//---
   return true;
  }

The entire process is repeated for all internal layers, creating a complex but coordinated processing mechanism. It enables the Decoder to accurately reconstruct the spatial and temporal characteristics of the data, integrate latent states, and prepare them for subsequent interpretation or prediction.

The method returns true only after all internal components have been successfully initialized and added, ensuring that the Decoder is in a correct and ready-to-operate state.

The forward pass algorithm of our Decoder is organized as a carefully coordinated sequence of data processing through two parallel streams (Main Line and Flow), with integration of latent states from the Encoder.

bool CNeuronSTEFlowNetDecoder::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Main Line
   CNeuronSSAMResNeXtBlock*   conv_res = NULL;
   CNeuronConvOCL*            conv = NULL;
   CNeuronBatchNormOCL*       norm = NULL;
   CNeuronSpikeActivation*    sa = NULL;
   CNeuronSpikeConvGRU*       gru = NULL;
//--- Flow
   CNeuronSSAMResNeXtBlock*   flow = NULL;
   CNeuronBaseOCL*            concat = NULL;
//---
   CNeuronBaseOCL* prev = NeuronOCL;
   int layers = (cMainLine.Total() + 1) / 4;
   int enc_layers = int(!cEncoder ? 0 : cEncoder.Layers());
   int enc_dim = 0;

First, the method prepares references to all internal blocks: Residual blocks, convolutional layers, normalization, Spike activations, and ConvGRU for the main stream, as well as SSAM ResNeXt blocks and concatenation objects in the Flow stream. The prev variable stores a pointer to the object containing the data from the previous step to ensure sequential processing. The layers and enc_layers variables store the number of levels in the Decoder and Encoder, respectively, defining the order in which latent states are integrated.

Decoding takes place in stages. First, the data passes through a Residual block, a convolutional layer, and normalization in the main stream.

   for(int i = 0; i < layers; i++)
     {
      conv_res = cMainLine[i * 4];
      conv = cMainLine[i * 4 + 1];
      norm = cMainLine[i * 4 + 2];
      sa = cMainLine[i * 4 + 3];
      if(enc_layers - i - 2 >= 0)
        {
         gru = cEncoder.GetLayer(uint(enc_layers - i - 2));
         if(!gru)
            return false;
         enc_dim = (int)gru.GetChanels();
        }
      else
        {
         gru = NULL;
         enc_dim = 0;
        }
      flow = cFlow[i * 2];
      concat = cFlow[i * 2 + 1];
      //--- Main Line
      if(!conv_res ||
         !conv_res.FeedForward(prev))
         return false;
      if(!conv ||
         !conv.FeedForward(conv_res))
         return false;
      if(!norm ||
         !norm.FeedForward(conv))
         return false;
      if(i == (layers - 1))
         break;

Next, if this is not the last layer, a Spike layer is activated for nonlinear transformation.

      if(!sa ||
         !sa.FeedForward(norm))
         return false;

At the same time, the Flow stream processes the input data and adds intermediate representations obtained from the Encoder via ConvGRU. At each level, the outputs of these two streams are combined into a concat object, which carefully concatenates local features from the Main Line, intermediate Flow maps, and, where available, latent states of the Encoder, creating a coherent representation for the next level.

      //--- Flow
      if(!flow ||
         !flow.FeedForward(prev))
         return false;
      if(!concat)
         return false;
      if(!gru)
        {
         if(!Concat(sa.getOutput(), flow.getOutput(), concat.getOutput(),
                    conv.GetFilters(), flow.GetChannelsOut(), conv.GetUnits()))
            return false;
        }
      else
        {
         if(conv.GetUnits() != gru.GetUnits())
            return false;
         if(!Concat(sa.getOutput(), flow.getOutput(), gru.getOutput(), concat.getOutput(),
                    conv.GetFilters(), flow.GetChannelsOut(), enc_dim, conv.GetUnits()))
            return false;
        }
      prev = concat;
     }

At the same time, the algorithm takes into account the matching of channel dimensions and unit counts between the Decoder and Encoder layers, ensuring that information is combined correctly without losing significant details.

After passing through all layers of the Decoder, the final Spike-Activation is invoked via the parent object, forming the final result of the Decoder operation.

   if(!CNeuronSpikeActivation::feedforward(norm))
      return false;
//---
   return true;
  }

As a result of this structure, the Decoder is capable of accumulating local and global features, integrating latent states of the Encoder, and transforming them into a complete, ready-to-use predictive representation. This is particularly important for financial streams, where it is necessary to account for both price micro-movements and long-term trends at the same time.

However, the forward pass only forms a preliminary prediction based on the model's current parameters, which is of little informative value without training. In order for the model to truly learn to identify patterns and adequately predict dynamics, it is necessary to implement backward pass algorithms that allow the weights to be adjusted based on the error between the prediction and the target value.

Special attention should be paid to the multi-stream architecture of the Decoder. The parallel streams Main Line and Flow, along with the integration of the latent states of the Encoder, create a complex network of dependencies in which an error in one stream affects the others. This requires a careful approach to gradient propagation in order to correctly account for the contribution of each block and avoid distorting the information when updating the weights.

Consequently, the backward pass in CNeuronSTEFlowNetDecoder must not simply propagate the error backward, but rather carefully aggregate it across all branches, correctly combining local, global, and latent features. Only with this approach does training become effective, allowing the model to adapt to complex spatiotemporal patterns and detect micro-movements and global trends, which is critically important for analyzing the dynamics of financial streams and developing reliable forecasting strategies.

The error-gradient distribution algorithm is implemented in the calcInputGradients method and is a key component of the model training process. It ensures correct error propagation through the Decoder's complex multi-stream architecture by accumulating the contribution of each block.

bool CNeuronSTEFlowNetDecoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
//--- Main Line
   CNeuronSSAMResNeXtBlock*   conv_res = cMainLine[-3];
   CNeuronConvOCL*            conv = cMainLine[-2];
   CNeuronBatchNormOCL*       norm = cMainLine[-1];
   CNeuronSpikeActivation*    sa = cMainLine[-4];
   CNeuronSpikeConvGRU*       gru = NULL;
//--- Flow
   CNeuronSSAMResNeXtBlock*   flow = cFlow[-2];
//---
   int layers = cMainLine.Total() / 4;
   CNeuronBaseOCL* prev = (layers > 1 ? cFlow[-1] : NeuronOCL);
   int enc_layers = int(!cEncoder ? 0 : cEncoder.Layers());
   int enc_dim = 0;

At the beginning of the method, local variables are created to store pointers to the internal components of the main stream: Residual blocks (conv_res), convolutional layers (conv), normalization (norm), and Spike activations (sa). For the Flow stream, objects named flow. are defined. The prev variable points to the previous layer, which will participate in backpropagation.

The number of layers in the Decoder and Encoder is also calculated, allowing latent states to be correctly integrated for the backward pass.

The first step is to compute the gradients for the final normalization layer.

   if(!CNeuronSpikeActivation::calcInputGradients(norm))
      return false;

This is followed by the sequential calculation of gradients for the convolutional layers and Residual blocks of the main stream.

   if(!conv ||
      !conv.CalcHiddenGradients(norm))
      return false;
   if(!conv_res ||
      !conv_res.CalcHiddenGradients(conv))
      return false;

This step ensures the accurate propagation of errors through linear and nonlinear transformations, laying the foundation for the correct adjustment of the model's weights.

Next, a loop is implemented that proceeds from the upper layers to the lower ones.

   if(!prev ||
      !prev.CalcHiddenGradients(conv_res))
      return false;
//---
   for(int i = layers - 1; i >= 0; i--)
     {
      if(enc_layers - i - 2 >= 0)
        {
         gru = cEncoder.GetLayer(uint(enc_layers - i - 1));
         if(!gru)
            return false;
         enc_dim = (int)gru.GetChanels();
        }
      else
        {
         gru = NULL;
         enc_dim = 0;
        }
      if(!sa || !flow)
         return false;

For each level, a check is performed to determine whether a corresponding ConvGRU layer is present in the Encoder. If it is present, it is included in the overall stream for gradient distribution, ensuring the correct integration of latent states.

Within the loop, gradients are split between the Main Line and Flow using the DeConcat method, which carefully distributes the error among local features, intermediate maps, and the latent vectors of the Encoder. This ensures that each component receives an adequate training signal.

      if(!gru)
        {
         if(!DeConcat(sa.getGradient(), flow.getGradient(), prev.getGradient(),
                      conv.GetFilters(), flow.GetChannelsOut(), conv.GetUnits()))
            return false;
        }
      else
         if(!DeConcat(sa.getGradient(), flow.getGradient(), gru.getGradient(), prev.getGradient(),
                      conv.GetFilters(), flow.GetChannelsOut(), enc_dim, conv.GetUnits()))
            return false;

After the error is distributed in the Flow stream, backpropagation is performed in the Main Line: first through the Spike activation, then through normalization and convolutional layers into the Residual block.

      //--- Flow
      prev = (i > 0 ? cFlow[i * 2 - 1] : NeuronOCL);
      if(!prev ||
         !prev.CalcHiddenGradients(flow))
         return false;
      //--- Main Line
      conv_res = cMainLine[i * 4];
      conv = cMainLine[i * 4 + 1];
      norm = cMainLine[i * 4 + 2];
      if(!norm.CalcHiddenGradients(sa))
         return false;
      if(!conv ||
         !conv.CalcHiddenGradients(norm))
         return false;
      if(!conv_res ||
         !conv_res.CalcHiddenGradients(conv))
         return false;

To correctly sum the gradients from two information streams, we use the tried-and-tested pointer-swapping trick for data buffers, which allows the contribution to be accumulated while minimizing distortion during error propagation.

      CBufferFloat* temp = prev.getGradient();
      if(!prev.SetGradient(prev.getPrevOutput(), false) ||
         !prev.CalcHiddenGradients(conv_res) ||
         !SumAndNormilize(temp, prev.getGradient(), temp, 1, false, 0, 0, 0, 1) ||
         !prev.SetGradient(temp, false))
         return false;
      if(i==0)
         continue;
      sa = cMainLine[i * 4 - 1];
      //--- Flow
      flow = cFlow[(i-1) * 2];
     }
//---
   return true;
  }

The method also takes into account the characteristics of the last layer, properly terminates the loop, and updates the gradients for all intermediate objects. As a result, the algorithm ensures stable and accurate error propagation throughout the Decoder's entire structure, enabling the network to effectively adapt to complex spatiotemporal dependencies and capture micro-movements and global trends, which is particularly important for financial streams and forecasting strategies.

Thus, calcInputGradients implements a complete, multi-stream backpropagation mechanism. It ensures that every block and every channel receives the correct information for updating its weights, providing reliable and fine-grained model training.

Adjusting the Decoder's parameters to minimize the error is handled by the updateInputWeights method, which directly updates the weights of all internal components of the module based on previously calculated gradients. This process is the final stage of the training loop: after the forward pass, prediction generation, and backpropagation, the model receives information about how each parameter affected the result and adjusts them accordingly to reduce the value of the loss function.

bool CNeuronSTEFlowNetDecoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
//--- Main Line
   CNeuronSSAMResNeXtBlock*   conv_res = NULL;
   CNeuronConvOCL*            conv = NULL;
   CNeuronBatchNormOCL*       norm = NULL;
   CNeuronSpikeActivation*    sa = NULL;
   CNeuronSpikeConvGRU*       gru = NULL;
//--- Flow
   CNeuronSSAMResNeXtBlock*   flow = NULL;
   CNeuronBaseOCL*            concat = NULL;
//---
   CNeuronBaseOCL* prev = NeuronOCL;
   int layers = (cMainLine.Total() + 1) / 4;
   int enc_layers = int(!cEncoder ? 0 : cEncoder.Layers());
   int enc_dim = 0;

The method begins by initializing pointers to key components. In addition, the number of Decoder and Encoder layers is determined in order to correctly integrate latent states when updating the weights.

Within the loop, the parameters are adjusted step by step for each Decoder layer. First, the weights of the Residual block are updated using the output of the previous layer as the input signal.

   for(int i = 0; i < layers; i++)
     {
      conv_res = cMainLine[i * 4];
      conv = cMainLine[i * 4 + 1];
      norm = cMainLine[i * 4 + 2];
      sa = cMainLine[i * 4 + 3];
      if(enc_layers - i >= 0)
        {
         gru = cEncoder.GetLayer(uint(enc_layers - i - 1));
         if(!gru)
            return false;
         enc_dim = (int)gru.GetChanels();
        }
      else
        {
         gru = NULL;
         enc_dim = 0;
        }
      flow = cFlow[i * 2];
      concat = cFlow[i * 2 + 1];
      //--- Main Line
      if(!conv_res ||
         !conv_res.UpdateInputWeights(prev))
         return false;

Next, the parameters of the convolutional layer, normalization, and Spike activation are updated sequentially, ensuring consistency across the entire Main Line and preventing the loss of gradient information at intermediate stages.

      if(!conv ||
         !conv.UpdateInputWeights(conv_res))
         return false;
      if(!norm ||
         !norm.UpdateInputWeights(conv))
         return false;
      if(i == (layers - 1))
         break;
      if(!sa ||
         !sa.UpdateInputWeights(norm))
         return false;

Special attention is given to integrating information from the Encoder. If the corresponding ConvGRU layer is present, its channels and latent vectors are taken into account when updating the weights, which allows the Decoder to correctly account for long-term dependencies and global features preserved during the signal encoding stages.

The Flow stream is updated in parallel with the Main Line, which ensures synchronization of local and global information during training and facilitates accurate reconstruction of the data's phase state.

      //--- Flow
      if(!flow ||
         !flow.UpdateInputWeights(prev))
         return false;
      prev = concat;
     }

After passing through all layers, the parent class's weights are updated, completing the adjustment process.

   if(!CNeuronSpikeActivation::updateInputWeights(norm))
      return false;
//---
   return true;
  }

This arrangement ensures that all internal Decoder parameters receive correct training signals that account for both local fluctuations in the input stream and global trends integrated through the Encoder.

Overall, the updateInputWeights method implements a multi-stream, hierarchical mechanism for updating parameters. Each block receives a precise correction signal; the gradients are accumulated and distributed among the components.

The complete source code for CNeuronSTEFlowNetDecoder and all of its methods is provided in the attachment.



Top-level object

In the next step, we move on to building an integrated top-level object that brings together all the key components of the model into a single architecture. After the careful development of the individual modules (Encoder, Decoder, and Residual block), it becomes necessary to create a structure capable not only of processing the event stream sequentially but also of synchronizing the operation of all internal components. Such an object makes it possible to combine spatiotemporal encoding, accurate extraction of local features, and prediction of data dynamics into a single, coherent mechanism.

The top-level object, CNeuronSTEFlowNet, serves as the integrator of all the model's key components. It inherits the base functionality from CNeuronSTEFlowNetDecoder, which eliminates the need to create an internal Decoder component. Its functionality is instead implemented using the parent class.

class CNeuronSTEFlowNet  :  public CNeuronSTEFlowNetDecoder
  {
   //---
protected:
   CNeuronSTEFlowNetEncoder         cSTEEncoder;
   CNeuronSTEFlowNetResidualBlock   cSTEResidualBlock;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSTEFlowNet(void) {};
                    ~CNeuronSTEFlowNet(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint &chanels[], uint &units[], uint group_size,
                          uint groups, uint heads, uint dimension_k, uint stack_size,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //--
   virtual int       Type(void)   override const   {  return defNeuronSTEFlowNet;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      TrainMode(bool flag) override;
   virtual bool      Clear(void) override;
  };

Within the object, cSTEEncoder is declared, which provides spatiotemporal encoding of the input event stream, along with cSTEResidualBlock, which supports stable and accurate extraction of local features and micro-movements. This approach makes it possible to simultaneously identify detailed patterns over short time intervals and global trends accumulated by the Encoder.

Initialization of the top-level STE-FlowNet object begins with a check to ensure that the sizes of the channel arrays and sequences match, in order to rule out configuration discrepancies.

bool CNeuronSTEFlowNet::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint &chanels[], uint &units[], uint group_size,
                             uint groups, uint heads, uint dimension_k, uint stack_size,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(chanels.Size() != units.Size())
      return false;

To ensure the decoder operates correctly, reverse arrays of channels and sequence lengths are created by reversing the original arrays. In this case, the first element of the channel array is doubled to account for the concatenated outputs of the Residual block and to ensure a consistent connection to the Decoder.

   uint rev_chanels[], rev_units[];
   if(ArrayResize(rev_chanels, chanels.Size()) < 0 ||
      ArrayResize(rev_units, units. Size()) < 0)
      return false;
   uint total = chanels.Size();
   for(uint i = 0; i < total; i++)
      rev_chanels[total - i - 1] = chanels[i];
   rev_chanels[0] *= 2;
//---
   for(uint i = 0; i < total; i++)
      rev_units[total - i - 1] = units[i];

Next, each key component is initialized. The Encoder is created using the original channel arrays and sequence lengths, with specified parameters for grouping, attention heads, and latent-space dimensionality.

   if(!cSTEEncoder.Init(0, 0, open_cl, chanels, units, group_size, groups, heads,
                        dimension_k, stack_size, optimization_type, batch))
      return false;

After the Encoder has been successfully started, the Residual block is initialized; it processes the Encoder output and forms a stable representation of the dynamics.

   if(!cSTEResidualBlock.Init(0, 1, open_cl, chanels[total - 1], units[total - 1], group_size, groups,
                              optimization, iBatch))
      return false;

The final step initializes the Decoder using the reverse arrays of channels and sequence lengths. It receives a pointer to the Encoder object, which allows it to use latent states to construct accurate intermediate representations of the event stream.

   rev_chanels[0] = cSTEResidualBlock.Neurons() / rev_units[0];
   if(!CNeuronSTEFlowNetDecoder::Init(numOutputs, myIndex, open_cl, rev_chanels,
                                      rev_units, group_size, groups, heads, dimension_k,
                                      cSTEEncoder.AsObject(), optimization_type, batch))
      return false;
//---
   return true;
  }

Once all steps have been successfully completed, the object is ready for use, integrating the Encoder, the Decoder, and the Residual block into a single, coordinated model for processing spatiotemporal data.

The forward pass of STE-FlowNet is organized as sequential data passing through all key components of the model. First, the input signal is fed into the Encoder, which forms a latent representation of the events being analyzed by accumulating spatiotemporal information and extracting meaningful features.

bool CNeuronSTEFlowNet::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cSTEEncoder.FeedForward(NeuronOCL))
      return false;

The Encoder output is then passed to the Residual block, which strengthens the extracted features and preserves critical local details. This block ensures the model's robustness against noise and improves the accuracy of subsequent calculations, thereby laying the foundation for correct decoding.

   if(!cSTEResidualBlock.FeedForward(cSTEEncoder.AsObject()))
      return false;

Finally, the updated data are fed into the Decoder, which constructs a prediction of the event stream based on the latent state of the Encoder and the outputs of the Residual block.

   return CNeuronSTEFlowNetDecoder::feedForward(cSTEResidualBlock.AsObject());
  }

This step-by-step approach ensures that each event is processed reliably. It takes into account local micro-movements and global trends, creating a coherent and informative representation of the system's dynamics. In a financial context, this is analogous to sequentially processing the analyzed transactions and price changes to build a highly detailed and adaptive forecast of market movements.

Ultimately, CNeuronSTEFlowNet is a fully integrated object capable of sequentially analyzing a stream of events, detecting critical micro-movements, taking global patterns into account, and dynamically adjusting its parameters to generate reliable predictions. In the context of financial markets, this means the ability to simultaneously track instantaneous price fluctuations and long-term trends, creating an adaptive, highly accurate analysis model for strategic decision-making and risk management.

The complete code for the class and all of its methods is included in the attachment to this article.

Creating a top-level object opens up new possibilities. Now, all previously implemented modules are combined into a single structure and can be integrated into models literally as a single layer. This approach greatly simplifies the construction of complex neural network architectures and makes integrating them into trading strategies as convenient and intuitive as possible.

Today, we will not delve into a detailed description of the architecture of each model being trained, as that is not the main focus here. All details regarding the implementation and structure can be found in the attachment to this article.



Testing

We have reached the final stage of our work with the STE-FlowNet framework — evaluating the effectiveness of the approaches we have implemented. But first, we need to train our trader.

The first stage involves offline training using historical data for the EURUSD currency pair on the H1 timeframe for the period from January 2024 through June 2025. This period became a full-fledged training ground. The model was trained to recognize historical patterns, analyze price dynamics and trade volumes, and identify correlations between key features. STE-FlowNet developed its own trader's intuition, combining it with precise strategic assessment and gradually building the ability to forecast market movements and assess the risk of each potential trade.

The next stage — online fine-tuning in the MetaTrader 5 Strategy Tester — moves the training process into more realistic conditions. The model processes data streams one candlestick at a time. Here, STE-FlowNet masters market dynamics, learns to remain stable amid noisy fluctuations, adjusts its actions during periods of low liquidity, and reacts instantly to sharp price spikes. The underlying structure, formed on historical data, remains unchanged, but the model learns to respond flexibly to current market conditions, minimizing the risk of overfitting and improving forecast accuracy even in a volatile environment.

The final verification was conducted using entirely new data for July–September 2025. All parameters obtained in the previous stages were loaded without modification, ensuring a fair assessment of STE-FlowNet's ability to generalize.


The backtesting results look convincing and demonstrate the strategy's stability. Let's start with the financial metrics. With an initial deposit of USD 100, the net profit was USD 54.02, representing total capital growth of more than 50% over the test period. Gross profit reached USD 119.57, while gross losses amounted to USD 65.55, resulting in a profitability coefficient (Profit Factor) of 1.82. This metric indicates that the strategy generates nearly twice as much income as its losses, which is considered a solid result for trading algorithms. A Recovery Factor of 1.31 indicates the strategy's ability to recover from drawdowns.

Capital drawdowns are at an acceptable level. The maximum drawdown based on equity was 28.57%, which is reasonable for an active algorithmic approach in the foreign exchange market. At the same time, the absolute balance drawdown was 25.13%, demonstrating the strategy's ability to hold positions effectively and avoid a sharp decline in capital.

The trading results also demonstrate the strategy's balance. A total of 50 trades were executed, 62% of which were profitable. The percentage of winning short positions was 63.16%, while that of winning long positions was 61.29%, indicating that the algorithm performs correctly in both downtrends and uptrends. The average profit per trade was USD 3.86, and the average loss was USD 3.45 — the ratio of winning to losing trades is well balanced. The largest profit on a single trade was USD 15.03, and the largest loss was USD 9.96.

Overall, the tests confirm that STE-FlowNet successfully learns from historical data, responds appropriately to market signals, and demonstrates consistent capital growth. The strategy is capable of combining forecast accuracy with risk management. However, before using it in actual trading, more thorough and comprehensive testing is required.



Conclusion

In this article, we have examined in detail the architecture and key components of the STE-FlowNet framework, demonstrating its flexibility and adaptability to various data analysis tasks. The tests conducted confirmed the high effectiveness of the implemented solutions, demonstrating stable and accurate performance even on complex time series. Particular attention is paid to the framework's unique ability to integrate multi-stream processing and recurrent structures, which speeds up computations and enables deeper modeling of data dependencies. The approach presented here opens up new opportunities for practical application in trading and related fields, enabling the creation of strategically significant forecasts with minimal resource expenditure.


Links


Programs used in this article

# Name Type Description
1 Study.mq5 Expert Advisor Expert Advisor for offline model training
2 StudyOnline.mq5 Expert Advisor Expert Advisor for online model training
3 Test.mq5 Expert Advisor Expert Advisor for model testing
4 Trajectory.mqh Class library Structure describing the system state and model architecture
5 NeuroNet.mqh Class library Class library for creating a neural network
6 NeuroNet.cl Library Code library for an OpenCL program

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

Attached files |
MQL5.zip (3251.54 KB)
Last comments | Go to discussion (1)
Chao Tang
Chao Tang | 3 Nov 2025 at 13:16
Using the same code, the STFS was run. The Critic data is clearly incorrect.
The Fractional Fourier Domain — A Chirp-Rate Adaptive Cycle Indicator for MetaTrader 5 The Fractional Fourier Domain — A Chirp-Rate Adaptive Cycle Indicator for MetaTrader 5
This article presents a native MQL5 Fractional Fourier indicator for measuring whether the dominant cycle is speeding up or slowing down. It searches for the rotation that maximizes spectral concentration in a detrended analytic window, using a unitary eigenvector FrFT and a golden‑section entropy search, and reconstructs a band‑limited cycle line. Results on known chirps and on real closes across symbols/timeframes show where the method is reliable, its bias, and its limited forward information in practice.
Using Renko-bar Regression with Error Correction Using Renko-bar Regression with Error Correction
The article presents a regression-based approach to forecasting Renko bars using CatBoost: the model estimates the logarithmic return of the next bar and the uncertainty of the forecast. We will examine a cascade of residual models with OOF validation using TimeSeriesSplit, shrinkage, and overall early stopping, as well as conditional bias correction. On EURUSD D1, a reduction in OOF-MAE was achieved, along with approximately 65% directional accuracy. A working script for MetaTrader 5 is provided that generates a signal, position size, SL, and TP in brick units.
Online Portfolio Selection: A Provable Regret Bound, and the Cost That Eats It Online Portfolio Selection: A Provable Regret Bound, and the Cost That Eats It
The article implements Online Portfolio Selection in MQL5: exact simplex projection, four allocators (EG, Universal Portfolio, PAMR, OLMAR), and a BCRP solver on a concave objective. We verify weight paths against the published Python reference and self-check the optimizer. A drift-aware transaction cost model measures turnover and net wealth on a multi-asset basket. Readers get a tested library and evidence on when regret-bounded methods lose to costs.
Engineering Trading Discipline into Code (Part 9): Building a Trade Activity Audit Trail in MQL5 Engineering Trading Discipline into Code (Part 9): Building a Trade Activity Audit Trail in MQL5
Trade authorization controls what a cooperating Expert Advisor is permitted to send, but not every account action passes through that code. A trade may originate from the desktop terminal, a mobile device, the web terminal, or another Expert Advisor. This installment develops an independent auditor that records live transaction stages through OnTradeTransaction(), reconciles available historical orders and deals within a configured recovery window, and writes an account-bound CSV journal. An on-chart panel summarizes the selected period and provides refresh, detail, and export controls without modifying trades.