Русский Español Português
preview
Neural Networks in Trading: Multi-Task Learning Based on the ResNeXt Model

Neural Networks in Trading: Multi-Task Learning Based on the ResNeXt Model

MetaTrader 5Trading systems |
2 960 9
Dmitriy Gizlyk
Dmitriy Gizlyk

Introduction

The rapid development of artificial intelligence has led to the active integration of deep learning methods into data analysis, including the financial sector. Financial inputs are characterized by high dimensionality, heterogeneity, and temporal structure, which complicates the application of traditional processing methods. At the same time, deep learning has demonstrated high efficiency in analyzing complex and unstructured data.

Among modern convolutional architectures, there's the one that stands out: ResNeXt introduced in "Aggregated Residual Transformations for Deep Neural Networks". ResNeXt is capable of capturing both local and global dependencies and effectively handling multidimensional data while reducing computational complexity through grouped convolutions.

A key area of financial analysis using deep learning is multi-task learning (MTL). This approach allows simultaneous solutions to multiple related tasks, improving model accuracy and generalization capability. Unlike classical approaches where each model addresses a single task, MTL leverages shared data representations, making the model more robust to market fluctuations and enhancing the training process. This approach is particularly valuable for market trend forecasting, risk assessment, and asset valuation, as financial markets are dynamic and influenced by numerous factors.

The study "Collaborative Optimization in Financial Data Mining Through Deep Learning and ResNeXt" introduced a framework for integrating the ResNeXt architecture into multi-task models. This solution opens new possibilities for processing time series, identifying spatiotemporal patterns, and generating accurate forecasts. ResNeXt's grouped convolutions and residual blocks accelerate training and reduce the risk of losing critical features, making this method especially relevant for financial analysis.

Another significant advantage of the proposed approach is the automation of extracting meaningful features from raw data. Traditional financial analysis often requires extensive feature engineering, whereas deep neural networks can autonomously identify key patterns. This is particularly important when analyzing multimodal financial data, which involves multiple sources such as market indicators, macroeconomic reports, and news publications. MTL's flexibility allows dynamic adjustment of task weights and loss functions, improving the model's adaptability to market changes and improving forecasting accuracy.


ResNeXt Architecture

The ResNeXt architecture is based on a modular design and grouped convolutions. At its core are convolutional blocks with residual connections, governed by two key principles:

  • If the output feature maps have the same size, blocks use identical hyperparameters (width and filter sizes).
  • If the feature map size is reduced, the block width is proportionally increased.

Adhering to these principles maintains roughly constant computational complexity across all model layers, simplifying design. It is enough to defining a single template module, while other blocks are generated automatically, ensuring standardization, easier tuning, and streamlined architectural analysis.

Standard neurons in artificial neural networks perform a weighted sum of inputs, the primary operation in convolutional and fully connected layers. This process can be divided into three stages: splitting, transformation, and aggregation. ResNeXt introduces a more flexible approach, allowing transformation functions to be more complex or even mini-models themselves. This gives rise to the Network-in-Neuron concept, expanding the architecture's capabilities through a new dimension: cardinality. Unlike width or depth, cardinality determines the number of independent, complex transformations within each block. Experiments show increasing cardinality can be more effective than increasing depth or width, providing a better balance between performance and computational efficiency.

All ResNeXt blocks share a uniform bottleneck structure. It consists of:

  • An initial 1×1 convolutional layer that reduces feature dimensionality,
  • A main 3×3 convolutional layer performing core data processing,
  • A final 1×1 convolutional layer restoring the original dimensionality.

This design reduces computational complexity while maintaining high model expressiveness. Additionally, residual connections preserve gradients during training, preventing vanishing gradients, which is a key factor for deep networks.

A major enhancement in ResNeXt is the use of Grouped Convolutions. Here, input data are divided into multiple independent groups, each processed by separate convolutional filters, with the results subsequently aggregated. This reduces model parameters, maintains high network throughput, and improves computational efficiency without significant accuracy loss.

