Русский
preview
Neural Networks in Trading: A Spiking Architecture for Spatio-Temporal Market Analysis (Conclusion)

Neural Networks in Trading: A Spiking Architecture for Spatio-Temporal Market Analysis (Conclusion)

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

Introduction

The market has always followed its own rules, moving rapidly and unevenly, with constant shifts in pace and sentiment. Every candle on the chart, every spike in the tick stream — each is a small event in a vast mechanism that never stops. What new ideas, one might ask, could possibly be offered in a field where dozens of analytical approaches have already been tried? A field where slow linear models have long since given way to deep neural networks, and modern transformers confidently uncover patterns in multidimensional processes? But financial time series, like the nature of human behavior, continue to stubbornly resist being fully structured. They remain as unpredictable as ever, filled with noise, broken dependencies, and asynchronous dynamics that challenge even the most powerful algorithms.

It is precisely against this backdrop that the need arises, time and again, for models capable of working not just with data, but with a stream of events. Not with smoothed candlesticks, but with real-time changes, instantaneous fluctuations, and specific market reactions. Traditional models — even those based on recurrent neural networks or transformers — often fail to capture an important part of market dynamics because they are forced to fit those dynamics into fixed time intervals. The financial environment is much closer to being event-driven; it breathes unevenly — speeding up and slowing down, breaking out of familiar time intervals.

In this sense, spiking architectures have proven to be conceptually closer to the nature of markets than one might have initially assumed. They react to events, generate spikes, and rely on temporal differences, shaping computation into a form that is surprisingly in tune with market chaos. A spiking model does not sample the data once per second. It reacts when there is a reason to react. It does not interpolate empty gaps but skips them, conserving resources and focusing on what really matters.

It was this idea that led us to explore SDformerFlow — an architecture originally designed to process event streams from cameras, yet remarkably close in spirit to the task of analyzing tick data. The framework's authors proposed an elegant synthesis of spiking neurons and attention mechanisms, enhancing both approaches. Attention has become more compact and energy-efficient, and the spiking dynamics have become more structured and predictable. The SDformerFlow architecture does not just transform a stream of events into meaningful representations — it does so efficiently, consistently, and with a clear internal structure. It is precisely this combination of properties that makes it so appealing for financial applications.

Looking back at the model's key elements, the central role belongs to the SpatioTemporal Swin SpikeFormer Encoder (STSF) — the heart of the system — which combines convolutional data preprocessing with windowed attention mechanisms and, layer by layer, builds a representation of the structure of the information being analyzed. In the original paper, this module was responsible for extracting the spatiotemporal structure of the optical flow. Small blocks of events were grouped into windows. The windows were shifted so as to cover the entire scene. And then, based on these, a generalized description of the dynamics was developed.

When we apply this approach to trading, we maintain the same general principle. Small segments of a time series — local events, spikes in activity, trend accelerations, and shifts in volatility — play the same role as patches of pixels in an event map in video processing tasks. Financial dynamics are broken down into overlapping windows that allow the model to view the movement from different angles, without overlooking either local or global structures.

This is where one of SDformerFlow's key advantages — its adaptability — comes into play. Unlike a classical transformer, where attention is directed to all positions at once — resulting in enormous computational costs — the window mechanism allows for a detailed analysis of local dependencies without overloading the system. At the same time, these shifted windows allow the model to periodically shake up its perception and see the big picture. In markets where local patterns often dominate over global structures, this hybrid approach proves particularly valuable.

The spiking component of the architecture completes the picture. It allows the model to respond not to absolute values, but to changes. This aligns perfectly with the nature of tick data. A spiking neuron is activated only when a significant change occurs. During periods of inactivity, it remains silent, creating no unnecessary computational load. This approach simultaneously reduces computational costs and improves resistance to noise — after all, noise is the main problem with financial time series.

The author's visualization of the SDformerFlow framework is shown below.

The author's visualization of the SDformerFlow framework

When we began porting SDformerFlow to MQL5, it became clear that the model's structure naturally lent itself to computational optimization. Its window-based mechanisms and clearly event-driven nature make it possible to parallelize computations. And spiking activity is a natural way to reduce the number of operations. That is precisely why, in previous articles, we implemented the basic building blocks — including spiking attention, residual structures, and modular components — needed to assemble a fully functional encoder block. All of this has laid the foundation for our current work, in which we are moving from individual elements to a cohesive architecture.

