Русский Español Português
preview
Neural Networks in Trading: Heterogeneity-Informed Meta-Parameter Learning (Key Components)

Neural Networks in Trading: Heterogeneity-Informed Meta-Parameter Learning (Key Components)

MetaTrader 5Trading systems |
38 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introduction

In the previous article, we looked at the HimNet framework — a practical tool that not only analyzes data but also understands its context. In trading, this means that the model distinguishes between periods of high liquidity and periods of low activity. HimNet automatically adapts to these changes.

The HimNet architecture is simple and logical: time series segments of length T across N locations are fed as input. First, the model generates two types of trainable embeddings — temporal and spatial. Temporal embeddings (time of day, day of the week) capture cycles and regimes — morning spikes at market open, daytime dips, and nighttime lulls. Spatial embeddings — a separate vector for each location or ticker — encode the profile of the time series. All of these vectors are stored in dictionaries and are refined during the training process. They act as queries to pools of meta-parameters and allow the model to select different weights for different market contexts, as defined by the behavior of the graph convolution. This on-the-fly generation provides the flexibility of meta-learning without consuming vast amounts of memory and computational resources.

At the heart of HimNet are graph recurrent blocks (GCRU), enhanced with a Chebyshev basis. This architectural approach makes the model both deep and pragmatic. GCRU interprets the network of trading venues as a graph: each node represents a ticker or a venue, and the edges reflect correlation, cross-spread, or an empirical relationship. Chebyshev polynomials provide a compact implementation of K-hop aggregation: the model accounts for the influence of neighbors up to K edges away, but does so without computationally intensive spectral diagonalization — fast, local, and numerically stable. Simply put, instead of computing the full spectrum of the graph, the framework's authors build, step by step, a set of matrices that capture the influence of immediate neighbors, their neighbors, and so on, and then combine these effects while taking context into account.

Encoding in HimNet proceeds along two parallel information streams. The first (spatial encoder) scans the profile of trading venues: order-book depth, typical volume, and historical reactions to news. The second (temporal encoder) captures temporal regimes: the market open, the news flow, nighttime quiet, or release periods. Both encoders produce hidden representations that are combined into a unified latent tensor. This is not a mechanical summation, but a careful convolution of meanings: time dictates the degree of sensitivity, while space dictates the method of aggregation. As a result, we get a unified picture of the current market state.

The Decoder takes this unified representation and projects it into a spatiotemporal embedding, which serves as a Query to the ST meta-parameter pool. The pool returns a set of parameters specific to this particular combination of place and time, and these parameters directly form the weights of the GCRU block in the Decoder. In other words, the framework’s authors reassemble on the fly the structure of recurrent processing for the current regime: one set of weights and sensitivities at the session open, and another during the overnight period. The Decoder then iteratively, step by step, generates forecasts for a specified planning horizon.

From an algebraic and numerical perspective, for each state we compute K Chebyshev levels, concatenate the results, and pass them through a matrix generated by the meta-pool from the embedding query. The gradients flow back into both the pool parameters and the embeddings. Therefore, the model learns not only to make predictions, but also to identify useful spatiotemporal contexts. In practice, this provides an adaptation mechanism that allows the model to select the operating mode that is most relevant at that particular moment.

In real-world trading, this translates into tangible benefits. When the main session opens, HimNet automatically strengthens its parameters for a rapid response to volume spikes, which reduces slippage. During periods of low liquidity, the model makes more conservative predictions and reduces false entries, which protects against one-off spikes caused by individual trades. This means an appropriate widening of the spread when liquidity disappears and stricter filtering of false gaps. In the event of a sudden shock, the ST model switches to a defensive mode, increasing the robustness of forecasts and reducing the risk of market traps.

Technically, the architecture remains pragmatic and suitable for industrial use: the meta-parameter pools are compact and do not trigger explosive growth in memory and compute consumption, while computations over Chebyshev powers scale easily on GPU.

Hyperparameters essentially represent a reasonable compromise between model flexibility and response speed. They are selected through validation on representative market scenarios. Interpretability is not merely decorative here: from the activation of individual pool candidates and from heat maps of adaptive graphs, we can quickly see which regimes the model is currently identifying, why certain predictions were made, and which execution rules should be applied.

In practice, this translates into a concrete control tool — ranging from automatic triggers that switch to a conservative mode to transparent reports for risk management — which makes HimNet manageable and easy to understand for both traders and engineers.

Thus, HimNet combines powerful local aggregation, adaptive meta-learning, and practical engineering implementation. This isn't just an improved forecasting model — it's a tool that understands market regimes and can adapt its behavior accordingly, while remaining predictable and manageable for traders and risk managers.