To maintain stable computational complexity when changing the number of groups, ResNeXt adjusts the width of bottleneck blocks, controlling the number of channels in internal layers. This allows scalable models without excessive computational overhead.

The multi-task learning framework based on ResNeXt represents a progressive approach to financial data processing, addressing shared feature utilization and cooperative modeling across various analytical tasks. It consists of three key structural components:

  • Feature extraction module,
  • Shared learning module,
  • Task-specific output layers.

This approach integrates efficient deep learning mechanisms with financial time series, delivering high forecasting accuracy and adapting the model to dynamic market conditions.

The feature extraction module relies on the ResNeXt architecture, effectively capturing both local and global characteristics of financial data. In multidimensional financial data, the critical parameter is the number of groups in the model. It balances detailed feature representation with computational cost. Each grouped convolution in ResNeXT identifies specific patterns in channel groups, which are then aggregated into a unified representation.

After passing through nonlinear transformation layers, extracted features form the basis for subsequent multi-task learning and task-specific adaptation. The shared learning module employs a weight-sharing mechanism that projects common features into task-specific spaces. This ensures the model can generate individual representations for each task while avoiding interference and simultaneously achieving high feature-sharing efficiency. Clustering tasks based on correlations further enhances the shared learning mechanism.

Task-specific output layers consist of fully connected perceptrons that project specialized features into the final prediction space. Output layers can adapt to the nature of each task. In particular, classification tasks use cross-entropy loss, while regression tasks use mean squared error (MSE). For multi-task learning, the final loss function is represented as a weighted sum of individual task losses.

Training occurs in multiple stages. Initially, models are pre-trained on individual tasks to ensure effective convergence of specialized MLPs. The model is then fine-tuned in the multi-task architecture to improve overall performance. Optimization is performed using the Adam algorithm with dynamic learning rate adjustment.



Implementation Using MQL5

After reviewing the theoretical aspects of the ResNeXt-based multi-task learning framework, we proceed to implement our interpretation using MQL5. We'll begin with the construction of the basic ResNeXt architectural blocks - bottleneck modules.

Bottleneck Module


The bottleneck module consists of three convolutional layers, each performing a critical role in processing raw data. The first layer reduces feature dimensionality, lowering the computational complexity of subsequent processing.

The second convolutional layer performs the main convolution, extracting complex high-level features necessary for accurate interpretation of the data. It analyzes interdependencies among elements, identifying patterns critical for later stages. This approach enables the model to adapt to nonlinear dependencies in financial data, improving forecast accuracy.

The final layer restores the original tensor dimensionality, preserving all significant information. Dimensionality reduction along the temporal axis in earlier feature extraction is compensated by expanding the feature space, consistent with ResNeXt principles.

To stabilize training, each convolutional layer is followed by batch normalization. It reduces internal covariate shift and accelerates convergence. ReLU activation enhances model nonlinearity, improving its ability to capture complex dependencies and generalization quality.

The above architecture is implemented within the CNeuronResNeXtBottleneck object that has the following structure.

class CNeuronResNeXtBottleneck :  public CNeuronConvOCL
  {
protected:
   CNeuronConvOCL          cProjectionIn;
   CNeuronBatchNormOCL     cNormalizeIn;
   CNeuronTransposeRCDOCL  cTransposeIn;
   CNeuronConvOCL          cFeatureExtraction;
   CNeuronBatchNormOCL     cNormalizeFeature;
   CNeuronTransposeRCDOCL  cTransposeOut;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronResNeXtBottleneck(void){};
                    ~CNeuronResNeXtBottleneck(void){};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint chanels_in, uint chanels_out, uint window,
                          uint step, uint units_count, uint group_size, uint groups,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronResNeXtBottleneck;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual CLayerDescription* GetLayerInfo(void) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

As the parent class, we use a convolutional layer object, which performs the function of restoring the feature space. Additionally, the structure includes a number of internal objects, each assigned a key role in the algorithms we are constructing. Their functionality will be detailed as we build the methods of the new class.

All internal objects are declared as static, allowing us to keep the class constructor and destructor empty. The initialization of these declared and inherited objects is performed in the Init method. This method receives a set of constants that unambiguously define the architecture of the module being created.

bool CNeuronResNeXtBottleneck::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint chanels_in, uint chanels_out, uint window,
                                   uint step, uint units_count, uint group_size, uint groups,
                                   ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   int units_out = ((int)units_count - (int)window + (int)step - 1) / (int)step + 1;
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, group_size * groups, group_size * groups,
                                              chanels_out, units_out, 1, optimization_type, batch))
      return false;