We will continue to develop the architecture, gradually integrating all the logic of SDformerFlow into a single, functional framework capable of analyzing the market with the precision of an event-driven model and the efficiency of modern transformers.


U-block

In the previous article, we completed the development of the SDSA block — the central component of the STSF encoder, which has become a sort of intellectual center of gravity for our model. It was in this block that we combined window-based attention, spiking dynamics, and a residual structure to create a mechanism capable of capturing the hidden temporal geometry of the event-driven stream. Now that this block has taken its final form, the next logical step is to move on to building the decoder, since without it, the model remains half-mute. It is capable of understanding the structure of the input data, but it is not yet able to transform its internal representations into a meaningful output signal.

When we take another look at the original SDformerFlow architecture, we notice an interesting detail. The structure of the decoder's components mirrors the architectural design of the CNeuronSpikeConvBlock spiking convolutional block that we have already implemented. This similarity is no coincidence, but rather a deliberate design feature of the model. The authors chose not to create a separate, unconventional design for the decoder. Instead, they used a compact, energy-efficient mechanism that works well and incorporates spiking dynamics and convolutional processing.

This works in our favor. We have already covered the entire process, from low-level implementation to optimizing the convolutional block's compute kernels using MQL5 and OpenCL. We have verified that the module can handle the load, responds correctly to events, and produces stable outputs. That means the architectural foundation is ready, and we do not need to build a new block from scratch.

There is a certain beauty to that. The architecture remains seamless, with no artificial breaks between the encoder and the decoder. All the key elements speak the same language — the language of event-driven convolutions, spiking activation, and consistent scales. This not only simplifies implementation but also improves the model's stability. Blocks built on the same principles interact with one another in a predictable way, and internal representations easily make the transition from encoding to reconstruction.

The next step in our work is to construct a fully-fledged U-shaped architecture that integrates the encoder and decoder into a single multilayer mechanism. In the author's interpretation of SDformerFlow, this part of the model looks fairly traditional. The encoder consists of several layers of SDSA blocks connected by SPM modules (Spiking Patch Merge). The data stream then passes sequentially through a cascade of spiking convolutional blocks that make up the decoder. Direct horizontal connections are established between them so that each level of the decoder can receive supplementary information. Thus, the input to each layer of the decoder consists of a concatenated tensor containing the outputs of the previous decoding block and the corresponding layer of the encoder. This approach has long proven its effectiveness in tasks where it is important to combine global coverage with local sensitivity, allowing models to extract structure and reconstruct it at various scales.

However, in our work, we decided to take a slightly different approach. Engineering practice suggests that breaking down the architecture into too many separate objects makes it difficult to implement and maintain the model. This is especially true when dealing with deep structures with multiple levels of nesting, where each layer depends on its neighbor and produces a representation at its own scale. Therefore, instead of implementing the encoder and decoder in separate branches and connecting them manually, we opted for a more compact and elegant solution — building a nested architecture, similar to a matryoshka doll. In this structure, each level contains two components: an encoder and a decoder, connected by a neck.

This approach makes it possible to represent the model as a series of self-similar modules, each of which reflects the principles of a U-shaped structure, but on a smaller scale. The top level serves as a global framework, and within it lies the next level — exactly the same, but more compact. This continues until, at the deepest level of nesting, we reach the MS-ResBlock — the final element that serves as the core of our structure and completes the architectural sequence. This solution offers us several advantages at once.

First, this gives us natural symmetry: the encoder and decoder are structured symmetrically at each level and share the context provided by the neck. Second, this approach significantly simplifies the management of internal tensors. Data is passed strictly according to the nesting logic, reducing the likelihood of errors and cross-mismatches. Third, in terms of implementation, this proves to be much more convenient — each module becomes not just a block, but an independent structural unit that is easy to manage. And one that is easy to expand, replace, or modify.

But the most important advantage lies in the consistency of the computational flow. In a classic U-shaped architecture, data flow constantly switches between different branches, requiring careful management of all connections. In our nested approach, this complexity disappears. Transitions occur within the block's unified space, which makes the architecture more cohesive and stable. In fact, we obtain a unique variant of a U-shaped network, where each level is an independent node containing all the necessary components — an SDSA encoder, a spiking convolutional decoder, and a neck connecting them, represented by the nested U-structure at the next level.