The author’s visualization of the HimNet framework is shown below.

The practical section of the first article confirmed that the idea works in a real-world engineering environment. We offloaded the computationally intensive operations of constructing Chebyshev polynomials and error backpropagation to the GPU by implementing OpenCL kernels ChebStep and ChebStepGrad.

On the main program side, the CChebPolinom object was implemented, which wraps the kernels and provides a unified interface for inference and gradient computation. The result was a hybrid pipeline: fast training and reliable inference.

In this article, we continue our work on implementing the approaches of the HimNet framework using MQL5.



Graph Convolution Object

At the next stage of our work, we move on to creating a graph convolution object that will use the Chebyshev basis as the computational lens for K-hop aggregation. This module plays a key execution role: it accepts the prepared feature windows, requests from CChebPolinom the ready-made matrices 𝑇𝑘, and, based on the current meta-parameters, constructs output sequences ready to proceed further through the model. It is important that it be flexible, fast, and predictable at the same time: flexible by supporting various meta-parameterization modes, and fast and predictable through strict memory management and numerical checks.

The architecture of the new CNeuronHimNetGrapConv object is designed as a pragmatic and modular adapter between temporal feature windows and graph convolution — it combines data preparation, calls to matrix operations, and integration with OpenCL kernels, without cluttering the model’s high-level logic with implementation details.

class CNeuronHimNetGrapConv   :  public CNeuronTransposeRCDOCL
  {
protected:
   CNeuronBaseOCL    cX_G;

public:
                     CNeuronHimNetGrapConv(void) {};
                    ~CNeuronHimNetGrapConv(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint count, uint window, uint cheb_k,
                          ENUM_OPTIMIZATION optimization_type, uint batch) override;
   //---
   virtual bool      FeedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support);
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support);
   //---
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void)        const                      {  return defNeuronHimNetGrapConv; }
   virtual void      SetOpenCL(COpenCLMy *obj);
   virtual uint      GetCount(void) override const { return iWindow; }
   virtual uint      GetWindow(void) override const { return GetDimension(); }
   virtual uint      GetChebK(void) const { return iCount; }
  };

Inside the class is a compact but essential auxiliary object, `cX_G`. It acts as a working buffer and ensures rapid data exchange between the various stages of computation. Since the object is declared statically, the class’s constructor and destructor can be left empty — initialization and memory cleanup occur once over the entire lifecycle, without any unnecessary overhead when creating or destroying instances.

A new instance of the class is initialized in the Init method. In this method, we first call the method of the same name in the parent class, which defines the general interfaces of a neuron.

bool CNeuronHimNetGrapConv::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                 uint count, uint window, uint cheb_k,
                                 ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronTransposeRCDOCL::Init(numOutputs, myIndex, open_cl, cheb_k,
                                    count, window, optimization_type, batch))
      return false;
   if(!cX_G.Init(0, 0, OpenCL, Neurons(), optimization, iBatch))
      return false;
//---
   return true;
  }

Next, the auxiliary buffer cX_G is initialized; it serves as a workspace for the data that has already been concatenated and stores the result of the matrix multiplication. If any one of these operations fails, Init returns false, and the object remains in a safe state.

The forward-pass method FeedForward acts as a conductor. First, the method strictly checks the compatibility of the objects received as parameters: the passed-in NeuronOCL and Support must exist. In addition, Support must have the expected Dimension and Steps values matching the module settings.