In the method body, we usually call the parent class method having the same name, which contains initialization algorithms for inherited objects and interfaces. However, in this case, the parent class functions as the final convolutional layer of the module. Its input receives data after feature extraction, during which the dimensionality of the processed tensor may have changed. Therefore, we first determine the sequence length at the module output and only then call the parent class method.

After successfully initializing inherited objects, we proceed to the newly declared objects. Work begins with the data projection block. The first convolutional layer prepares projections of the raw data for the required number of working groups.

//--- Projection In
   uint index = 0;
   if(!cProjectionIn.Init(0, index, OpenCL, chanels_in, chanels_in, group_size * groups,
                                                    units_count, 1, optimization, iBatch))
      return false;
   index++;
   if(!cNormalizeIn.Init(0, index, OpenCL, cProjectionIn.Neurons(), iBatch, optimization))
      return false;
   cNormalizeIn.SetActivationFunction(LReLU);

These projections are then normalized and activated using the LReLU function.

Note that the result of these operations is a three-dimensional tensor [Time, Group, Dimension]. To implement independent processing of individual groups, we move the group identifier dimension to the first position using a 3D tensor transposition object.

   index++;
   if(!cTransposeIn.Init(0, index, OpenCL, units_count, groups, group_size, optimization, iBatch))
      return false;
   cTransposeIn.SetActivationFunction((ENUM_ACTIVATION)cNormalizeIn.Activation());

Next is the feature extraction block. Here, we use a convolutional layer specifying the number of groups as the number of independent sequences. This ensures that the values of individual groups are not "mixed". Each group uses its own matrix of trainable parameters.

//--- Feature Extraction
   index++;
   if(!cFeatureExtraction.Init(0, index, OpenCL, group_size * window, group_size * step, group_size,
                                                           units_out, groups, optimization, iBatch))
      return false;

Additionally, note that the method receives parameters for the convolution window size and its step along the temporal dimension. When passing these parameters to the internal convolutional layer initialization, we multiply the corresponding values by the group size.

After the convolutional layer, we add batch normalization with the LReLU activation function.

   index++;
   if(!cNormalizeFeature.Init(0, index, OpenCL, cFeatureExtraction.Neurons(), iBatch, optimization))
      return false;
   cNormalizeFeature.SetActivationFunction(LReLU);

The final feature space backward projection block consists solely of a 3D tensor transposition object that merges the groups into a single sequence. The actual data projection, as mentioned earlier, is performed using the inherited methods of the parent class.

//--- Projection Out
   index++;
   if(!cTransposeOut.Init(0, index, OpenCL, groups, units_out, group_size, optimization, iBatch))
      return false;
   cTransposeOut.SetActivationFunction((ENUM_ACTIVATION)cNormalizeFeature.Activation());
//---
   return true;
  }

We now just need to return the logical result of the operations to the caller and complete the object initialization method.

The next step is building the feed-forward pass algorithm, implemented in the feedForward method.

bool CNeuronResNeXtBottleneck::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Projection In
   if(!cProjectionIn.FeedForward(NeuronOCL))
      return false;

The method receives a pointer to the raw data object, which is immediately passed to the method of the same name in the first internal convolutional layer of the data projection block. We do not check the validity of the pointer, as this verification is already handled within the internal layer, making an additional check redundant.

