preview
MQL5 Wizard Techniques you should know (Part 92): Using B-Tree Indexing and a Bayesian NN in a Custom Signal Class

MQL5 Wizard Techniques you should know (Part 92): Using B-Tree Indexing and a Bayesian NN in a Custom Signal Class

MetaTrader 5Trading systems |
134 0
Stephen Njuki
Stephen Njuki

Introduction

In the last article in this series, we added to our toolkit a trailing stop risk shield that utilized the Skip-List algorithm together with Hopfield networks. Trailing stops help in defending floating profits not just by having a stop loss but also by ensuring it is moved only at the optimal time and not in response to market noise or erratic swings. Constructing entry signals for multiple symbols can expose structural limits in standard toolkits, especially when running multiple strategies. First, the default MQL5 library does not provide relational state management. So, inbuilt data structures such as MQLRates are symbol-specific and if comparison across symbols is key in any strategy, this could be a speed-bump.

Secondly, there is the danger of deterministic certainty. Regular indicators and traditional neural networks do not have a safety net in that they are always issuing rigid buy and sell orders while callously treating chaotic market noise with the same conviction as clear trends. In other words, they are always exploiting. However, today one can argue that traders can use more sophisticated toolkits - ones that not only allow multi-symbol mapping, but afford a degree of exploration by perhaps incorporating a probability-based safety net in order to better appraise all market states with an appropriate conviction.


Task

As strategies change from single-asset analysis to more complex setups of multi-symbol portfolios, the design limitations of some MQL5 in-built data structures can come to the fore. When an Expert Advisor must triangulate pricing data, correlations, or divergence across multi-currency pairs, coders usually default to multidimensional arrays. However, arrays are not flexible, by design. They tend to be flat and need to be hard-coded, and nested into loop iterations in order to cross-reference historical states. As strategies scale—an increasingly common requirement—this brittle architecture can produce convoluted, error-prone code that adapts poorly.

Standard arrays do not have native relational state-management; out-of-the-box, they cannot map hierarchical or temporal relationships between different market variables without excessive custom scaffolding. We therefore embark on having a tool to natively handle relational data. Another issue we want to tackle in this article is dependence on deterministic entry models as is the case with most of the technical indicators, as well as supervised networks. It can be argued that traditional signal classes that rely on these are bound to suffer from an illusion of certainty. These mechanically output "buy" and "sell" signals devoid of not just a confidence interval, but entirely based on what has worked in the past and no undertaking of new exploration.

In uncertain markets a deterministic model can blindly issue a signal even if ambient noise implies the signal is unsound. In many custom signal implementations, there is no built-in mechanism to quantify forecast confidence before execution. There is also no exploratory method to consider alternatives to the default signal. We thus want to step up our design framework to not only forecast what happens next but to do so in a process that quantifies variance of the prediction and doing some exploration.


B-Tree Indexing

To overcome some of these limitations, we look beyond standard indicators to provide a solution that integrates with the MQL5 Wizard ecosystem. In doing so, we introduce the "CSignalBTreeBayesian" class. By inheriting directly from the "CSignalExpert" base class this is not an isolated script. Thus, as always we are building on the principle of abstracting quantitative complexity by exposing three concise parameters to the MQL5 Wizard interface:

  • BTreeMode (Integer, 1-4): This dictates the algorithm’s relational search patterns. It enables the user to change easily between structural queries of: Direct, or Range, or Depth, or Hybrid triangulation without changing base class code.

  • UseBayesian (Boolean): This is a toggle parameter that activates the neural network. It shifts the signal’s function from purely algorithmic data-gathering into probability-based filtering.

  • MaxUncertainty (Double): This is the critical variance threshold. Whenever the Bayesian network is engaged, this parameter acts as the statistical gatekeeper, by establishing the maximum acceptable predictive noise prior to a trade being rejected.

The strength of this custom signal class could lie in the merging of the SQLite indexing rules and Bayesian inference mathematics within a custom class so that we have heavy quantitative lifting being hidden from the end user. Our overall implementation is not a literal triangulation of three or more forex pairs but is a simulation of the Balanced-Tree Logic.