bool CNeuronHimNetGrapConv::FeedForward(CNeuronBaseOCL *NeuronOCL,
                                        CChebPolinom *Support)
  {
   if(!NeuronOCL || !Support)
      return false;
   if(Support.GetDimension() != iWindow ||
      Support.GetSteps() != iCount)
      return false;
   if(NeuronOCL.Neurons() != (Neurons() / iCount))
      return false;

It also checks that the number of neurons in the source data object matches the expected shape. These checks are not just a formality; they ensure that the matrix operations below do not turn into a silent disaster on the GPU.

Next comes the key matrix multiplication operation, in which the resulting Chebyshev polynomials are multiplied by the tensor of the time series under analysis. The result of the operation is stored in our cX_G buffer.

if(!MatMul(Support.getOutput(), NeuronOCL.getOutput(), cX_G.getOutput(),
           iWindow, iWindow, GetDimension(), iCount, false))
   return false;

In essence, this is the very operation that constructs the X_G concatenation of the transformed representations T0 X, T1 X, …, Tk X. In a trading analogy: Support is a map of relationships between individual series (Chebyshev polynomials), NeuronOCL is a feature window of a multimodal time series, and cX_G is the already analyzed version of the raw data, where the influence of neighbors is taken into account for each unitary sequence. It is precisely this adjusted matrix that we need to pass on.

However, note that the result of the matrix multiplication operation is a sequence of several sets of adjusted source data with varying levels of detail {K-hop, N, C}. And this is not the data representation expected at the object's output. Therefore, as the final step of the forward-pass method, we pass cX_G to the method of the same name in the parent class, which transposes the tensor into the expected representation. This sequence makes the code modular, which simplifies testing and optimization.

If something goes wrong at any stage, the method cleanly returns false without leaving any side effects.

Once the forward pass has computed the layer’s response, the next stage — which is just as important — is error-gradient propagation. It is precisely here that the entire architecture’s ability to learn from historical data and adapt to real-world market scenarios is established.

The calcInputGradients method begins with a block that checks the received parameters: whether the pointers to the previous layer and to the Chebyshev polynomial matrix are valid. This check serves as a sort of safety net to prevent incorrect calculations in the event that the model structure becomes out of sync.

bool CNeuronHimNetGrapConv::calcInputGradients(CNeuronBaseOCL *NeuronOCL,
      CChebPolinom *Support)
  {
   if(!NeuronOCL || !Support)
      return false;
   if(Support.GetDimension() != iWindow ||
      Support.GetSteps() != iCount)
      return false;
   if(NeuronOCL.Neurons() != (Neurons() / iCount))
      return false;

Next, the method checks the dimensions: the number of steps (K-hop) and the window size must match expectations. This step is important because graph convolutions are sensitive to dimensional mismatches — an error here can trigger a chain reaction of incorrect gradients and derail the entire training process.

If the check passes, we proceed directly to the error-gradient distribution process. First, we transpose the gradients obtained from the subsequent layer of the model into the representation of our convolution outputs.

   if(!CNeuronTransposeRCDOCL::calcInputGradients(cX_G.AsObject()))
      return false;
   if(!MatMulGrad(Support.getOutput(), Support.getGradient(),
                  NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                  cX_G.getGradient(), iWindow, iWindow,
                  GetDimension(), iCount, false))
      return false;
//---
   return true;
  }

The final step involves distributing the error gradients between the input data and the Chebyshev polynomials. In this step, the method effectively distributes the accumulated effect of the error across the same channels through which the information propagated during the forward pass, but now in the opposite direction. We will use the MatMulGrad method, which is inherited from the parent object. As a result, we obtain two types of gradients. The first is the gradient with respect to the layer's input data itself, which will then be used to adjust the weights of the preceding layers. The second is with respect to the Chebyshev expansion coefficients, allowing the framework to adapt the graph-structured representation if market dependencies shift over time.

Thus, it is at this stage that the final debrief takes place: the system determines which connections proved useful and which were redundant, and lays the groundwork for the next optimization cycle.

From a practical standpoint, CNeuronHimNetGrapConv is where the model makes the signal audible to the trader. It converts raw quote windows into locally adjusted signals. The class architecture is specifically designed to ensure that these transformations are fast, reproducible, and explainable. The complete code for this class and all of its methods is provided in the attachment.


Recurrent Block

Now that we have examined the internal structure of the basic graph convolution block based on Chebyshev polynomials and seen how the streams of input data and error gradients are distributed within this relatively compact element, it is time to move up a level — to the node that links local graph processing with the dynamics of hidden states. This is where the model's actual memory is formed, and it is this component that ensures that information is not lost between time steps but is seamlessly woven into the graph structure. We are referring to the CNeuronHimNetGCRU class — the central component that acts as a sort of signal dispatcher between the temporal and spatial components of the model. The structure of the object is shown below.

class CNeuronHimNetGCRU    :  public CNeuronBaseOCL
  {
protected:
   CNeuronBaseOCL          cInpAndHidden;
   CNeuronHimNetGrapConv   cZ_R;
   CNeuronConvOCL          cZ_R_emb;
   CNeuronBaseOCL          cZe_Re;
   CNeuronBaseOCL          cZ;
   CNeuronBaseOCL          cR;
   CNeuronBaseOCL          cCandidate;
   CNeuronHimNetGrapConv   cHC;
   CNeuronConvOCL          cHC_emb;
   CNeuronBaseOCL          cHCe;

public:
                     CNeuronHimNetGCRU(void) {};
                    ~CNeuronHimNetGCRU(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units, uint window, uint window_out,
                          uint cheb_k, uint embed_dim,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL,
                                 CChebPolinom *Support,
                                 CNeuronBaseOCL *Embedding);
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL,
                                        CChebPolinom *Support,
                                        CNeuronBaseOCL *Embedding);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL,
                                        CChebPolinom *Support,
                                        CNeuronBaseOCL *Embedding);
   //---
   virtual int       Type(void)   const   {  return defNeuronHimNetGCRU;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
  };