The final level, where an MS-ResBlock replaces a further nested structure, will handle the full depth of the computational logic and serve as the center of the final transformation. This block is well-suited to serve as the deepest node. It is stable and compact, has high throughput, and can carefully aggregate signals from all upper levels, laying the foundation for the decoder's return path. Thus, our recursive architecture preserves the spirit of the original SDformerFlow while adapting to the realities of event-driven markets and the characteristics of MQL5 — it is compact, hierarchical, and features a high degree of structural repetition.

To implement the nested U-shaped architecture, we needed a new structural unit capable of integrating multiple levels of computation and ensuring end-to-end interaction between the block's encoder, decoder, and neck. These requirements led us to create a separate object, CNeuronSDformerUBlock — the central element that will serve as a building block for our nesting-doll structure. This object encapsulates the logic for bidirectional data flow, data merging, processing, and subsequent reconstruction.

class CNeuronSDformerUBlock   :   public CNeuronSpikeConvBlock
  {
protected:
   CLayer            cEncoder;
   CNeuronBaseOCL*   cNeck;
   CNeuronBaseOCL    cConcat;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSDformerUBlock(void) {};
                    ~CNeuronSDformerUBlock(void) { DeleteObj(cNeck) };
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units, uint &windows[], uint &heads[],
                          uint &layers[], uint dimension_k,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const  {  return defNeuronSDformerUBlock;   }
   //--- 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;
   virtual CBufferFloat*   GetStack(void);
  };

Conceptually, this object builds on the spiking convolutional modules we have already implemented, so inheriting from CNeuronSpikeConvBlock is not only natural but also extremely useful. This helps maintain consistency in the computational interface, reuse proven mechanisms, and avoid code duplication. However, the new class significantly extends the functionality of the base block, transforming it from a simple processing module into a full-fledged component of the U-shaped architecture.

The object's structure reflects its conceptual role. Three key elements coexist within it. The first is the encoder block, cEncoder, which transforms data along the descending path and extracts structural features at the current nesting level. The second is the variable cNeck, which represents the neck — that is, a nested U-block at the next level. It is precisely this mechanism that creates the hierarchical structure of the architecture. Each block contains another, more compact block, until the structure terminates in an MS-ResBlock at the lowest nesting level. The third element is the cConcat module, which correctly concatenates the tensors from the encoder and the previous level's decoder, with the latter received from the neck. We implement the decoder's functionality using the inherited methods of the parent class.

The initialization method acts as a kind of architect for the entire U-shaped structure. This is where the abstract scheme — specifying the number of nesting levels, the number of SDSA-blocks at each level, and the window and attention-head parameters — becomes an actual set of interacting objects.

bool CNeuronSDformerUBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                 uint units, uint &windows[], uint &heads[],
                                 uint &layers[], uint dimension_k,
                                 ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   uint block = layers.Size();
   if(block <= 0 ||
      block > heads.Size() ||
      (block + 1) > windows.Size())
      return false;

The layers array, passed as a method parameter, determines the depth of the nesting doll, specifying how many times the structure will recursively drill down into itself before finally reaching an MS-Resblock at the lowest level.

The method begins by checking parameter consistency: the number of levels must match the sizes of the corresponding attention-head and window arrays, since each stage of the U-structure must rely on a precisely defined set of parameters. If the numbers do not add up, there is no point in continuing construction.

Once the received data is verified as correct, control is passed to the method of the same name in the parent class. It is used to configure the main parameters of the decoding module: window sizes, the number of channels, and key optimization parameters.

   if(!CNeuronSpikeConvBlock::Init(numOutputs, myIndex, open_cl, 2 * windows[1], 2 * windows[1],
                                   windows[0], units, 1, optimization_type, batch))
      return false;

Next, an encoder is constructed — a linear sequence of SDSA blocks whose number is specified by layers[0]. Each SDSA block is created dynamically, assigned its own index within the level, and immediately added to the cEncoder container.

   cEncoder.Clear();
   cEncoder.SetOpenCL(OpenCL);