B-Tree Indexing SQLite B-Tree Advantages:

Before we get into the code of the "CSignalBTreeBayesian" class it may be helpful to outline the approach used, and explain the paradigm shift within the algorithm. In regular MQL5 development, assessing market states over different timeframes or correlated symbols can take a lot of parsing through multidimensional arrays or the copying of large data buffers via CopyRates. This can force the Expert Advisor into linear computationally heavy loop structures. The introduction of SQLite B-Tree Logic significantly changes our design approach.

A B-Tree (Balanced Tree) is an autonomous balancing data structure that maintains sorted data and allows searches, sequential access, insertion, and deletion at a logarithmic complexity O(log n) instead of O(n). As opposed to storing flattened market data in rigid arrays, the B-Tree structures: historical data price action, volatility nodes, and multi-symbol correlations into a relational map that is hierarchical. The "root" node branches out into internal nodes based on defined indexing keys (like timestamps or volatility thresholds). These terminate in nodes of "leaves" that contain price data.

In our custom class, we abstract this logic into four operational modes. The standard Close() and Open() price retrievals used in MQL5 code only serve as a proxy for deeper relational queries that would be used in triangulation. These queries typically involve price spreads or correlation deltas. In this custom signal class, however, we only simulate triangulation. This is the code, first up mode 1. The main search method for our relational structure is the direct lookup which we use when the input parameter "m_btree_mode" is set to 1.

//+------------------------------------------------------------------+
//| Mode 1: B-Tree Direct Lookup                                     |
//+------------------------------------------------------------------+
double CSignalBTreeBayesian::BTreeTriangulateDirect(void)
  {
   double diff = Close(StartIndex()) - Open(StartIndex());
   return diff * 1.5;
  }

Mode 1 simply runs a pinpoint query against the indexed node that is most recent. We call the base class function StartIndex() and set the pointer to the correct current bar being processed by the Expert Advisor. This acts like a "primary key" in our query. The returned "diff" value can be seen as a discrete price vector.

double diff = Close(StartIndex()) - Open(StartIndex());

In this situation, our algorithm works out the discrete price vector of the current node. In a scenario where multi-symbol triangulation is being used, this would represent an exact match lookup that ensures the prices of all three symbols have their respective price values in sync at a given timestamp. An equivalent metric that captures this could be the difference between the price of one of these three symbols and the product of the other two in case these two values should be identical.

For our function above, we multiply the difference with a scalar value of 1.5, which serves as a deterministic weight since it is not degraded with time. For the second mode we use the Balanced Tree to perform a range scan. When the market shows a localized chop, if we perform a lookup with "BTreeTriangulateDirect" could retrieve a "noisy" node. Enter mode-2. By initiating a range scan, our query now acts like a bounding box within the B-Tree.

//+------------------------------------------------------------------+
//| Mode 2: B-Tree Range Scan                                        |
//+------------------------------------------------------------------+
double CSignalBTreeBayesian::BTreeTriangulateRange(void)
  {
   double sum = 0;
   for(int i = 0; i < 5; i++)
      sum += (Close(StartIndex() + i) - Open(StartIndex() + i));
   return sum / 5.0;
  }

Rather than pull one primary key, this method traverses nearby leaf nodes in order to map a localized cluster. We initialize this relational accumulator, "sum" as zero. Then we run a for loop where the algorithm sets out a bounding box for the query. This is done by iterating backwards through 5 adjacent chronological nodes in the B-Tree, where we add up the price bar changes. As the algorithm scans every node in the set range, it extracts the price vector and overall structural momentum. The returned value is a mean across the covered range. In essence, this approach normalizes the overall sum by the bounding box size, which is 5 in our case.

This gets us a mean relational state for this specific cluster of the 5 recent price points. This mode can be helpful in smoothing out anomalies within the dataset as well as giving a stable baseline vector instead of reacting to every new bar. In Mode 3, we perform a depth-first discrepancy search. This could be the most aggressive and revealing mode because, in the forward-walk optimization tests below, it was the chosen mode. Activated when the input parameter "m_btree_mode" is assigned 3, the algorithm would delve deeper into the historical branches of the dataset in order to locate points of maximum divergence or structural breaks.