In this case, it is no longer enough to simply apply a convolution over the node's neighbors or pass the data through a recurrent cell. As we know, financial markets rarely behave in a linear fashion: currency pairs form interconnected clusters, stocks move by sector, and commodities often move in tandem with the currencies of exporting countries. That is why a temporal model alone will not suffice — it needs a connection map, and graph convolution provides that map. But on its own, it is static. To decide in the moment whether to amplify the signal for EURUSD when the neighboring pairs GBPUSD and EURGBP are moving in opposite directions, or to dampen it. Control logic is needed. That is exactly what this class implements.

This module contains not just the GRU mechanism familiar to developers of temporal models, but a version of it adapted to a graph structure. And while in the previous graph convolution class we dealt with a fairly compact set of elements, here we are faced with a much more complex configuration. The list of internal components is impressive, but each one plays a strictly defined role in the overall operation. We will reveal their roles gradually, as we develop the methods for this object, so as not to overwhelm the reader with details ahead of time. It is important to note that all these internal objects are declared statically, which makes the class easier to manage and eliminates the need to create cumbersome constructors and destructors — they remain empty, like well-oiled doors that do not creak and do not require any extra effort each time the algorithm is run.

Moving from the general structure of the class to the specific details of how it works, we naturally come to the Init initialization method. This is where the chess pieces are set up — where all the internal components that will later determine the dynamics of the calculations are created and prepared. The Init method is like a behind-the-scenes conductor's baton: while the external code simply calls it, a complex yet well-organized sequence of steps for preparing each module unfolds internally.

bool CNeuronHimNetGCRU::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint units, uint window, uint window_out,
                             uint cheb_k, uint embed_dim,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units * window_out,
                                                   optimization_type, batch))
      return false;
   activation = None;

It all starts with initializing the parent class. This is where all the inherited base interfaces of the neural layer are created. Please note that the number of neurons is defined as the product of the number of elements in the sequence and the size of the feature window at the object’s output — thereby allocating computational space in advance for the full temporal context.

Immediately after that, the activation function is reset to zero. This is done intentionally: at this stage, it is important to maintain neutrality so that the subsequent layers receive a clean canvas for their specific activation.

Next, the initialization sequence for the internal components begins. The first one — cInpAndHidden — serves as a sort of gateway connecting the input data and the hidden state. Its dimensionality is determined by the sum of the input and output feature windows, multiplied by the number of elements in the sequence, which allows it to accumulate all the necessary information for subsequent processing.

   int index = 0;
   if(!cInpAndHidden.Init(0, index, OpenCL, (window + window_out)*units, optimization, iBatch))
      return false;
   cInpAndHidden.SetActivationFunction(None);

Next, cZ_R enters the scene; it is responsible for forming the combined matrices Z and R — the key GRU gates. This is where the mechanism for working with graph structures comes into play: the method takes the tensor shape of the data being analyzed and the number of terms in the Chebyshev polynomial, which directly affects the depth of the graph approximation.

   index++;
   if(!cZ_R.Init(0, index, OpenCL, units, window + window_out, cheb_k, optimization, iBatch))
      return false;
   cZ_R.SetActivationFunction(None);

Next in the initialization chain is the cZ_R_emb object — a sort of bridge between the embedding and convolutional dynamics. Its task is to generate a matrix of convolution parameters — not mechanically, but based on the resulting embedding — so that these parameters can subsequently be applied in the computational pipeline.

This is not merely a utility variable, but a key intermediary: it is here that the intelligent repackaging of connections between individual series takes place, transforming them from a set of disparate observations into a coherent map of interrelationships. If we draw a parallel with financial markets, this step resembles the work of an experienced analyst who takes raw trading activity data and transforms it into a format where trends, correlations, and lagged dependencies begin to take shape, making it possible to forecast future price movements.

   index++;
   if(!cZ_R_emb.Init(0, index, OpenCL, embed_dim, embed_dim,
                     2 * cheb_k * (window + window_out) * window_out,
                     1, units, optimization, iBatch))
      return false;
   cZ_R_emb.SetActivationFunction(None);
   index++;
   if(!cZe_Re.Init(0, index, OpenCL, 2 * window_out * units, optimization, iBatch))
      return false;
   cZe_Re.SetActivationFunction(None);
   index++;
   if(!cZ.Init(0, index, OpenCL, window_out * units, optimization, iBatch))
      return false;
   cZ.SetActivationFunction(None);
   index++;
   if(!cR.Init(0, index, OpenCL, window_out * units, optimization, iBatch))
      return false;
   cR.SetActivationFunction(None);

