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

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

MetaTrader 5 — Trading systems |
163 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introduction

The behavior of modern financial markets increasingly resembles a continuous stream of events in which the magnitude of price changes and the exact moment they occur, the time intervals between impulses, and the underlying fabric of the market’s micro-movements are all significant. It was precisely this idea that formed the basis for the transition from a static view to an event-driven approach to data processing, capable of capturing the market’s dynamic pulse in real time.

Unlike classical models, which use discrete candlesticks and smooth out price movements, the event-driven approach treats the market as a continuous, reactive system. Every event (a tick, a spike in volume, instantaneous liquidity) has its own weight and significance. This makes it possible to avoid the loss of information that is inevitable with coarse time quantization. This approach is more in line with the nature of actual market dynamics. It helps models learn not only from the values themselves, but also from the structure of the temporal relationships between them.

In the previous article, we explored the theoretical aspects of the STE-FlowNet framework. However, the first step was only a foundation. The time has come to move forward — to transition from concept to developing the approaches proposed by the framework’s authors, adapting them to the tasks of analyzing and forecasting financial market time series.

We did not merely abandon the traditional view of the market as a sequence of averaged bars; we effectively recalibrated our perspective. Instead of recording the outcome, we learned to listen to the process itself. Not the outcome of the candlestick, but the impulse that preceded it. Not a ready-made structure, but the fabric of continuous becoming. We built an event-based picture of market time. Each tick is perceived not as just another number, but as an act of interaction between supply and demand, a moment of decision-making at the threshold of micro-equilibrium.

This laid the groundwork for moving on to the next step — models capable of detecting the presence of an event and capturing the internal direction of the flow. The ability to discern exactly how this is happening right now is a key prerequisite for building systems capable of making predictions not from the traces left by an event, but from the trajectory of its unfolding. And this is exactly where the territory of the STE-FlowNet framework begins: an event-dynamic view of the market.

We have learned to recognize the very emergence of a market event; now it is important for us to learn to discern where it is pulling the market next. Not merely to register the fact of an impulse, but to read the direction of the hidden flow of forces even before that flow becomes evident in the price. It is precisely this ability to anticipate that distinguishes passive modeling from true market anticipation. In classical ML models, this problem is solved after the fact: the flow has already formed, the candle has closed, and the movement has occurred. STE-FlowNet offers a fundamentally different approach: learning to see movement within movement and reconstructing the direction of the trajectory while the event is forming.

The STE-FlowNet framework was originally created not for finance, but for analyzing event flows in dynamic environments: in optical tracking tasks, where what matters is not what has already happened in the frame, but how the change in the image itself evolves over time. This is a hybrid event-temporal architecture capable of reconstructing the hidden direction of flow based on the differences between events, much like the human brain perceives motion where the eyes detect only discrete flashes of light. We adapt this mechanism to the market, transforming financial time into a stream of optical micro-shifts from which we can extract the vector structure of a probable future.

For financial markets, this is critically important. Price does not move on its own: it is driven by clusters of micro-impulses of liquidity, most of which remain invisible in traditional OHLC representations. Even a high-frequency candle merely records the outcome of the struggle, but does not reveal its internal dynamics. It is important for us to understand exactly where the system is headed right now, before the full force of its potential movement has been revealed. This is precisely what sets STE-FlowNet apart from statistical predictive models: it does not make guesses based on past data, but rather reconstructs the direction of the unfolding action.

It is precisely for this purpose that we are applying STE-FlowNet to the analysis of market time series: not as just another neural network for price forecasting, but as a tool for reconstructing the internal field of market movement, hidden within the microstructure of events. In fact, we are teaching the model to interpret the stream of ticks in the same way that a person intuitively senses the build-up of market tension before an explicit price movement occurs: only not in a subjective or probabilistic way, but through a formalizable, computable structure.

To illustrate this idea, let’s imagine the market as a force field in which each event is not a point but a disturbance that creates local tension. It is precisely this tension — not the price change itself, but the direction of its potential — that we need to capture. Spatio-Temporal Event embedding in STE-FlowNet functions as a converter for this field. It does not record price as a value, but instead encodes the vector of state change over time and in the event space. Every event is interpreted not as a fact, but as a transfer of force. Like a micro-pressure that either dissipates or intensifies in a specific direction.