//+------------------------------------------------------------------+
//| Mode 3: B-Tree Depth Search                                      |
//+------------------------------------------------------------------+
double CSignalBTreeBayesian::BTreeTriangulateDepth(void)
  {
   double max_diff = 0;
   for(int i = 0; i < 10; i++)
     {
      double diff = Close(StartIndex() + i) - Open(StartIndex() + i);
      if(MathAbs(diff) > MathAbs(max_diff))
         max_diff = diff;
     }
   return max_diff;
  }

Whereas with the range scan we sought a normalized mean, the depth-search is hunting maximum divergence deep within relational branches with the final output "max_diff" being initialized as zero. We loop through a deeper search than the range search (twice as deep) by looking at 10 nodes. At each level, we work out the discrete vector of the node which we evaluate with the condition:

if(MathAbs(diff) > MathAbs(max_diff))

This is a critical check where we apply MathAbs() function in order to disregard any directional bias and strictly measure the magnitude of displacement. If the checked node shows large displacement in comparison to the stored maximum, then the stored max will be overwritten. This maximum displacement is what is returned by the function. Mode-3 is most suitable at identifying hidden volatility spikes or deep correlations in past data that the standard array-smoothing methods in Mode-1, and Mode-2 can miss.

Our final use mode is Mode-4 which is a hybrid of Range and Depth. It showcases flexibility of mapping trading logic into distinct usable functions. This Hybrid approach dynamically brings together the normalized smoothing of the range scan with extreme outlier detection of the Depth search.

//+------------------------------------------------------------------+
//| Mode 4: B-Tree Hybrid (Range + Depth)                            |
//+------------------------------------------------------------------+
double CSignalBTreeBayesian::BTreeTriangulateHybrid(void)
  {
   return (BTreeTriangulateRange() + BTreeTriangulateDepth()) / 2.0;
  }

This composite function serves as a robust, all-weather metric. With this, the "BTreeTriangulateRange()" calls the range query to establish a normalized smooth trajectory over the immediate 5-node cluster; while the "BTreeTriangulateDepth()" method concurrently queries the deeper 10-node branch in order to separate the most extreme structural anomaly. Our final output is obtained by averaging these two to arrive at a mean.

In synthesizing these queries, the algorithm gets a "nuanced" view of the market. We get a sense of the current smoothed momentum as well as the looming influence of extreme recent divergences. This merged output, named triangulated_val, is then ready to be passed to the next method. Nonetheless, as robust as this relational mapping appears, it remains mostly a deterministic algorithm up to this point. We therefore make the case that in order to truly protect our "margin", this structured data output needs to be filtered in a probability-based Bayesian Neural Network.


Filtering with the Bayesian Neural Network

While the Balanced-Tree indexing did resolve the constraint of multi-symbol triangulation, the resulting "triangulated_val", remained a deterministic output. It stands for a precise mathematical state, which unfortunately can lack adaptability when handling market noise. If we feed this value straight into a regular trade execution block, we fall back into the trap of the certainty illusion. In order to bridge this shortfall, we pass the triangulated data through our Bayesian Neural Network filter.

In this network, unlike regular neural networks that depend on static fixed weights to generate an "absolute best guess", a Bayesian Neural Network treats every internal weight as a probability distribution. By running several forward passes (which amounts to sampling different weights from these distributions at a time), the network can output a range of forecasts. From this, we can calculate two crucial metrics: the expected outcome (Mean) and the model's confidence in that outcome (Variance). Below is the core engine of this logic:

//+------------------------------------------------------------------+
//| Bayesian Inference: Forward Pass with Monte Carlo Dropout        |
//+------------------------------------------------------------------+
void CSignalBTreeBayesian::BayesianInference(double input_data, double &out_mean, double &out_variance, int idx)
  {
//--- SEED THE PRNG deterministically based on the current bar's timestamp.
//--- This guarantees the exact same random sampling sequence for this specific bar.
   m_seed = (uint)Time(idx) + (uint)m_btree_mode;
   const int passes = 20;
   double predictions[20];
   double sum_preds = 0.0;
   for(int p = 0; p < passes; p++)
     {
      double activation = GenerateGaussian(m_bias_mu, m_bias_sigma);
      for(int i = 0; i < 10; i++)
        {
         double sampled_weight = GenerateGaussian(m_weight_mu[i], m_weight_sigma[i]);
         activation += (input_data * sampled_weight);
        }
      predictions[p] = MathTanh(activation);
      sum_preds += predictions[p];
     }
   out_mean = sum_preds / passes;
   double variance_sum = 0.0;
   for(int p = 0; p < passes; p++)
     {
      variance_sum += MathPow(predictions[p] - out_mean, 2);
     }
   out_variance = variance_sum / passes;
  }

In the listing above, we use deterministic seeding where the function starts by seeding our custom pseudo-random number generator (PRNG). This is done by using the current bar's timestamp "Time(idx)". When using MetaTrader 5's Strategy Tester this is very important. If we depend on regular unanchored randomization, the Expert Advisor would give us completely different results on every test run. By anchoring the seed, this ensures that the probabilistic sampling is repeatable and consistent for any specific historical moment.

We use Monte Carlo Passes that are preset to 20. This implies that the network will evaluate the exact input_data 20 distinct times, thus building a distribution of forecasts instead of depending on a solo calculation. With this, we then work out the mean and variance in a for loop where the network samples its structural weights in order to generate forecasts. Rather than multiplying the input by a rigid array of numbers, the algorithm calls the "GenerateGaussian()" function that transforms the regular parameters of mean (mu) and standard deviation (sigma) into a randomized weight that is picked from a normal distribution.

Once we complete 20 passes, and they are pushed through the hyperbolic tangent activation function (MathTanh) our algorithm computes the "out_mean" variable, the average of our predictions, as well as the "out_variance" parameter across a dispersion of 20 predictions. In the event that the 20 predictions are tightly clustered together, and the variance is very low, the model would be more confident in its assessment.

However, if the forecasts are scattered wildly, this would inevitably spike variance, signaling that the network cannot reliably "get to the truth" given the current market noise. Our BNN can be taken as a secondary filter within the Expert Advisor that serves as the "Uncertainty Gate". The real power of this expansion can be realized when we bring this into the MQL5 Wizard code classes. This integration would be via the LongCondition() and ShortCondition() voting blocks.

//+------------------------------------------------------------------+
//| "Voting" that price will grow.                                   |
//+------------------------------------------------------------------+
int CSignalBTreeBayesian::LongCondition(void)
  {
   int result = 0;
   int idx = StartIndex();
//--- 1. B-Tree Triangulation Algorithm
   double triangulated_val = 0.0;
   switch(m_btree_mode)
     {
      case 1:
         triangulated_val = BTreeTriangulateDirect();
         break;
      case 2:
         triangulated_val = BTreeTriangulateRange();
         break;
      case 3:
         triangulated_val = BTreeTriangulateDepth();
         break;
      case 4:
         triangulated_val = BTreeTriangulateHybrid();
         break;
      default:
         triangulated_val = BTreeTriangulateDirect();
         break;
     }
//--- 2. Bayesian NN Filter
   if(m_use_bayesian)
     {
      double mean_pred = 0.0, variance = 0.0;
      //--- Pass the current bar index (idx) to anchor the random generation
      BayesianInference(triangulated_val, mean_pred, variance, idx);
      if(variance > m_max_uncertainty || mean_pred < 0.1 * m_symbol.Point())
         return(0);
     }
   else
     {
      if(triangulated_val < 0.0)
         return(0);
     }
//--- 3. Confirmation
   result = m_pattern_0;
   return(result);
  }