//---
   uint index = 0;
   CNeuronSDSA* sdsa = NULL;
   for(uint i = 0; i < layers[0]; i++)
     {
      sdsa = new CNeuronSDSA();
      if(!sdsa ||
         !sdsa.Init(0, index, OpenCL, units, windows[0], heads[0], dimension_k, optimization, iBatch) ||
         !cEncoder.Add(sdsa))
        {
         DeleteObj(sdsa)
         return false;
        }
      index++;
     }

This sequence forms the input branch of the level, where the computational load increases as the data moves down the U-structure.

The next step is to add a Spiking Patch Merge object. It merges spatial patches and reduces dimensionality, forming a narrowing point before the neck component.

   CNeuronSpikeConvBlock* spm = new CNeuronSpikeConvBlock();
   if(!spm ||
      !spm.Init(0, index, OpenCL, 2*windows[0], 2*windows[0], 2*windows[1], (units+1)/2, 1, optimization, iBatch) ||
      !cEncoder.Add(spm))
     {
      DeleteObj(spm)
      return false;
     }
   index++;

The logic for selecting the neck depends on the nesting depth. If the current level is the last one, it is sufficient to create an MS-Resblock. This block completes the descending branch and begins the ascending part, where spatial structures are restored and refined.

   if(block == 1)
     {
      CNeuronMSRes* neck = new CNeuronMSRes();
      if(!neck ||
         !neck.Init(0, index, OpenCL, units, windows[1], 1, optimization, iBatch))
        {
         DeleteObj(neck)
         return false;
        }
      cNeck = neck;
     }

However, if the depth is greater than one, the neck is a nested U-block. Truncated arrays of parameters are created for this block by omitting the first element of each array. This is because each subsequent level must use its own local window sizes, number of attention heads, and number of SDSA layers.

   else
     {
      uint new_layers[], new_windows[], new_heads[];
      if(ArrayResize(new_layers, block - 1) < 0 ||
         ArrayResize(new_windows, block) < 0 ||
         ArrayResize(new_heads, block - 1) < 0)
         return false;
      if(ArrayCopy(new_heads, heads, 0, 1, block - 1) < (int(block) - 1) ||
         ArrayCopy(new_layers, layers, 0, 1, block - 1) < (int(block) - 1) ||
         ArrayCopy(new_windows, windows, 0, 1, block) < int(block))
         return false;

A recursive call creates a new CNeuronSDformerUBlock, essentially repeating the same logic, but for the next layer of the nesting doll.

      CNeuronSDformerUBlock* neck = new CNeuronSDformerUBlock();
      if(!neck ||
         !neck.Init(0, index, OpenCL, units, new_windows, new_heads,
                    new_layers, dimension_k, optimization, iBatch))
        {
         DeleteObj(neck)
         return false;
        }
      cNeck = neck;
     }
   index++;

The final component is the cConcat object, which concatenates the tensors from the decoder and the parallel encoder layer. Its initialization must account for the change in dimensionality after SPM and the neck, which is reflected in the parameter that uses the second-level window.

   if(!cConcat.Init(0, index, OpenCL, 2 * windows[1]*units, optimization, iBatch))
      return false;
   cConcat.SetActivationFunction(None);
//---
   return true;
  }

The concatenator is a key component of the ascending part that enables the model to reconstruct details using both deep abstractions and high-level features.

Once all elements have been successfully initialized, the structure is considered fully constructed. Each component knows its place, the parameters are consistent, and the connections have been established. Recursive nesting transforms logical abstraction into a hierarchical computational mechanism capable of folding and unfolding signals in complex ways.

At the same time, the forward pass algorithm looks surprisingly clean and well-coordinated, just like a well-rehearsed scene. Every participant knows exactly when to go on stage. Despite the nested nature of the U-blocks, the code remains compact and easy to read. And the logic of the computational flow is sequential and intuitive.

bool CNeuronSDformerUBlock::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL *prev = NeuronOCL;
   CNeuronBaseOCL *curr = NULL;
   for(int i = 0; i < cEncoder.Total(); i++)
     {
      curr = cEncoder[i];
      if(!curr ||
         !curr.FeedForward(prev))
         return false;
      prev = curr;
     }

The forward pass begins with encoder traversal. A chain of SDSA blocks and a final SPM is formed to process the input data sequentially. Each object receives the output of the previous one as input, gradually condensing the representation and passing it further down the U-structure. This part functions as a descending branch — the signal gradually loses redundant spatial detail, transforming into a more concentrated and informative representation.