The next step is Learnable Flow Reconstruction. This is the core of the architecture. The model does not simply string events together one after another; instead, it attempts to reconstruct a meaningful trajectory of the flow — as if it were reconstructing an invisible wind from the swaying of the grass. It extracts from the sequence of events not a story, but a moving form — an evolution vector. Therefore, STE-FlowNet does not predict the market state; rather, it learns to read the market’s underlying drive in real time, while that drive is still only emerging within the microstructure of trading.

It is precisely here that the fundamental difference between STE-FlowNet and classical approaches becomes apparent. It works as a system that reads the market's intent even before it manifests itself in price fluctuations. This is neither a statistical forecast nor pattern identification — it is an attempt to grasp the causal architecture of market movement as it is taking shape. And this is the key shift: from analyzing the consequence to perceiving the cause.

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

In the practical section of the previous article, we successfully implemented the CNeuronSpikeConvGRU layer, laying the foundation for further work. Today, we continue on this path, delving deeper into the development of algorithms for the STE-FlowNet framework. The main focus is shifting from a theoretical understanding of the market to the practical implementation of an architecture capable of interpreting the flow of events and predicting the direction of market movement in real time.


Correlation Module

The next step in our work is to develop a module for learning the correlations between states. It is important to note that, in their work, the authors of the original framework analyzed correlations of the current frame with respect to displacements of the first frame in the analyzed sequence, thereby determining the dynamic momentum of the entire preceding sequence. This approach is entirely justified for the analysis of fixed, finite sequences where the historical context remains accessible.

Our task, however, is fundamentally different: we aim to analyze data in real time. This leads to the analysis of effectively infinite sequences. Moreover, the impact of events diminishes as the distance from them increases, which makes long-term use of a static state ineffective.

The first natural solution is to use a movable point as a reference for analysis. This approach is simple, leaves the rest of the architecture unchanged, and allows the offset size to be controlled as a model hyperparameter, providing a trade-off between analysis speed and accuracy. However, there is also a conceptual limitation: each state gets its own reference, which makes correlations between consecutive states incomparable, not to mention correlations with more distant events.

Consequently, there is a need to find a more suitable solution. A state stack may provide such a solution. In this case, we abandon augmentation of the reference state and instead determine the correlations between each new state and all the states stored on the stack. This approach makes it possible to preserve the historical structure of event influence while also ensuring that correlations remain comparable, which is important for analyzing and forecasting dynamic market processes.

Stack object

Organizing a stack is a general-purpose task that requires a careful and flexible approach. To ensure modularity and reusability, we implemented it as a separate class that manages dynamic state storage and supports integration with other components.

class CNeuronAddToStack :  public CNeuronBaseOCL
  {
protected:
   uint              iStackSize;
   uint              iDimension;
   uint              iVariables;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronAddToStack(void) : iStackSize(0), iDimension(0), iVariables(0) {};
                    ~CNeuronAddToStack(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint stack_size, uint dimension, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronAddToStack;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
   //---
   virtual uint      GetStackSize(void) const { return iStackSize; }
   virtual uint      GetDimension(void) const { return iDimension; }
   virtual uint      GetVariables(void) const { return iVariables; }
  };

In the class structure, we see only three variables declared that define the stack parameters:

  • iStackSize — stores the number of states on the stack;
  • iVariables — the number of unit sequences in each state;
  • iDimension — specifies the dimensionality of the description of a single unit sequence for each state.

Buffers inherited from the parent class are used for actual data storage.

The actual initialization of a new class instance is performed in the Init method, which carefully configures all parameters.

bool CNeuronAddToStack::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint stack_size, uint dimension, uint variables,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, stack_size * dimension * variables,
                                                                   optimization_type, batch))
      return false;
//---
   activation = None;
   iStackSize = stack_size;
   iDimension = dimension;
   iVariables = variables;
//---
   return true;
  }

The algorithm for this method is fairly simple and straightforward. First, we pass control to the method of the same name in the parent class, initializing all inherited interfaces. In this case, we set the size of the result buffer to be large enough to hold the entire stack. After it completes successfully, we store the stack parameters in internal variables. Finally, the method returns true, confirming successful configuration.

After the object has been successfully initialized, the next step is to implement the forward pass algorithm. As in our other modules, the entire computation process has been moved to the OpenCL context to enable acceleration and parallel data processing. To achieve this, a new AddToStack kernel was added to the OpenCL program; it performs a sequential shift of stack elements, ensuring that a new state is added and historical data is correctly preserved.