//+------------------------------------------------------------------+
//| "Voting" that price will fall.                                   |
//+------------------------------------------------------------------+
int CSignalBTreeBayesian::ShortCondition(void)
  {
   int result = 0;
   int idx = StartIndex();
//--- 1. B-Tree Triangulation Algorithm
   double triangulated_val = 0.0;
   switch(m_btree_mode)
     {
      case 1:
         triangulated_val = BTreeTriangulateDirect();
         break;
      case 2:
         triangulated_val = BTreeTriangulateRange();
         break;
      case 3:
         triangulated_val = BTreeTriangulateDepth();
         break;
      case 4:
         triangulated_val = BTreeTriangulateHybrid();
         break;
      default:
         triangulated_val = BTreeTriangulateDirect();
         break;
     }
//--- 2. Bayesian NN Filter
   if(m_use_bayesian)
     {
      double mean_pred = 0.0, variance = 0.0;
      //--- Pass the current bar index (idx) to anchor the random generation
      BayesianInference(triangulated_val, mean_pred, variance, idx);
      if(variance > m_max_uncertainty || mean_pred > -0.1 * m_symbol.Point())
         return(0);
     }
   else
     {
      if(triangulated_val > 0.0)
         return(0);
     }
//--- 3. Confirmation
   result = m_pattern_0;
   return(result);
  }

From our two key functions above, m_max_uncertainty serves as the final stat gatekeeper. By exposing this variable to the Wizard as an input, our Expert Advisor allows one to set their preferred "tolerance for chaos". Once a trade setup is identified, the algorithm checks the variance. Even if the Balanced Tree finds a plausible structure and mean_pred suggests a profitable setup, the trade is rejected when variance exceeds the user-optimized m_max_uncertainty threshold; both functions return 0. The signal is in effect nullified. It can be thought of as an elevated risk management protocol. Our Expert Advisor is no longer blindly following orders, but is now as a secondary measure capable of stepping away from a trade when the "statistical fog is too thick". This could have the effect of preserving margin for higher-probability trade environments.


Test Analysis: Algorithm vs Algorithm + Network

In order to show the practical impact this toolkit expansion has, we test-run our custom signal class CSignalBTreeBayesian as a Wizard-assembled Expert Advisor within Strategy Tester. The aim here, besides establishing overall forecast acumen, is to separate the "merits" of the Balanced-Tree indexing on their own from risk-mitigating potential of the Bayesian Neural Network. We performed two optimization runs, each followed by a forward walk.

In the first run, the Expert Advisor did not use the Bayesian Neural Network, and we optimized Balanced-Tree-Indexing Mode 3 (Signal_BTreeBayes_BTreeMode=3) as the most stable indexing mode. We performed both optimizations on the pair GBP USD from 2025.01.01 to 2026.05.01, where the forward walk window was at the quarter mark stretching from 2026.01.01 to 2026.05.01. Furthermore, we used the 4-hour timeframe.

Run-1:

In our first testing (input settings attached as r1.set) the Expert Advisor worked solely using the relational data structure of the Balanced-Tree. The Bayesian Network was deactivated since the input parameter "Signal_BTreeBayes_UseBayesian" was set to false. The EA thus only depended on the algorithm's output.

r1

The results indicate both the power and vulnerability of a purely deterministic structural search. Since the depth triangulation is able to identify complicated, non-obvious setups that regular flat arrays could miss, the algorithm was able to place a relatively high number of trades. This suggests it mapped the market with some precision. However, an analysis of the equity curve of this forward walk shows some core flaws when dealing with deterministic certainty.

Without a probability-based variance filter, the Expert Advisor executed all the structurally valid triangulations it found. There was no consideration for market noise. In these periods of chaotic volatility, a lot of low-probability setups were entered. This can be seen in the sharp equity drawdowns as well as a compressed profit factor. The algorithm on its own proved capable of searching out worthy setups, however, without a confidence interval it lacked the requisite self-preservation instincts that can be crucial in protecting margin during turbulent conditions.

Run-2:

In the second optimization (input settings attached as r2.set), we use both the algorithm and neural network. The Bayesian network was activated by having the input parameter "Signal_BTreeBayes_UseBayesian" assigned true. We also had to optimize the variance gatekeeper threshold and this came to 0.475035 (for input parameter Signal_BTreeBayes_MaxUncertainty). With both algo and network in action, we had a significant change in the forward walk results.