Next comes the neck element, which becomes a key turning point in the computational path. Whether it is the final MS-Resblock or a nested U-block, the neck object receives the already somewhat compressed features and continues to transform them. At this stage, the data reaches the lowest point of the nested structure, after which an upward movement begins.

   if(!cNeck ||
      !cNeck.FeedForward(prev))
      return false;

Next, the outputs from the neck and the upper branch of the encoder are concatenated. This step effectively restores the U-shaped nature of the structure. The model combines deep features obtained after successive compressions with their shallower counterparts, preserving both generalized information and important details. Concatenation forms an extended tensor that serves as the starting point for the decoder and is implemented strictly in accordance with the window sizes and the number of channels, which ensures that the data is assembled correctly.

   if(!Concat(prev.getOutput(), cNeck.getOutput(), cConcat.getOutput(), GetWindow() / 2,
                                                           GetWindow() / 2, GetUnits()))
      return false;

The final step is to pass the combined tensor to the decoder, whose role is fulfilled by the parent class — the spiking convolutional block. It completes the forward pass, generating the final representation at this level of the U-structure and ensuring the connectivity of the entire recursive chain.

   if(!CNeuronSpikeConvBlock::feedForward(cConcat.AsObject()))
      return false;
//---
   return true;
  }

The result is an elegant and well-balanced algorithm in which each part of the U-block is seamlessly integrated into the overall computation flow. Despite its deep recursion and multi-layered nature, the structure retains its architectural purity. Its operational logic remains entirely transparent.

And now a natural question arises: How does this complex, multilayered structure learn? How does an error propagate through nested levels without getting lost or compromising the stability of the entire architecture? The answer to these questions lies in the gradient calculation mechanism implemented in the calcInputGradients method.

Everything here is neatly arranged — as if it were the epilogue to a well-directed play, where every line is delivered at just the right moment and in just the right order. The method acts as the conductor of the backward pass. It carefully retraces the forward pass, distributes errors across the nested levels, and sends the signal back to where it is needed to update the weights.

bool CNeuronSDformerUBlock::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !cNeck)
      return false;
//---
   if(!CNeuronSpikeConvBlock::calcInputGradients(cConcat.AsObject()))
      return false;
   

First comes a simple but vital check that the input context and neck node objects exist — without them, there is no point in continuing.

Next, the method of the same name in the parent class is called, which will propagate the error gradients through our decoder module down to the concatenated tensor of the neck and encoder outputs.

Next comes careful deconcatenation — DeConcat splits the overall gradient into two components: one that goes back to the neck, and one that returns to the encoder.

if(!DeConcat(cConcat.getPrevOutput(), cNeck.getGradient(), cConcat.getGradient(),
                GetWindow() / 2, GetWindow() / 2, GetUnits()))
      return false;
   Deactivation(cNeck)

This is followed by deactivation of the neck block — processing the derivative of the activation function, a crucial step for correctly propagating the error through the spiking dynamics. This is the very moment when the model recalls exactly which inputs it responded to and adjusts the weights based on the sensitivity of the activations.

The backward pass through the encoder begins with the SPM block. For this block, hidden gradients with respect to the neck are computed first, followed by backward deactivation and summation with the gradient received from the decoder.

   CNeuronBaseOCL* curr = cEncoder[-1];
   if(!curr ||
      !curr.CalcHiddenGradients(cNeck))
      return false;
   if(!DeActivation(curr.getOutput(), cConcat.getPrevOutput(), cConcat.getPrevOutput(), curr.Activation()) ||
      !SumAndNormilize(curr.getGradient(), cConcat.getPrevOutput(), curr.getGradient(), GetWindow() / 2,
                                                                                          false, 0, 0, 0, 1))
      return false;

After that, the signal propagates sequentially through the remaining encoder blocks in reverse order. Each block receives a gradient from the next one and computes its own internal hidden gradients, paving the way for weight updates.

   for(int i = cEncoder.Total() - 2; i >= 0; i--)
     {
      curr = cEncoder[i];
      if(!curr ||
         !curr.CalcHiddenGradients(cEncoder[i + 1]))
         return false;
     }

The final chord is the transfer of the accumulated gradient to the input data object.

   if(!NeuronOCL.CalcHiddenGradients(curr))
      return false;