__kernel void AddToStack(__global const float* inputs,
                         __global float* stack,
                         const int stack_size)
  {
   const size_t id = get_global_id(0);
   const size_t loc_id = get_local_id(1);
   const size_t var = get_global_id(2);
   const size_t dimension = get_global_size(0);
   const size_t total_loc = get_local_size(1);
   const size_t variables = get_global_size(2);

In its parameters, the kernel accepts pointers to two data buffers: new states (inputs) and the current stack, stack. In addition, the user specifies the stack size, stack_size.

In the kernel body, we first initialize the identifiers and sizes of the task space:

  • id — the global index of an element in the state description vector of a single unit sequence;
  • loc_id — the local index in the work-group,
  • var — the index of a unit sequence.

Next, we determine the number of inner-loop iterations required for the work-group elements to process the entire stack sequence, in order to properly distribute computations and manage parallelism.

   const int total = (stack_size - 1) / total_loc;

In general, we expect that a single iteration of the loop will be sufficient. However, adding a loop makes the algorithm universal, without limiting the stack size to the parameters of the device being used.

We run the main loop from end to beginning to sequentially shift the stack elements, starting with the one farthest away and ending with the one closest to the current state.

   for(int i = total; i >= 0; i--)
     {
      int inp = 0;
      if(i == 0 && loc_id == 0)
         inp = IsNaNOrInf(inputs[RCtoFlat(var, id, variables, dimension, 1)], 0);
      else
         if((i * total_loc + loc_id) < stack_size)
           {
            int shift = RCtoFlat(i * total_loc + loc_id - 1, id, stack_size, dimension, var);
            inp = IsNaNOrInf(stack[shift], 0);
           }
      BarrierLoc

In the body of the loop, we first read data from the global data buffers. For the very first state, we retrieve data from the new data buffer. All other elements read the values previously saved in the stack, shifted by one state. In doing so, we must verify the correctness of the obtained values and synchronize the work-group threads so that all shift calculations are completed before new values are written. This prevents conflicts when the state stack is updated concurrently.

As the final step, we write the prepared value to the current position in the state stack.

      if((i * total_loc + loc_id) < stack_size)
        {
         int shift = RCtoFlat(i * total_loc + loc_id, id, stack_size, dimension, var);
         stack[shift] = inp;
        }
     }
  }

Thus, the new element is added, and the preceding elements are shifted correctly, preserving the sequence of historical states.

This algorithm enables dynamic state stack updates with parallel processing, allowing STE-FlowNet to analyze the state sequence in real time without compromising performance or data correctness.

Using both global and local identifiers allows data arrays to be processed efficiently and the state stack dimensionality to be managed. Thus, the OpenCL implementation provides high performance and scalability when working with dynamic financial market time series.

In our class, we will create a wrapper method for this kernel using the familiar pattern.

bool CNeuronAddToStack::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !OpenCL)
      return false;
//---
   if(NeuronOCL.Neurons() < int(iDimension * iVariables))
      return false;
//---
   uint global_work_offset[3] = {0};
   uint global_work_size[3] = { iDimension,
                                MathMin((uint)OpenCL.GetMaxLocalSize(1), iStackSize),
                                iVariables
                              };
   uint local_work_size[3] = { 1, global_work_size[1], 1 };
   uint kernel = def_k_AddToStack;
   setBuffer(kernel, def_k_ats_inputs, NeuronOCL.getOutputIndex())
   setBuffer(kernel, def_k_ats_stack, getOutputIndex())
   setArgument(kernel, def_k_ats_stack_size, (int)iStackSize)
   kernelExecuteLoc(kernel, global_work_offset, global_work_size, local_work_size)
//---
   return true;
  }

Please note that to specify the work-group size, we use the minimum value between the user-specified stack size and the maximum possible work-group size for the device in use. This ensures optimized memory usage and parallelism.

Adding data to the stack addresses only the forward pass. However, this is not enough for the model to be fully trainable — the error gradients must be correctly propagated back to the input data. In our case, the state stack class itself does not contain any trainable parameters: it merely stores and shifts states. Nevertheless, it is important that the error signal reach the element that produced the current state.

In practice, we implement this as follows. During training, gradients are passed only to the current (top) element of the state stack. This does not require writing a new OpenCL kernel. It is sufficient to use the existing de-concatenation operation on the object's shared output error buffer. We extract the gradient segment corresponding to the position of the current state and send it to the input data source.