The method then initializes several lighter-weight, but no less important, blocks: cZe_Re, cZ, and cR. These modules generate the final Z and R vectors, providing flexible control over information forgetting and updating. Their dimensionality depends directly on the feature window and the number of sequence elements in the result tensor — in effect, this reflects the model’s ability to retain the current market context in memory.

Next, cCandidate is created — a component responsible for calculating the candidate new state. Its dimensionality is inherited from the already initialized cInpAndHidden, which underscores the close relationship between the input signal and the proposed hidden-state model.

   index++;
   if(!cCandidate.Init(0, index, OpenCL, cInpAndHidden.Neurons(), optimization, iBatch))
      return false;
   cCandidate.SetActivationFunction(None);

The second major block — cHC — is another graph convolution that operates at the level of the updated state. It has its own convolution parameters, cHC_emb, which are constructed based on the obtained embedding, but with different coefficients. The final element is cHCe, which ultimately forms the representation of the updated hidden state.

   index++;
   if(!cHC.Init(0, index, OpenCL, units, window + window_out, cheb_k, optimization, iBatch))
      return false;
   cHC.SetActivationFunction(None);
   index++;
   if(!cHC_emb.Init(0, index, OpenCL, embed_dim, embed_dim,
                    (window + window_out) * window_out * cheb_k,
                    1, units, optimization, iBatch))
      return false;
   cHC_emb.SetActivationFunction(None);
   index++;
   if(!cHCe.Init(0, index, OpenCL, window_out * units, optimization, iBatch))
      return false;
   cHCe.SetActivationFunction(None);
//---
   if(!Output.Fill(0))
      return false;
//---
   return true;
  }

Once all modules have been initialized, the final preparation takes place: the result buffer is zeroed. Let's not forget that we are working with a recurrent block. And clearing the results buffer ensures that the model starts with a clean state, uncontaminated by past noise.

All of this only seems complicated at first glance. In practice, it is a streamlined architecture in which each block plays its own role: from preliminary data packaging to fine-tuning the forgetting and updating of information in a graph GRU structure. This is critically important for financial market tasks: the model must be able to account for both local price fluctuations (within the window) and their global context (through graph approximation), while responding flexibly to new events without losing historical momentum.

Now that we have taken a detailed look at the initialization process, it is time to dive into how the graph-based recurrent block works and see how the forward pass method feedForward brings the architecture to life. First, we carefully verify that all necessary components are up to date: the main source data buffer, the Chebyshev polynomial tensor, and the embeddings. If even one of them is missing, the process does not start. It's like a trader who checks all the indicators and data before opening a position: you cannot make a forecast without the full picture.