//---
   return true;
  }

This call completes the backward pass within the U-block and allows higher-level structures to continue propagating errors along the training chain. As a result, the method follows a strict order of operations and is robust against structural errors. At the same time, it is flexible — it correctly handles both a terminal MS-Resblock and a recursively nested U-block in the neck. This design minimizes the likelihood of gradient mismatches between levels and preserves readability even with deep nesting — a quality that is particularly valuable when implementing complex spiking architectures.

Thus, CNeuronSDformerUBlock serves as a sort of central hub for the entire structure. The object integrates components into a single whole, elevating the architecture to a new level of abstraction and transforming a complex set of local operations into a coordinated mechanism for building a deep U-shaped structure. This object implements the core logic governing end-to-end data flow. The complete code for the class and all of its methods is provided in the attachment.


Top-level Object

At this stage, we have essentially reached the logical conclusion of the implementation. There is one final step left — building a top-level object that will act as a sort of conductor for the entire model. This object defines the overall data flow: from the event sequences being analyzed, through a sequence of nested U-blocks, to the generation of the final result.

This object does more than simply combine previously created components. It ensures that their interactions are coordinated. It ensures the correct sequence of forward and backward passes. It provides a single point of control for training parameters. As a result, all layers of the model function as a single entity: they exchange information synchronously, combine multiscale features, and maintain computational stability.

In our implementation, the CNeuronSDformerFlow object serves as the crowning element of the entire SDformerFlow architecture — the top-level component that integrates the solutions developed so far and defines the model's unified operational logic. If CNeuronSDformerUBlock can be thought of as a building block or a nesting doll, then CNeuronSDformerFlow is the framework that holds the entire structure together and keeps it in sync.

Inheriting from CNeuronSDformerUBlock gives the object all the benefits of a nested U-shaped structure. Building on this, CNeuronSDformerFlow adds another layer of control and coordination: the cFlow container aggregates all incoming data streams, forming a single channel for processing them and passing them down the hierarchy. This ensures consistency in computations at all levels of nesting and simplifies the management of training and weight updates.

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

public:
                     CNeuronSDformerFlow(void) {};
                    ~CNeuronSDformerFlow(void) {};
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units, uint &windows[], uint &heads[],
                          uint &layers[], uint dimension_k,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const  {  return defNeuronSDformerFlow;   }
   //--- 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;
  };

The initialization function transforms the model's abstract architectural diagram into a fully functional object ready to process event-driven financial time series. At this stage, all key parameters are specified: the number of outputs, window sizes, the number of attention heads, the nesting depth, as well as optimization parameters and the batch size.

bool CNeuronSDformerFlow::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                               uint units, uint &windows[], uint &heads[],
                               uint &layers[], uint dimension_k,
                               ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(layers.Size() < 3)
      return false;
   if(heads.Size() != (layers.Size() - 2))
      return false;
   if(windows.Size() < (layers[0] + layers.Size() - 1))
      return false;

The method thoroughly checks the validity of the input arrays. There must be at least three layers; the array of attention heads must match the level structure; and the window sizes must ensure consistency across all blocks.

Next, the number of blocks needed to build the data preprocessing pipeline (cFlow) is determined, and new arrays, new_layers and new_windows, are created for correct passing to the parent U-block object.

   uint block = layers.Size() - 2;
   uint new_layers[], new_windows[];
   if(ArrayResize(new_layers, block) < 0 ||
      ArrayResize(new_windows, block + 1) < 0)
      return false;
   if(ArrayCopy(new_layers, layers, 0, 2, block) < int(block) ||
      ArrayCopy(new_windows, windows, 0, layers[0], block + 1) < int(block + 1))
      return false;
//---
   if(!CNeuronSDformerUBlock::Init(numOutputs, myIndex, open_cl, units, new_windows,
                          heads, new_layers, dimension_k, optimization_type, batch))
      return false;

This call builds out the main U-structure, including the encoder, neck, and decoder, ensuring consistency across the nested levels.

This is followed by the initialization of the cFlow container and the passing of a pointer to the OpenCL context.

   cFlow.Clear();
   cFlow.SetOpenCL(OpenCL);