Projection results are normalized and transposed into separate group representations.

   if(!cNormalizeIn.FeedForward(cProjectionIn.AsObject()))
      return false;
   if(!cTransposeIn.FeedForward(cNormalizeIn.AsObject()))
      return false;

In the feature extraction block, grouped convolution operations are performed, and the results are normalized.

//--- Feature Extraction
   if(!cFeatureExtraction.FeedForward(cTransposeIn.AsObject()))
      return false;
   if(!cNormalizeFeature.FeedForward(cFeatureExtraction.AsObject()))
      return false;

Extracted features from individual groups are transposed back into a single multidimensional sequence and projected into the designated feature space using the parent class.

//--- Projection Out
   if(!cTransposeOut.FeedForward(cNormalizeFeature.AsObject()))
      return false;
   return CNeuronConvOCL::feedForward(cTransposeOut.AsObject());
  }

The logical result of these operations is returned to the calling program, and the method concludes.

As you can see, the feed-forward algorithm is linear. Error gradients propagate linearly as well. Therefore, backpropagation methods are left for independent study. The complete code for this object and all its methods is provided in the attachment.

Residual Connections Module


The ResNeXt architecture features residual connections for each Bottleneck module, which facilitate efficient error gradient propagation during backpropagation. These connections allow the model to reuse previously extracted features, improving convergence and reducing the risk of gradient vanishing. As a result, the model can be trained to greater depths without significantly increasing computational costs.

It is important to note that the output tensor of a Bottleneck module maintains approximately the same overall size, but individual dimensions may change. A reduction in temporal steps is compensated by an increase in feature space dimensionality, preserving critical information and capturing long-term dependencies. A separate projection module ensures proper integration of residual connections by mapping input data to the required dimensions. This prevents mismatches and maintains training stability even in deep architectures.

In our implementation, we created this module as the CNeuronResNeXtResidual object, whose structure is shown below.

class CNeuronResNeXtResidual:  public CNeuronConvOCL
  {
protected:
   CNeuronTransposeOCL     cTransposeIn;
   CNeuronConvOCL          cProjectionTime;
   CNeuronBatchNormOCL     cNormalizeTime;
   CNeuronTransposeOCL     cTransposeOut;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronResNeXtResidual(void){};
                    ~CNeuronResNeXtResidual(void){};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint chanels_in, uint chanels_out,
                          uint units_in, uint units_out,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronResNeXtResidual;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual CLayerDescription* GetLayerInfo(void) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

When developing this object, we used approaches similar to those used in constructing Bottleneck modules but adapted to the different dimensions of the input tensor.

In the presented object structure, you can see several nested objects - their functionality will be described during the implementation of the new class methods. All internal objects are declared statically. This allows us to leave the class's constructor and destructor empty. Initialization of all objects, including inherited ones, is performed in the Init module, which receives a set of constants defining the object architecture.

bool CNeuronResNeXtResidual::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                  uint chanels_in, uint chanels_out,
                                  uint units_in, uint units_out,
                                  ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, chanels_in, chanels_in, chanels_out,
                            units_out, 1, optimization_type, batch))
      return false;

Within the method, we first call the parent class method of the same name, passing the necessary parameters. As with Bottleneck modules, we use a convolutional layer as the parent class. It also handles the projection of data into the new feature space.

After successful initialization of inherited objects and interfaces, we move on to working with the newly declared objects. To convenient working with temporal dimensions, we first transpose the input data.

   int index=0;
   if(!cTransposeIn.Init(0, index, OpenCL, units_in, chanels_in, optimization, iBatch))
      return false;

Next, a convolutional layer projects individual unit sequences into the specified dimensionality.

   index++;
   if(!cProjectionTime.Init(0, index, OpenCL, units_in, units_in, units_out, chanels_in, 1, optimization, iBatch))
      return false;