bool CNeuronHimNetGCRU::feedForward(CNeuronBaseOCL *NeuronOCL,
                                    CChebPolinom *Support,
                                    CNeuronBaseOCL *Embedding)
  {
   if(!NeuronOCL || !Support || !Embedding)
      return false;
   if(!SwapOutputs())
      return false;

Next, pointers to the data buffers for the current and previous results are swapped, creating recurrent memory. This step is important because the past influences the future — just as yesterday’s market fluctuations shape today’s expectations.

Next, note that during the initialization process, we did not store the block's configuration parameters in separate variables. This was done on purpose. After all, they are all duplicated in the internal components. And we can extract them.

   uint cheb_k = cZ_R.GetChebK();
   uint units = cZ_R.GetCount();
   uint window_out = Neurons() / units;
   uint window = cZ_R.GetWindow() - window_out;

Only after the preparatory work is complete do we move on to organizing the process. The source data and the previous state are concatenated into the cInpAndHidden object, forming a single information stream. This stream — a combination of news, indicators, and signals — is ready for further processing.

   if(!Concat(NeuronOCL.getOutput(), PrevOutput, cInpAndHidden.getOutput(), window, window_out, units))
      return false;

Next, cZ_R comes into play, where Chebyshev polynomials perform graph convolution: neighboring nodes influence one another, but this influence is limited to the K-hop neighborhood, ensuring efficient use of resources and computational stability.

   if(!cZ_R.FeedForward(cInpAndHidden.AsObject(), Support))
      return false;
   if(!cZ_R_emb.FeedForward(Embedding))
      return false;

At the same time, cZ_R_emb generates a convolution parameter matrix based on the embeddings, much like a chef adds spices to a dish to bring out subtle flavors: temporal and spatial features are properly packaged for further use.

The convolution results are multiplied and passed through a sigmoid activation function, after which they are split into the Z and R gates. They control the flow of information, determining what should be preserved from the past and what should be updated.

   if(!MatMul(cZ_R.getOutput(), cZ_R_emb.getOutput(), cZe_Re.getOutput(), 1,
                 (window + window_out)*cheb_k, 2 * window_out, units, true))
      return false;
   if(!Activation(cZe_Re.getOutput(), cZe_Re.getOutput(), SIGMOID))
      return false;
   if(!DeConcat(cZ.getOutput(), cR.getOutput(), cZe_Re.getOutput(), window_out,
                                                            window_out, units))
      return false;

Next, a candidate state cCandidate is formed, combining the current input and the modified previous state. This state is fed into the second graph convolution block cHC, where new parameters are created in a similar manner via cHC_emb, and the result is passed through a hyperbolic tangent function, forming an updated hidden state.

   if(!ElementMult(cZ.getOutput(), PrevOutput, cZ.getPrevOutput()))
      return false;
   if(!Concat(NeuronOCL.getOutput(), cZ.getPrevOutput(), cCandidate.getOutput(),
                                                      window, window_out, units))
      return false;
   if(!cHC.FeedForward(cCandidate.AsObject(), Support))
      return false;
   if(!cHC_emb.FeedForward(Embedding))
      return false;
   if(!MatMul(cHC.getOutput(), cHC_emb.getOutput(), cHCe.getOutput(), 1, 
                  (window + window_out)*cheb_k, window_out, units, true))
      return false;
   if(!Activation(cHCe.getOutput(), cHCe.getOutput(), TANH))
      return false;
   if(!GateElementMult(PrevOutput, cHCe.getOutput(), cR.getOutput(), Output))
      return false;
//---
   return true;
  }

In the final stage, the previous state is combined element by element with the candidate via the R gate, and the output is a prediction that is ready for analysis or further processing by the decoder. If we translate this into the language of financial markets, each step represents multilayer signal filtering: past movements, neighboring assets, temporal features, and internal embeddings are combined to form an accurate and stable forecast. The feedForward method here is like a conductor who coordinates an orchestra of many instruments, transforming chaotic market data into a harmonious symphony of forecasts.

Now that we have covered the forward pass in detail, it is time to move on to an equally important and fascinating step — error-gradient propagation. If the forward pass can be compared to a trader who builds a forecast based on current data, then backpropagation is the moment when the trader sits down after the trading day, opens their trading journal, and analyzes where the signals worked and where the forecast failed. Based on this analysis, the model's internal parameters are adjusted so that it will perform more accurately the following day.

The calcInputGradients method begins by thoroughly checking all components: the input data tensor, the Chebyshev polynomial object, and the embeddings.