bool CNeuronAddToStack::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !OpenCL)
      return false;
   if(NeuronOCL.Neurons() < int(iDimension * iVariables))
      return false;
   if(!DeConcat(NeuronOCL.getGradient(), PrevOutput, Gradient, iDimension,
                               iDimension * (iStackSize - 1), iVariables))
      return false;
   Deactivation(NeuronOCL)
//---
   return true;
  }

After that, all that remains is to adjust the resulting values by the derivative of the activation function of the input data object.

This approach is simple, deterministic, and resource-efficient. No additional computational kernels are needed, and the backpropagation logic remains local and straightforward.

Correlation Object

Now that we have a reliable object for organizing the state stack, it makes sense to move on to the next step — building the correlation module. In the spirit of the authors’ original work, we will evaluate similarity between states using the scalar product (dot product) of the corresponding descriptor vectors. However, it is important to clarify the context. Instead of working with fixed frames, we apply this measure in the context of unit sequences — short, semantically coherent blocks of events that are easy to process in real time.

The scalar-product approach is simple, fast, and interpretable. For two vectors, it provides a direct estimate of the directionality of their projections in feature space, which, in our interpretation, is equivalent to estimating the alignment of the directions of forces in the field. Working with unit sequences offers an additional advantage: we preserve the local coherence of the context while simultaneously limiting the computational load, which is critical for the online analysis of infinite data streams.

We implement the proposed approach within the new CNeuronStackCorrelation class.

class CNeuronStackCorrelation :  public CNeuronBaseOCL
  {
protected:
   CNeuronAddToStack cStack;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronStackCorrelation(void);
                    ~CNeuronStackCorrelation(void);
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint stack_size, uint dimension, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronStackCorrelation;   }
   //--- 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      Clear(void) override;
  };


In the structure of the new class presented here, the key element is the internal cStack object, which is responsible for managing the state history. It is created statically, so the class's constructor and destructor can be left empty. There is no need to manually allocate or release resources here. The process of initializing a new object has been completely moved into the Init method.

bool CNeuronStackCorrelation::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint stack_size, uint dimension, uint variables,
                                   ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, variables * stack_size, optimization_type, batch))
      return false;

As usual, the procedure begins by calling the method of the same name in the parent class, where the algorithm for initializing the inherited interfaces has already been configured.

It is important to note here that the operation of multiplying two vectors returns a scalar value. Therefore, we specify the size of the results buffer as the product of the number of unit sequences and the stack depth.

If this stage is successful, we immediately initialize the internal cStack object by passing it a reference to the current OpenCL context and the parameters that define the stack structure.

   if(!cStack.Init(0, 0, OpenCL, stack_size, dimension, variables, optimization, iBatch))
      return false;
//---
   return true;
  }

Thus, both components are connected to the same computing environment simultaneously, and the stack is ready to receive the first data as soon as the initialization method completes, without any additional calls or hidden deferred operations. In fact, everything is initialized up front and explicitly, without lazy logic or side effects, which is critical for reproducible behavior during training and debugging.

The next step in our work is to construct the forward pass algorithm, which in this module is extremely logical and strictly follows the concept of stream analysis.

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

First, we update the state stack by passing into it the state received from the input data object. If, for any reason, the stack cannot be updated, execution is immediately interrupted: the forward pass does not allow ambiguous intermediate states.

Next comes the key step: calculating the correlation between the current state and the history. The MatMul matrix multiplication operation is used for this purpose. The constructed stack is used as the left matrix, and the current state as the right matrix. The result is written to the Output result buffer.

   if(!MatMul(cStack.getOutput(), NeuronOCL.getOutput(), Output, cStack.GetStackSize(),
              cStack.GetDimension(), 1, cStack.GetVariables(), true))
      return false;

It should be noted here that, in the context of analyzing unit sequences, we multiply the fixed-history matrix by the current state vector. As a result, we obtain a similarity vector for the current observation with respect to each element of the stack, forming a complete map of the local causal-temporal context.

If necessary, an activation function can be applied directly to the resulting values.

   if(activation != None)
      if(!Activation(Output, Output, activation))
         return false;
//---
   return true;
  }

Thus, the forward pass is built according to the principle Memory → Comparison → Interpretation. The stack is updated. Correlations are calculated. The result is optionally converted to the required format. Everything is concise, strictly sequential, and fully compatible with real-time streaming data processing.