The results are normalized, similar to the Bottleneck module. However, no activation function is applied, as the residual module must pass all information without loss.

   index++;
   if(!cNormalizeTime.Init(0, index, OpenCL, cProjectionTime.Neurons(), iBatch, optimization))
      return false;

We then adjust the feature space. For this, we perform inverse transposition. The projection is then handled using the parent class.

   index++;
   if(!cTransposeOut.Init(0, index, OpenCL, chanels_in, units_out, optimization, iBatch))
      return false;
//---
   return true;
  }

All that remains for us to do is return the logical result of the operations to the calling program and complete the work of the new object initialization method.

Next, we move on to constructing the feed-forward pass algorithm in the feedForward method.

bool CNeuronResNeXtResidual::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Projection Timeline
   if(!cTransposeIn.FeedForward(NeuronOCL))
      return false;

The method receives a pointer to the object containing the input data. This pointer is passed to the internal data transposition layer, which converts the data into unit sequences.

Next, we adjust the dimensionality of the unit sequences to the target size using a convolutional layer.

   if(!cProjectionTime.FeedForward(cTransposeIn.AsObject()))
      return false;

The results are normalized.

   if(!cNormalizeTime.FeedForward(cProjectionTime.AsObject()))
      return false;

Finally, inverse transposition is applied, and the data is projected into the feature space.

//--- Projection Chanels
   if(!cTransposeOut.FeedForward(cNormalizeTime.AsObject()))
      return false;
   return CNeuronConvOCL::feedForward(cTransposeOut.AsObject());
  }

The final projection is performed using the parent class. The logical result of the operations is then returned to the calling program, completing the feed-forward method.

The feed-forward pass algorithm is linear. Therefore, during backpropagation, we have a linear gradient flow. So, backpropagation methods are left for independent study, similar to the CNeuronResNeXtBottleneck object. The complete code for these objects and all their modules is provided in the attachment.

The ResNeXt Block


Above, we created separate objects representing the two information streams of the ResNeXt framework. Now it is time to combine these objects into a single structure, allowing more efficient data processing. For this purpose, we create the CNeuronResNeXtBlock object, which will serve as the main block for subsequent data processing. The structure of this object is presented below.

class CNeuronResNeXtBlock :  public CNeuronBaseOCL
  {
protected:
   uint                     iChanelsOut;
   CNeuronResNeXtBottleneck cBottleneck;
   CNeuronResNeXtResidual   cResidual;
   CBufferFloat             cBuffer;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronResNeXtBlock(void){};
                    ~CNeuronResNeXtBlock(void){};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint chanels_in, uint chanels_out, uint window,
                          uint step, uint units_count, uint group_size, uint groups,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronResNeXtBlock;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual CLayerDescription* GetLayerInfo(void) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

In this structure, we see familiar objects and a standard set of virtual methods, which we will need to override.

All internal objects are declared as static, allowing us to keep the class constructor and destructor empty. The initialization of these declared and inherited objects is performed in the Init method. Its parameter structure is entirely inherited from the CNeuronResNeXtBottleneck object.

bool CNeuronResNeXtBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                               uint chanels_in, uint chanels_out, uint window,
                               uint step, uint units_count, uint group_size, uint groups,
                               ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   int units_out = ((int)units_count - (int)window + (int)step - 1) / (int)step + 1;
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_out * chanels_out, optimization_type, batch))
      return false;

Within the method body, we first determine the sequence dimensionality at the block output, then initialize the base interfaces inherited from the parent object.

After successfully executing the parent class initialization method, we save the necessary parameters into the object's variables.

   iChanelsOut = chanels_out;

We initialize the internal objects of the previously constructed information streams.

   int index = 0;
   if(!cBottleneck.Init(0, index, OpenCL, chanels_in, chanels_out, window, step, units_count,
                       group_size, groups, optimization, iBatch))
      return false;
   index++;
   if(!cResidual.Init(0, index, OpenCL, chanels_in, chanels_out, units_count, units_out, optimization, iBatch))
      return false;