We were forcing the triangulation vector through the Monte Carlo passes of the Bayesian network, and it seems the Expert Advisor was able to quantify the chaos. The overall number of placed trades dropped markedly in comparison to run-1, and this arguably was not a flaw but the exclusion of low-confidence, noisy trades.

r2

Whenever the BNN evaluated a setup that was valid, but also computed an internal predictive variance exceeding the 0.475 limit, the trade was abandoned. The impact of this on performance metrics does seem apparent, immediately. The equity curve smooths out dramatically, maximum drawdown is reduced a lot and the profit factor also improves compared with run 1. The case can be made therefore that the Bayesian filter was able to neutralize false positives that we saw in run-1, and serve as a dynamic risk-mitigating shield that only allowed execution of trades when the confidence was reasonably high.


Summary (Take-Aways)

    • Relational Data Overcomes Array Rigidity: When strategies start scaling and have to evaluate several correlated assets, depending on standard multidimensional arrays can become a dead-end. If we embed SQLite B-Tree indexing into an Expert Advisor we get scalability. This allows the Expert Advisor to natively map complex hierarchical and time-based correlations, that span large datasets without settling for brittle nested for-loops.
    • Adaptable Searches give Flexibility: Any strategy that is sophisticated usually requires some flexible way of retrieving data. By exploring separate algorithmic search modes of: direct-lookup; range-scanning; depth-traversal; and a hybrid-synthesis; all in one custom signal class, developers can expand how they utilize Wizard-assembled Expert Advisors. The assembled Expert Advisors can dynamically shift their data gathering behavior based on prevalent market microstructure.
    • Probability-Based filtering Neutralizes Market Noise: The illusion of deterministic certainty, or strict dependence on preset entry logic, can be a primary destroyer of margin. By having a transition to Bayesian models, we introduce an arguably important layer to managing risk. Our approach of evaluating weight distributions by Monte Carlo passes makes the Bayesian network put a number on its own predictive variance. This allows the Expert Advisor to statistically avoid chaotic setups.


Conclusion

The aim was to make the trade-initiation architecture adept at handling multiple data streams. Some structural limitations with the rigidity and scaling bottlenecks presented by flat arrays can be addressed by expanding to our B-Tree custom class. This integration overwrote convoluted for-loop habits with relational and adaptable search modes which allow the Expert Advisor to handle more complex datasets, organically.

Also, the illusion of certainty that can be native to deterministic models was to a degree neutralized by embedding a Bayesian Neural Network. As a secondary gate, we forced data through a probability-based distribution so that our model was better positioned to calculate variance thus serving as a shield to dynamically reject low-confidence noisy signals.

name description
wz_92.mq5 wizard assembled Expert Advisor
SignalBTreeBayesian.mqh Custom Signal Class used in Wizard Assembly
r1.set Input settings for solo Algorithm Test
r2.set Input settings for Test with Algorithm and Neural Network
Attached files |
r1.set (1.5 KB)
r2.set (1.51 KB)
wz_92.mq5 (6.96 KB)
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
MQL5 Bootstrap (I): Reusable Functions for Working with Positions and Orders MQL5 Bootstrap (I): Reusable Functions for Working with Positions and Orders
This article presents a compact MQL5 utility layer for routine trade operations. It includes position existence checkers, position counters, bulk close helpers, and functions to retrieve the most recent or oldest position by symbol, magic, or type. A simple SMA crossover Expert Advisor demonstrates integration. The result is cleaner EAs, fewer inconsistencies across projects, and faster maintenance.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Modular Indicator Architecture in MQL5 (Part 1): Stop Copy-Pasting and Start Writing Scalable, Reusable Code Modular Indicator Architecture in MQL5 (Part 1): Stop Copy-Pasting and Start Writing Scalable, Reusable Code
This article develops an object-oriented framework for MQL5 indicators by evolving a primitive example into reusable modules. It formalizes partial buffer recalculation in OnCalculate, moves logic into header-based classes (CAppliedPrice, CSma), and introduces CSubIndiBase, CIndicatorBase, and a registry to centralize requirements. You get portable components, isolated inputs, and clean buffers with minimal boilerplate, making new indicators faster to assemble and easier to maintain.