Although this algorithm does not contain any trainable parameters of its own, it is a full-fledged component of the overall computational chain and must therefore correctly support backpropagation. Otherwise, the model loses its ability to adapt, and the entire training process turns into a static scheme with no feedback. That is precisely why the calcInputGradients method implements the distribution and processing of gradients among all elements of the structure.

bool CNeuronStackCorrelation::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

In the first step, the validity of the pointer to the input data object is verified. Without a valid pointer, further operations are meaningless.

Next, the gradient distribution function is invoked via a matrix multiplication operation, which computes the gradients based on the results of the previous forward pass.

   if(!MatMulGrad(cStack.getOutput(), cStack.getGradient(),
                  NeuronOCL.getOutput(), cStack.getPrevOutput(),
                  Gradient, cStack.GetStackSize(),
                  cStack.GetDimension(), 1, cStack.GetVariables(), true))
      return false;

Here, the sensitivity of each state in the state stack to the error accumulated at the output is recalculated. Essentially, this step allows us to determine which of the previous states had the greatest impact on the current result.

If an activation function is specified in the input data object, we adjust the gradient distribution to account for nonlinear transformations.

   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), cStack.getPrevOutput(),
                       cStack.getPrevOutput(), NeuronOCL.Activation()))
         return false;

Next, we pass the error gradients of the current state along the backbone of our stack.

   if(!NeuronOCL.CalcHiddenGradients(cStack.AsObject()))
      return false;