At the block output, we expect to receive the sum of the values from the two information streams. Therefore, the received error gradient can be fully propagated to both data streams. To avoid unnecessary data copying, pointers to the corresponding data buffers are swapped.

   if(!cResidual.SetGradient(cBottleneck.getGradient(), true))
      return false;
   if(!SetGradient(cBottleneck.getGradient(), true))
      return false;
//---
   return true;
  }

Finally, we return a boolean result to the calling program and complete the initialization method.

Next, we build the feed-forward pass algorithms in the feedForward method. Everything is quite simple here.

bool CNeuronResNeXtBlock::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cBottleneck.FeedForward(NeuronOCL))
      return false;
   if(!cResidual.FeedForward(NeuronOCL))
      return false;

The method receives a pointer to the input data object, which is immediately passed to the methods of the two information stream objects. The results are then summed and normalized.

   if(!SumAndNormilize(cBottleneck.getOutput(), cResidual.getOutput(), Output, iChanelsOut, true, 0, 0, 0, 1))
      return false;
//--- result
   return true;
  }

The logical result of the operations is then returned to the calling program, completing the feed-forward method.

Although the structure may appear simple at first glance, it actually includes two information streams, which adds complexity to error gradient distribution. The algorithm responsible for this process is implemented in the calcInputGradients method.

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

The method receives a pointer to the input data object used during the feed-forward pass. In this case, the error gradient must be propagated according to the influence of the input data on the final model output. Data can only be passed to a valid object. Therefore, before continuing operations, we first check the relevance of the received pointer.

After passing this verification, the error gradient is propagated through the first information stream.

   if(!NeuronOCL.calcHiddenGradients(cBottleneck.AsObject()))
      return false;

Before propagating the gradient through the second stream, the previously obtained data must be preserved. Rather than fully copying the data, we use a pointer-swapping mechanism. The pointer to the error gradient buffer of the input data is saved in a local variable.

   CBufferFloat *temp = NeuronOCL.getGradient();

Next, we check if the auxiliary buffer matches the gradient buffer dimensions. Adjust, if necessary.

   if(cBuffer.GetOpenCL() != OpenCL ||
      cBuffer.Total() != temp.Total())
     {
      if(!cBuffer.BufferInitLike(temp))
         return false;
     }

Its pointer is then passed to the input data object.

   if(!NeuronOCL.SetGradient(GetPointer(cBuffer), false))
      return false;

Now the error gradient can be safely propagated through the second information stream without risk of data loss.

   if(!NeuronOCL.calcHiddenGradients(cResidual.AsObject()))
      return false;

The values from the two streams are summed, and the buffer pointers are restored to their original state.

   if(!SumAndNormilize(temp, NeuronOCL.getGradient(), temp, 1, false, 0, 0, 0, 1))
      return false;
   if(!NeuronOCL.SetGradient(temp, false))
      return false;
//---
   return true;
  }

Then we return the logical result of the operation to the caller and complete the error gradient distribution method.

This concludes the overview of the algorithmic construction of the ResNeXt block object methods. The complete code for this object and all its methods is provided in the attachment.

We have now reached the end of this article, but our work is not yet complete. We will pause briefly and continue in the next article.



Conclusion

In this article, we introduced a multi-task learning framework based on the ResNeXt architecture, designed for processing financial data. This framework enables efficient feature extraction and processing, optimizing classification and regression tasks in high-dimensional and time-series data environments.

In the practical section, we constructed the main elements of the ResNeXt architecture. In the next article, we will build the multi-task learning framework and evaluate the effectiveness of the implemented approaches on real historical data.

References


Programs used in the article

#NameTypeDescription
1Research.mq5Expert AdvisorExpert Advisor for collecting examples
2ResearchRealORL.mq5
Expert Advisor
Expert Advisor for collecting samples using the Real-ORL method
3Study.mq5Expert AdvisorModel training Expert Advisor
4Test.mq5Expert AdvisorModel testing Expert Advisor
5Trajectory.mqhClass librarySystem state and model architecture description structure
6NeuroNet.mqhClass libraryA library of classes for creating a neural network
7NeuroNet.clLibraryOpenCL program code

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