Within cFlow, two types of blocks are created: spiking convolutional blocks (CNeuronSpikeConvBlock) and multiscale blocks with residual connections (CNeuronMSRes). First, a sequence of convolutional blocks is created for the first module; each block is initialized using the corresponding windows and channel count, then added to cFlow.

   CNeuronSpikeConvBlock* conv = NULL;
   uint index = 0;
   for(uint i = 0; i < layers[0]; i++)
     {
      conv = new CNeuronSpikeConvBlock();
      if(!conv ||
         !conv.Init(0, index, OpenCL, windows[i], windows[i], windows[i + 1], units, 1, optimization, iBatch) ||
         !cFlow.Add(conv))
        {
         DeleteObj(conv)
         return false;
        }
      index++;
     }

Next, the MS-Res blocks of the second module are created, adding depth to the network and processing features at a higher level of abstraction.

   CNeuronMSRes* msres = NULL;
   for(uint i = 0; i < layers[1]; i++)
     {
      msres = new CNeuronMSRes();
      if(!msres ||
         !msres.Init(0, index, OpenCL, units, windows[layers[0]], 1, optimization, iBatch) ||
         !cFlow.Add(msres))
        {
         DeleteObj(msres)
         return false;
        }
      index++;
     }
//---
   return true;
  }

Each block is created dynamically, checked to ensure it has been initialized correctly, and added to the processing pipeline. If an error occurs, the object is destroyed, which ensures stability and prevents memory leaks.

Upon completion of all operations, the method returns true, indicating that the object has been fully constructed and is ready for end-to-end data processing.

Thus, the initialization method creates the top-level CNeuronSDformerFlow object as a link between the recursive U-structure and subsequent computational pipelines, integrating all model components and preparing the system for practical use in the analysis of event-driven financial time series.

The forward pass algorithm is implemented linearly, which ensures the predictability of computations and the clarity of the model's structure. This is especially important when working with deeply nested U-blocks.

bool CNeuronSDformerFlow::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL* prev = NeuronOCL;
   CNeuronBaseOCL* curr = NULL;
   for(int i = 0; i < cFlow.Total(); i++)
     {
      curr = cFlow[i];
      if(!curr ||
         !curr.FeedForward(prev))
         return false;
      prev = curr;
     }
//---
   return CNeuronSDformerUBlock::feedForward(prev);
  }

All operations are performed strictly in order. First, the convolutional and MS-Res blocks in cFlow, are processed sequentially; then the method of the same name in the parent U-block is called, which carefully integrates the outputs of the encoder, neck, and decoder.

The backward pass is thus organized in a straightforward and transparent manner — the gradients follow the same chain as the data in the forward pass. We encourage you to explore the details of the backpropagation and weight update algorithms on your own to see how a linear structure simplifies the management of a complex recursive architecture.

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

Essentially, this is the final node that completes the SDformerFlow architectural chain and transforms the framework's theoretical principles into a fully functional, ready-to-use system.


Testing

In this project, we are developing a fully-fledged trading system capable of making decisions and executing trades on its own in the financial market. It operates smoothly, precisely, and confidently, like a trader who knows every twist and turn of the chart.

The model's architecture was largely carried over from previous work without any major changes, which made it possible to retain time-tested signal processing and learning algorithms. The main innovations involved the environment encoder, into which the top-level object CNeuronSDformerFlow was integrated. This approach makes it easier to integrate the framework into the model, preserves modularity, and simplifies maintenance. The complete architecture description code is available in the attachment.

Training the model on historical data for the first time is like a novice trader's practice session. Think of EURUSD from January 2024 to June 2025 as a huge training ground. The model is like a young trader, closely scrutinizing every candlestick, analyzing volume, and looking for patterns between indicators and market reactions. SDformerFlow is both a microscope, allowing you to see the minutest movements of each candlestick, and a telescope, revealing global market trends. It generates an informative state for the Actor and the Critic, combines multiscale features, and learns alongside the model. With each new candlestick, the model learns to predict market movements and assess risks, gradually transforming from a timid learner into a confident practitioner capable of making independent decisions.

The real test comes during the online training phase in the MetaTrader 5 Strategy Tester. Here, there is no luxury of working calmly with historical data. Each candlestick is like a blow from the real market. The model responds quickly and accurately, analyzes noise fluctuations, adapts to sudden spikes, and adjusts its behavior during periods of low liquidity. SDformerFlow combines multiscale features, synchronizes the operation of all blocks, and learns alongside the model.