bool CNeuronHimNetGCRU::calcInputGradients(CNeuronBaseOCL *NeuronOCL,
      CChebPolinom *Support,
      CNeuronBaseOCL *Embedding)
  {
   if(!NeuronOCL || !Support || !Embedding)
      return false;
//---
   uint cheb_k = cZ_R.GetChebK();
   uint units = cZ_R.GetCount();
   uint window_out = Neurons() / units;
   uint window = cZ_R.GetWindow() - window_out;

Only after verifying that everything is in place do we proceed to compute the key parameters: the order of the Chebyshev polynomial, the number of elements in the sequence, and the feature dimensionality at the input and output. These values provide the framework for error-gradient propagation, determining how the error signal will travel through the architecture.

The first step involves processing the gradient of the operation that combines the hidden state with the candidates element by element via the R gate. The operation takes into account the effects of all the components used. Imagine that we are evaluating each indicator's contribution to the strategy's performance: some signals are corrected more strongly, others more weakly, depending on their impact on the forecast.

   if(!GateElementMultGrad(PrevOutput, cR.getPrevOutput(),
                           cHCe.getOutput(), cHCe.getGradient(),
                           cR.getOutput(), cR.getGradient(),
                           Gradient, None, TANH, cR.Activation()))
      return false;

Next, we pass the error gradient through the matrix multiplication operation to update the cHC and cHC_emb blocks. It is similar to filtering market signals: the error passes through all layers and is carefully distributed so that each component of the network receives its share of the correction.

   if(!MatMulGrad(cHC.getOutput(), cHC.getGradient(),
                  cHC_emb.getOutput(), cHC_emb.getGradient(),
                  cHCe.getGradient(), 1, (window + window_out)*cheb_k,
                  window_out, units, true))
      return false;

Calling calcInputGradients for cHC ensures that gradients are correctly propagated to the hidden state candidate, as if we were evaluating not only the results of individual trades but also their interrelationships within the overall trading strategy.

   if(!cHC.calcInputGradients(cCandidate.AsObject(), Support))
      return false;
   if(!DeConcat(NeuronOCL.getGradient(), cZ.getPrevOutput(), cCandidate.getGradient(),
                window, window_out, units))
      return false;

This is followed by deconcatenation — distributing the error gradients between the input data and the candidates from the hidden state.

Passing the error gradients through an element-wise multiplication operation and then concatenating them produces the correct gradients for the Z and R gates. Adjusting the obtained values by the derivative of the activation function ensures correct error scaling, preventing model overload.

   if(!ElementMultGrad(cZ.getOutput(), cZ.getGradient(),
                       PrevOutput, cCandidate.getPrevOutput(),
                       cZ.getPrevOutput(), None, None))
      return false;
   if(!Concat(cZ.getGradient(), cR.getGradient(), cZe_Re.getGradient(),
              window_out, window_out, units))
      return false;
   if(!DeActivation(cZe_Re.getOutput(), cZe_Re.getGradient(), cZe_Re.getGradient(), SIGMOID))
      return false;

Special attention is given to Chebyshev polynomials. First, the previous gradient obtained from the cHC object is saved. Next, calcInputGradients is called for cZ_R, after which the accumulated gradients are carefully summed. This allows the model to account for the direct and indirect influences of neighboring nodes in the graph, ensuring a stable and accurate update of the graph convolution weights.

   if(!MatMulGrad(cZ_R.getOutput(), cZ_R.getGradient(),
                  cZ_R_emb.getOutput(), cZ_R_emb.getGradient(),
                  cZe_Re.getGradient(), 1, (window + window_out)*cheb_k,
                  2 * window_out, units, true))
      return false;
   CBufferFloat* temp = Support.getGradient();
   if(!Support.SetGradient(Support.getPrevOutput(), false) ||
      !cZ_R.calcInputGradients(cInpAndHidden.AsObject(), Support) ||
      !SumAndNormilize(temp, Support.getGradient(), temp, Support.GetDimension(), false, 0, 0, 0, 1) ||
      !Support.SetGradient(temp, false))
      return false;

The main information flow process concludes with the transmission of gradients to the raw data level, combining information from the two information pathways. With subsequent adjustment by the derivative of the activation function.

   if(!DeConcat(cInpAndHidden.getPrevOutput(), cCandidate.getPrevOutput(),
                cInpAndHidden.getGradient(), window, window_out, units))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), cInpAndHidden.getPrevOutput(),
                       NeuronOCL.getGradient(), window, false, 0, 0, 0, 1))
      return false;
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), 
                       NeuronOCL.getGradient(), NeuronOCL.Activation()))
         return false;

Next, the embeddings undergo a similar procedure: their gradients are adjusted via cHC_emb and cZ_R_emb. These are then summed, ensuring that all parameters are updated consistently.

   if(!Embedding.CalcHiddenGradients(cHC_emb.AsObject()))
      return false;
   temp = Embedding.getGradient();
   if(!Embedding.SetGradient(Embedding.getPrevOutput(), false) ||
      !Embedding.CalcHiddenGradients(cZ_R_emb.AsObject()) ||
      !SumAndNormilize(temp, Embedding.getGradient(), temp, cZ_R_emb.GetWindow(), false, 0, 0, 0, 1) ||
      !Embedding.SetGradient(temp, false))
      return false;
//---
   return true;
  }

As a result, calcInputGradients becomes a sophisticated system for redistributing errors throughout the GCRU architecture. In practice, this is like a trader who does not just identify flaws in a strategy, but analyzes each component in detail — temporal and spatial patterns, relationships between assets, and reactions to market events — and uses this information to improve the accuracy of future forecasts. This approach makes the model training process lively, dynamic, and as close as possible to real-world market logic.

Here we come to what might seem to be the most unassuming part of the mechanism — updating the weights. However, as is often the case with architectures that have a carefully thought-out internal logic, this simplicity conceals a well-designed hierarchy for managing parameters. The entire cumbersome system — with its gates, Chebyshev polynomials, matrix multiplications, and activations — ultimately boils down to correctly configuring just two key objects: cZ_R_emb and cHC_emb. It's as if, in a complex clockwork mechanism with dozens of gears, only two adjustment axes actually determined the accuracy of the whole movement.