Attached files |
MQL5.zip (2430.99 KB)
Last comments | Go to discussion (9)
Edgar Akhmadeev
Edgar Akhmadeev | 16 Jan 2026 at 21:25
Alain Verleyen #:
In my experience, traders who can share something really useful never share anything.

Yes, they know (like me since 1998) that a working strategy quickly stops working after distribution.

That's why forum programmers share individual solutions, while a working (profitable) strategy has never been published. Or sold.

lynxntech
lynxntech | 16 Jan 2026 at 21:29
Edgar Akhmadeev #:

Yes, they know (as I have since 1998) that a strategy that works quickly stops working once it is disseminated.

That's why forum programmers share individual solutions, and a working (profitable) strategy has never been published. Or sold.

and the need to transfer funds between countries doesn't count anymore?)

How can you be such a system?

A trading robot will always work if you buy on a pullback, the question is where is the pullback?

lynxntech
lynxntech | 16 Jan 2026 at 21:47
I've seen the translation, I'm definitely not translatable.
Edgar Akhmadeev
Edgar Akhmadeev | 16 Jan 2026 at 22:21
lynxntech #:
I've seen the translation, I'm definitely not translatable

I have to admit, I wasn't smart enough to understand the original.

"I've been talking to myself all night, and they didn't understand me!" (Zhvanetsky

Vitaly Muzichenko
Vitaly Muzichenko | 17 Jan 2026 at 00:40
Edgar Akhmadeev #:
Yes, they know (as I have since 1998) that a strategy that works quickly ceases to work once it is disseminated.

This applies to exchanges with limited liquidity, it does not apply to forex, there is enough liquidity there for everyone

P.S. I remembered Mikhail, he has a system of hedging on the Moscow Exchange, he shared it and it works, and it should work in the future. Everything depends on personal capital, and there is nothing to do there with 100 dollars.

Here, everyone is looking for a system for a hundred quid, and profitability of 10% per day. That's why such results of searches.

Automating Trading Strategies in MQL5 (Part 44): Change of Character (CHoCH) Detection with Swing High/Low Breaks Automating Trading Strategies in MQL5 (Part 44): Change of Character (CHoCH) Detection with Swing High/Low Breaks
In this article, we develop a Change of Character (CHoCH) detection system in MQL5 that identifies swing highs and lows over a user-defined bar length, labels them as HH/LH for highs or LL/HL for lows to determine trend direction, and triggers trades on breaks of these swing points, indicating a potential reversal, and trades the breaks when the structure changes.
The MQL5 Standard Library Explorer (Part 5): Multiple Signal Expert The MQL5 Standard Library Explorer (Part 5): Multiple Signal Expert
In this session, we will build a sophisticated, multi-signal Expert Advisor using the MQL5 Standard Library. This approach allows us to seamlessly blend built-in signals with our own custom logic, demonstrating how to construct a powerful and flexible trading algorithm. For more, click to read further.
Introduction to MQL5 (Part 30): Mastering API and WebRequest Function in MQL5 (IV) Introduction to MQL5 (Part 30): Mastering API and WebRequest Function in MQL5 (IV)
Discover a step-by-step tutorial that simplifies the extraction, conversion, and organization of candle data from API responses within the MQL5 environment. This guide is perfect for newcomers looking to enhance their coding skills and develop robust strategies for managing market data efficiently.
The View component for tables in the MQL5 MVC paradigm: Base graphical element The View component for tables in the MQL5 MVC paradigm: Base graphical element
The article covers the process of developing a base graphical element for the View component as part of the implementation of tables in the MVC (Model-View-Controller) paradigm in MQL5. This is the first article on the View component and the third one in a series of articles on creating tables for the MetaTrader 5 client terminal.