The final exam is a test on data from July through September 2025. There are no hints here, no historical data to draw on. Only the market and the model's own decisions. Every market move tests the model's preparedness: how well it has mastered the principles of analysis and learned to combine multiscale features and anticipate market reactions. The test results presented below demonstrate how SDformerFlow transforms a complex architecture into a cohesive, adaptive, and efficient trading system capable of performing almost like an experienced trader.

During testing, the model showed a capital increase from 100.0 USD to 133.36 USD — a steady gain of 33.36%. The balance and equity show steady growth with controlled drawdowns and no sharp declines. The maximum drawdown was 29.83 USD (25.36%), but the model recovered quickly, demonstrating its ability to adapt to market fluctuations.

The strategy demonstrated moderate profitability with a reasonable level of risk: a profit factor of 1.48, an average winning trade of 2.12 USD, and an average losing trade of -1.70 USD. Of the 86 trades, 47 were profitable (54.65%); long positions were profitable 53.52% of the time, and short positions 46.67% of the time. The longest winning streak was 9 trades, and the longest losing streak was 6. This confirms the model's stability.


Conclusion

The implemented SDformerFlow architecture has become a full-fledged tool for analyzing event-driven financial time series. The model simultaneously captures the slightest market movements and identifies global trends. Each candle and each signal pass through a multilevel U-shaped structure, where local and global features are combined, enriching the model's internal state and ensuring forecast accuracy.

The implementation of recursive U-blocks and the top-level CNeuronSDformerFlow object allowed us to create a single, cohesive organism. The forward and backward passes are coordinated, gradients and weights are updated synchronously, and the entire model is trained harmoniously. This structure ensures the stability of calculations, end-to-end information flow, and the ability to adapt to any market fluctuations, transforming the framework’s theoretical principles into a fully functional trading system.

Test results based on historical data confirm the effectiveness of this approach. Sustained capital growth, controlled drawdowns, and the model’s high adaptability demonstrate the practical value of SDformerFlow. The model shows that the combination of multilevel processing, the concatenation of multiscale features, and the synchronization of all blocks makes it a reliable foundation for building modern trading systems.


Links


Software used in this article

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


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

Attached files |
MQL5.zip (3366.79 KB)
Neural Networks in Practice: The Gradient Neural Networks in Practice: The Gradient
This article explains why and how the gradient is used in training a perceptron, based on the least squares method and the chain rule for computing partial derivatives. We will demonstrate how to implement gradient calculation in the C_Neuron class using the MQL5 language and test it using examples with 1 and 2 inputs. You'll learn how to adjust the weights and bias using gradient descent and prepare for forward propagation and backpropagation.
Self-Learning SuperTrend: An Adaptive Machine Learning-Based Trend Indicator Self-Learning SuperTrend: An Adaptive Machine Learning-Based Trend Indicator
The classic SuperTrend loses accuracy when the market regime changes because it uses fixed ATR parameters and a fixed multiplier. The article examines the architecture of ML SuperTrend Pro v2.00 written entirely in MQL5: a background test matrix with adaptive parameter updates, a regime grid as a context detector, an accuracy layer consisting of five filters, and Parabolic-style rendering with well-designed buffers. The article presents the principles of L1 regularization, the results of a comparison with the classic SuperTrend, and practical recommendations for deployment and integration via iCustom.
Neural Networks in Practice: The Perceptron Neural Networks in Practice: The Perceptron
In this article, the perceptron is presented as the basis of a neural network, and its implementation in MQL5 is described in detail. We will explain what activation functions and their derivatives are, the difference between forward propagation and backpropagation, and how the least squares loss function and the gradient are used. You will learn how to train weights and biases, verify the results using test scripts, and adjust the learning rate to achieve stable convergence.
Coyote Optimization Algorithm (COA) Coyote Optimization Algorithm (COA)
Introducing an MQL5 implementation of the Coyote Optimization Algorithm: packs with local alphas, a median-based tendency, and built-in crossover enable parallel exploration of regions of the search space while keeping premature convergence in check. The algorithm is integrated into C_AO and has been tested on the standard test bench and in a composite anti-cheat test. The article includes code, pseudocode, and an analysis of the operators that enable the use of COA to optimize the parameters of a trading system.