The updateInputWeights method begins with a simple but fundamental check: whether an embeddings object exists. Without it, the update is meaningless, because it is through this that the model absorbs the spatial and semantic relationships of the graph. If the object does not exist, the method immediately terminates and returns false.

bool CNeuronHimNetGCRU::updateInputWeights(CNeuronBaseOCL *NeuronOCL,
                                           CChebPolinom *Support,
                                           CNeuronBaseOCL *Embedding)
  {
   if(!Embedding)
      return false;
//---
   if(!cZ_R_emb.UpdateInputWeights(Embedding))
      return false;
   if(!cHC_emb.UpdateInputWeights(Embedding))
      return false;
//---
   return true;
  }

Control is then passed to the two main stores of trainable parameters: first, the weights responsible for the Z-R block (cZ_R_emb) are updated, followed by those responsible for the H-C candidate block (cHC_emb). Both UpdateInputWeights calls are executed sequentially, like two precise adjustment screws, each affecting its own part of the computational node.

It is worth emphasizing that this conciseness is the result of sound architectural organization. All other layers, intermediate buffers, and gates have already received their gradients during backpropagation. Here, we are effectively issuing the command to apply these changes to the parameters that actually determine the model's behavior in the long term. In other words, if the previous method (calcInputGradients) can be compared to analyzing the causes of past errors, then updateInputWeights is the act of adjusting the trading system: we do not just draw conclusions; we incorporate them into the settings so that tomorrow’s forecast will be more accurate and stable.

This approach makes the architecture not only efficient but also predictable: making changes at just two points allows controlling a vast computational chain while preserving its integrity and consistency.

We have done a substantial amount of work today, and now is a good time to take a short break. In the next article, we will continue on this path. Let's finish what we started by bringing the implementation to its logical endpoint, and then test it on real historical data to see how the theory manifests itself in practice.



Conclusion

In this article, we focused on one of the key components of the entire architecture and analyzed its operation step by step — from the formation of internal connections to the error-gradient propagation mechanism and the parameter update procedure. We saw how individual modules, seemingly autonomous at first glance, begin to interact in a single rhythm, creating a cohesive structure ready for training and operation under real-market conditions. It is particularly important to emphasize that such a well-structured system relies on only two objects with trainable parameters, which makes it not only compact but also predictable to control.

At this stage, we have obtained an almost fully formed element of the future computational chain that is ready to take on the load of real trading data. In the next article, we will take a decisive step: we will integrate all the components into a single structure, conduct full-scale testing on historical time series, and see to what extent the algorithm we have created is capable not only of reproducing past patterns, but also of confidently navigating the ever-changing flow of the market.


References


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 for describing the system state and model architecture
5 NeuroNet.mqh Class Library Class library for building neural networks
6 NeuroNet.cl Library Library of OpenCL program code


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

Attached files |
MQL5.zip (3013.86 KB)
From Option Chain to Risk-Neutral Density: The Market's Own Probability Distribution From Option Chain to Risk-Neutral Density: The Market's Own Probability Distribution
The article builds an MQL5 indicator that recovers the risk-neutral density from an option chain via the Breeden–Litzenberger identity. Quotes are inverted to implied volatilities, the smile is smoothed and priced back to arbitrage‑free calls, and the second derivative yields the density. The tool reports probabilities above any level, the expected move, skew and kurtosis, and overlays the realized-return distribution for comparison.
Building a News Filter Engine in MQL5 Using a Local Economic Calendar File Building a News Filter Engine in MQL5 Using a Local Economic Calendar File
A file-based news filter for MQL5 reads a pre-downloaded Forex Factory CSV from MQL5/Files, avoiding fragile web scraping and paid APIs. It provides a modular CNewsFilter with a quote-aware CSV parser, suffix-robust currency extraction, an inclusive time-window checker with clear block reasons, and chart zones for today's events. A demo EA and assertion tests help you integrate and verify offline filtering around scheduled releases.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Automating Classic Market Methods in MQL5 (Part 8): Ed Seykota's Trend Following System Automating Classic Market Methods in MQL5 (Part 8): Ed Seykota's Trend Following System
The article presents a full MQL5 implementation of a multi-symbol trend system: dual EMA crossovers for entries, ADX to avoid ranges, ATR to normalize position size, and a heat monitor to cap total portfolio risk. We explain the architecture, calculation details, and entry/exit logic on daily bars. The result is a practical EA template for systematic, risk-aware portfolio trading.