The final step is to accumulate the gradients passed through the two information streams at the level of the input data object.

   if(!SumAndNormilize(NeuronOCL.getGradient(), cStack.getPrevOutput(),
                       NeuronOCL.getGradient(), cStack.GetDimension(), false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

Thus, the method provides full support for the backward pass, transforming the correlation module from a simple analytical tool into an active component of the trainable architecture. It carefully collects and redistributes error information, laying the foundation for effective self-learning across the entire system.


Encoder

We have reached one of the key stages. All the core building blocks of the STE-FlowNet Encoder are now ready, and it is time to integrate them into a cohesive architecture. At this stage, we create the top-level class CNeuronSTEFlowNetEncoder, which acts as a conductor and links together the recurrent line, the flow dynamics module, and the correlation mechanism.

class CNeuronSTEFlowNetEncoder   :  public CNeuronSpikeConvGRU
  {
protected:
   CLayer            cRecurrentLine;
   CLayer            cFlow;
   CLayer            cCorrelation;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSTEFlowNetEncoder(void);
                    ~CNeuronSTEFlowNetEncoder(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 CNeuronBaseOCL* GetLayer(uint layer);
   virtual uint      Layers(void)   const { return (cRecurrentLine.Total() + 1) / 3; }
   //--
   virtual int       Type(void)   override const   {  return defNeuronSTEFlowNetEncoder;   }
   //--- 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;
  };

Three internal components are declared within the class: cRecurrentLin, cFlow, and cCorrelation. These are dynamic arrays of pointers to objects that are responsible, respectively, for processing temporal sequences, extracting flow information, and evaluating local dependencies between states.

A new object is initialized in the Init method, which plays a key role in constructing the Encoder's entire computational pipeline. Its task is to assemble a cascade of recurrent, correlation, and residual blocks, which together form flow-based data processing.

bool CNeuronSTEFlowNetEncoder::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;

It all starts with verifying that the input parameters are consistent. The size of the array of channel counts must match the array of analyzed elements in the sequence and indicate the number of internal blocks in the Encoder. Otherwise, further assembly is not possible.

After that, the method of the same name in the parent class is called; it sets the fundamental parameters of the last layer and serves as the anchor point for the entire architecture.

   const uint layers = chanels.Size() - 1;
   if(!CNeuronSpikeConvGRU::Init(numOutputs, myIndex, open_cl, units[layers], chanels[layers] + stack_size,
                                 chanels[layers], optimization_type, batch))
      return false;

Next, three containers are cleared and prepared — for the recurrent line, the flow component, and the correlation modules. These containers will store the blocks being created.

   CNeuronSSAMResNeXtBlock*   conv_res = NULL;
   CNeuronStackCorrelation*   correl = NULL;
   CNeuronSpikeConvGRU*       gru = NULL;
   CNeuronBaseOCL*            neuron = NULL;
//---
   cRecurrentLine.Clear();
   cFlow.Clear();
   cCorrelation.Clear();
   cRecurrentLine.SetOpenCL(OpenCL);
   cFlow.SetOpenCL(OpenCL);
   cCorrelation.SetOpenCL(OpenCL);

Next, the main loop starts, in which the structure of the Encoder is built step by step. At each iteration, a residual SSAM-ResNeXt block is added first; it strengthens local dependencies and allows the attention mechanism to identify meaningful patterns.

   uint index = 0;
   for(uint i = 0; i <= layers; i++)
     {
      conv_res = new CNeuronSSAMResNeXtBlock();
      if(!conv_res ||
         !conv_res.Init(0, index, OpenCL, chanels[i], chanels[i + 1],
                        units[i], units[i + 1], group_size, groups, heads,
                        dimension_k, optimization, iBatch) ||
         !cRecurrentLine.Add(conv_res))
        {
         DeleteObj(conv_res)
         return false;
        }

This is followed by a base neuron, which is used to concatenate the values of the main flow with the correlation coefficients between the states being analyzed.

      index++;
      neuron = new CNeuronBaseOCL();
      if(!neuron ||
         !neuron.Init(0, index, OpenCL, units[i + 1] * (chanels[i + 1]*stack_size), optimization, iBatch) ||
         !cRecurrentLine.Add(neuron))
        {
         DeleteObj(neuron)
         return false;
        }

If the layer is not the last one, a recurrent GRU module is inserted into the chain to integrate temporal dependencies and account for signal dynamics.

      if(i == layers)
         break;
      //---
      if(i < layers - 1)
        {
         index++;
         gru = new CNeuronSpikeConvGRU();
         if(!gru ||
            !gru.Init(0, index, OpenCL, units[i + 1], chanels[i + 1] + stack_size, chanels[i + 1],
                                                                          optimization, iBatch) ||
            !cRecurrentLine.Add(gru))
           {
            DeleteObj(gru)
            return false;
           }
        }

Next, a correlation block is added, which is responsible for analyzing inter-channel relationships and stack representations.

      index++;
      correl = new CNeuronStackCorrelation();
      if(!correl ||
         !correl.Init(0, index, OpenCL, stack_size, chanels[i + 1], units[i + 1], optimization, iBatch) ||
         !cCorrelation.Add(correl))
        {
         DeleteObj(correl)
         return false;
        }

Each iteration is completed by another SSAM-ResNeXt block, but now within the context of the flow backbone, which creates a parallel processing path and strengthens multichannel connections.

      index++;
      conv_res = new CNeuronSSAMResNeXtBlock();
      if(!conv_res ||
         !conv_res.Init(0, index, OpenCL, chanels[i + 1], chanels[i + 2],
                        units[i + 1], units[i + 2], group_size, groups, heads,
                        dimension_k, optimization, iBatch) ||
         !cFlow.Add(conv_res))
        {
         DeleteObj(conv_res)
         return false;
        }
     }
//---
   return true;
  }

Thus, the method gradually builds a branched architecture:

  • a recurrent sequence of ResNeXt blocks, base neurons, and GRU modules;
  • a correlation line for analyzing relationships between channels;
  • a flow branch that strengthens spatial dependencies.

As a result, the Encoder can simultaneously account for temporal dynamics, identify local patterns, and analyze multidimensional relationships, making it a robust and flexible tool for processing complex data.

The feedForward method is responsible for organizing the forward pass and essentially brings the entire architecture assembled during initialization to life. The input data are passed in as the initial state, after which they begin to move along the recurrent line.

bool CNeuronSTEFlowNetEncoder::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL *recur = NeuronOCL;
   CNeuronBaseOCL *flow = cRecurrentLine[0];
   CNeuronStackCorrelation *correl = NULL;
   CNeuronSSAMResNeXtBlock* resnext = NULL;
   int layers = (cRecurrentLine.Total() + 1) / 3;
//---
   for(int i = 0; i < layers; i++)
     {
      //--- ResNeXt
      if(!cRecurrentLine[i * 3] || !cRecurrentLine[i * 3].FeedForward(recur))
         return false;
      recur = cRecurrentLine[i * 3];

The first step in each iteration of the loop is to activate the ResNeXt block, which processes the current state and passes its pointer to the local variable recur. In parallel, a correlation module is run that operates on the flow and extracts intertemporal dependencies between states.

      //--- Correlation
      correl = cCorrelation[i];
      if(!correl ||
         !correl.FeedForward(flow))
         return false;

The results from these two branches are then combined. The output of ResNeXt and the correlation block are concatenated and fed to the next neuron. This makes it possible to combine local spatial features with information about the relationships between states.

      //--- Concatenate
      resnext = recur;
      recur = cRecurrentLine[i * 3 + 1];
      if(!recur ||
         !Concat(resnext.getOutput(), correl.getOutput(), recur.getOutput(), resnext.GetChannelsOut(),
                                                        correl.GetStackSize(), resnext.GetUnitsOut()))
         return false;

If the current layer is not the last one, a GRU cell is used, which adds global temporal dynamics and enables recurrent processing.

      if((i * 3 + 2) == cRecurrentLine.Total())
         break;
      //--- GRU
      if(!cRecurrentLine[i * 3 + 1] ||
         !cRecurrentLine[i * 3 + 2].FeedForward(recur))
         return false;
      recur = cRecurrentLine[i * 3 + 1];

After that, the cFlow flow branch is updated; it strengthens spatial dependencies and forms a parallel path for information transfer.

      if(!cFlow[i] ||
         !cFlow[i].FeedForward(flow))
         return false;
      flow = cFlow[i];
     }
//---
   return CNeuronSpikeConvGRU::feedforward(recur);
  }

Thus, at each iteration of the loop, the data pass through the sequence ResNeXt → Correlation → Concat → GRU → Flow, with each element making its own contribution.

When all layers have been processed, control is passed to the method of the same name in the parent class, which completes the forward pass. Ultimately, the method ensures coordinated data flow through the architecture's parallel paths, maintaining a balance between temporal dynamics, spatial patterns, and inter-channel correlations.

We've worked hard today and deserve a short break. In the next article, we will return to this topic with renewed energy and bring what we have started to its logical conclusion.


Conclusion

In this article, we have taken a step-by-step look at how the STE‑FlowNet encoder is built and how it works. We saw how individual modules come together to form a cohesive architectural whole. Analyzing the forward pass method also made it clear how these elements interact. The result is an architecture capable of extracting spatiotemporal patterns while taking hidden causal dynamics into account, capturing temporal memory via the state stack and instantly evaluating correlations between states.

In the next part, we will continue our work by moving on to practical testing: we will test the model on real historical data and evaluate its convergence rate, resilience to market noise, and the practical viability of the approach. It is at this stage that the key question will become clear: whether the proposed design proves its worth under live market conditions.

Links


Programs used in the 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 building 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/20039

Attached files |
MQL5.zip (3222.94 KB)
Integrating News Sentiment to Avoid Strategy Disruption in MQL5 Integrating News Sentiment to Avoid Strategy Disruption in MQL5
We present a dynamic news‑sentiment filter for MQL5 Expert Advisors. It uses WebRequest to query a REST API, parses JSON sentiment and confidence, and gates entries and exits alongside Moving Average and RSI signals. You get an example EA, integration steps, timer‑based updates, and Strategy Tester workarounds. The outcome is fewer trades against dominant news flow and earlier risk reduction when sentiment flips.
Real-Time Polynomial Regression: A Causal Savitzky-Golay Implementation for MQL5 Real-Time Polynomial Regression: A Causal Savitzky-Golay Implementation for MQL5
We adapt the Savitzky–Golay filter to a strictly causal, real-time form for MQL5 and derive the fixed coefficients for end-point evaluation. The article provides a full indicator and an Expert Advisor, explains the edge-evaluation noise sensitivity, and implements a multi-bar confirmation safeguard. Backtests on several symbols are shown without cherry-picking. You get ready-to-compile code, parameter guidance, and a reduced-lag alternative to moving averages.
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.
Machine Learning Under Constraint (Part 4): The Stop-Loss Ceiling Meets the Optimal Trading Rule Search Machine Learning Under Constraint (Part 4): The Stop-Loss Ceiling Meets the Optimal Trading Rule Search
The article constrains the optimal trading rule by a prop-firm stop-loss ceiling and corrects OU fitting by pooling within-trade pairs per AFML eq. 13.6. We apply the ceiling as a feasibility mask on a shared PT–SL mesh, ensuring the constrained set is a subset of the baseline. Practical guidance covers when the ceiling binds, the Sharpe cost, and how exposure and live budget state affect the bound.