preview
MQL5 Wizard Techniques you should know (Part 94): Using Reservoir Sampling and Linear Regression in a Custom Trailing Stop Class

MQL5 Wizard Techniques you should know (Part 94): Using Reservoir Sampling and Linear Regression in a Custom Trailing Stop Class

MetaTrader 5Trading systems |
115 0
Stephen Njuki
Stephen Njuki

Introduction

In the last article on trailing stops within this series, we explored the Skip-List algorithm pairing to a Hopfield Network, with an associative memory approach, customized for sharp momentum environments. One can say it suits aggressive traders who are after fast-moving markets that are also gap-heavy. Non-contiguous price action would often be the norm in these cases.

However, different models tend to favor different market environments and trading styles. Trying to develop a universal solution is often a non-starter. In this article, we pivot to Reservoir Sampling paired with a Linear Regression network. This is a move from associative memory to statistical normalization. The approach here is particularly suited to markets where filtering micro-noise while monitoring a smooth memory-efficient average-buffer outperforms rapid gap detection.


Math Definition

In order to establish the case for why this model combination thrives in continuous trending environments, we need to look into its two pillars. Our Trailing Stops application will run in two modes: the 'Engine' or the Reservoir Sampling algorithm gives us a base price that is stabilized with probability; and the 'Filter' or Linear Regression network that establishes whether it is safe to run the stop-loss update.

Reservoir Sampling

For most algorithmic trading, whether high-frequency or not, storing every price bar in order to work out the moving average can be computationally expensive. As the buffer size increases, the strain on resources can be a major processing bottleneck. Whereas with the Skip-List we mapped disjoint price gaps by skipping over data, with Reservoir Sampling we tackle a different problem. How do we maintain a statistically 'fair' and proper representation of prices from a continuous data stream whose size is not known, by using as small an amount of memory as possible. This is a reservoir-sampling algorithm. We decide whether an incoming price should replace an existing element in the buffer (the reservoir) while keeping memory usage fixed. We follow these steps in implementing this:

  • If we set a fixed size, k, for a reservoir array
  • The first k prices of the market session would all be copied to this array
  • After this, every subsequent new price at index i (where i > k), we generate a random integer j between 1 and i.
  • If j is less than or equal to k then the new price replaces the price at j in the reservoir.

    The formula is based on the probability that a new data point replaces an existing one in the reservoir, which decays as the overall data stream increases. This is expressed as:

    f1

    Where:

    • P is the replacement likelihood
    • k is the fixed maximum size of the reservoir array
    • i is the index (or current count) of the incoming price bar currently being observed

    Given that every time the stream has an equal likelihood k/i of ending up in the reservoir, the sample remains evenly balanced over time. It more or less filters out micro-noise and flash crashes without hoarding large amounts of data. In MQL5 Strategy Tester, pseudo-random generators such as MathRand() share a global state. To preserve sampling integrity and ensure repeatable backtesting results, we isolate randomness with a deterministic linear congruential generator. This is built directly into the class as will be shown in the coding section further below.

    Linear Regression

    While Reservoir Sampling helps us get a stabilized base price, for adjusting our stop-loss, it is inherently historical. This is because it tells us where the market has been for the most part, without giving us any 'direction' on where price might head. In the last article on Trailing Stops, we used a Hopfield Network since its associative memory is good at spotting chaotic and fragmented patterns. However, the Hopfield networks can be too much when dealing with smooth continuous trends. In such a setting, we layer a simpler, single-layer predictive network that uses Ordinary Least Squares (OLS) for Linear Regression.

    Instead of doing pattern-matching, Linear Regression works out the basic mathematical vector of the present price action. It serves as a decision filter - when the calculated vector forecasts that the market is heading for a spike against a current position, we freeze the trailing stop to prevent being stopped out too soon from a micro-pullback. The formula used by the network seeks the line of best fit across a rolling window of recent prices. Its main equation is given as follows:

    f2

    In order to get the slope m, and the y-intercept b, the network minimizes the sum of the squares of the vertical deviations of the price data from the best-fit-line. The slope and intercept equations are given below:

    f3

    Where:

    • Y is the forecast future price
    • X is the future time step projected (worked out as n + 1)
    • Sigma X is the total time indices (1 to n)
    • Sigma Y is the total observed prices within the rolling window
    • Sigma XY is the sum of multiplied time indices and their prices
    • Sigma X^2 is the total of the squared time indices.

    By projecting Y for the next price bar, the network is able to define our execution logic. When trailing a long position for example, and the network forecasts a downward vector (Y < the current price), the trailing stop would halt. When this micro pullback passes, and the vector points upward again, the reservoir engine would be 'allowed' to resume trailing. 


    MQL5 Implementation

    To implement this two-phase model, we create a custom trailing class named CTrailingReservoirLinReg. It inherits, as always, directly from the standard library's 'CExpertTrailing' class which as we have mentioned in the past is heavily integrated with other MQL5 Wizard classes. In building our custom class we allow traders to optimize/tune the reservoir size, iteration mode, as well as to toggle whether we are applying the Linear Regression network filter.

    Let's review the code to show how the model processes continuous data while maintaining memory efficiency. If we start at the indicator initialization, within this standard class function we also perform memory allocation. Unlike strategies that need a lot of history buffers, this class is meant to tightly control its memory footprint by using dynamic arrays. In the indicator initialization function we allocate only the amount of RAM requested by the user as set out via the input parameters.

    //+------------------------------------------------------------------+
    //| Initialize Indicators and Arrays                                 |
    //+------------------------------------------------------------------+
    bool CTrailingReservoirLinReg::InitIndicators(CIndicators *indicators)
      {
       if(!CExpertTrailing::InitIndicators(indicators))
          return false;
    //--- Initialize dynamic arrays for algorithms and networks
       if(m_res_size <= 0)
          m_res_size = 100;
       if(m_lr_period <= 0)
          m_lr_period = 14;
       ArrayResize(m_reservoir, m_res_size);
       ArrayInitialize(m_reservoir, 0.0);
       ArrayResize(m_lr_prices, m_lr_period);
       ArrayInitialize(m_lr_prices, 0.0);
       return true;
      }
    

    By using the 'ArrayResize()' and 'ArrayInitialize()' functions once during the start of the Expert Advisor, we are pre-allocating memory for the fair sample ('m_reservoir') as well as the rolling network window ('m_lr_prices'). Also we want to have safety fallbacks (initializing to 100 and 14 respectively) in order to avert array-out-of-range errors in the event we have invalid zero values as inputs. With this, we now move on to the deterministic core or the algorithm engine. The main hurdle in testing probability based random algorithms in MQL5's Strategy Tester is that built-in functions such as 'MathRand()' depend a lot on a shared global state.

    When an indicator, a background event, or even another Expert Advisor calls 'MathRand()' in a test the global state changes unpredictably, by design. This well intentioned desynchronization can destroy the repeatability of a fair sample. The main implication of this is optimization or fine-tuning this algorithm with other trade classes and systems becomes problematic. In order to best ensure that backtesting results can be repeatable, we separate the algorithm's randomness by using a Linear Congruential Generator (LCG) that we build directly into the class, as follows:

    //+------------------------------------------------------------------+
    //| Isolated Deterministic Random Generator (POSIX.1 LCG)            |
    //+------------------------------------------------------------------+
    int CTrailingReservoirLinReg::DeterministicRand()
      {
    //--- Normalizes randomness completely independent of MT5's MathRand()
    //--- Guarantees identical sequence generation per backtest pass
       m_norm_seed = (m_norm_seed * 1103515245 + 12345) & 0x7FFFFFFF;
       return (int)m_norm_seed;
      }

    From our listing above, the bitwise AND operation '& 0x7FFFFFFF' is key. Its inclusion masks the sign bit, guaranteeing that our LCG always returns a positive integer, that can safely be converted to a valid array index. Our normalized engine then drives the P = k/i replacement logic within the state manager as follows:

    //+------------------------------------------------------------------+
    //| Reservoir State Manager (Algorithm R Core)                       |
    //+------------------------------------------------------------------+
    void CTrailingReservoirLinReg::UpdateReservoirState(double price)
      {
       m_samples_seen++;
       if(m_samples_seen <= m_res_size)
         {
          m_reservoir[m_samples_seen - 1] = price;
         }
       else
         {
          //--- Standard Reservoir Sampling replacement probability
          //--- utilizing the deterministic normalization factor
          int random_index = DeterministicRand() % m_samples_seen;
          if(random_index < m_res_size)
             m_reservoir[random_index] = price;
         }
      }

    When the 'UpdateReservoirState()' function is called, it starts by incrementally filling the available RAM array up to when 'm_samples_seen' becomes greater than 'm_res_size'. Once it is full, it uses the deterministic random index modulus of the total price bars seen ('DeterministicRand() % m_samples_seen') to replace older price bars with new ones. This is done by using probability ensuring the sample remains representative over an unlimited time without expanding memory.

    As has been the case lately in this series of articles, we implement the core engine/algorithm in four different modes. These are meant to be tuned so traders can pick what is suitable in their context. Once a fair sample is defined, the class uses a 'switch' directive within both the standard long and short functions of 'CheckTrailingStopLong' and 'CheckTrailingStopShort'. The reservoir is evaluated in four separate ways, below we have the first mode that uses a standard mean:

    //+------------------------------------------------------------------+
    //| Mode 1: Standard Mean of Fair Sample                             |
    //+------------------------------------------------------------------+
    double CTrailingReservoirLinReg::ReservoirModeMean(double price)
      {
       double sum = 0.0;
       int count = MathMin(m_samples_seen, m_res_size);
       if(count == 0)
          return price;
       for(int i = 0; i < count; i++)
          sum += m_reservoir[i];
       return sum / count;
      }

    With this mode, we iterate across the array to calculate the statistical mean. In doing so, we use 'MathMin(m_samples_seen, m_res_size)' to ensure that the for-loop only processes data that exists in the starting phase. Given that the sample is 'probabilistically fair', in theory it means we have a smoother baseline for setting our stop loss levels as opposed to lagging 'tail' that are common in simple moving averages. The second mode is different from 1 in that instead of averaging everything, we extract the absolute minimum and maximum values that are within the fair sample.

    Mode-2 initializes 'min_val' and 'max_val' at 'm_reservoir[0]', and then loops from index 1 to dynamically update the extreme values. The returned amount of '(max_val + min_val) / 2.0' creates a midpoint which could imply stability and a bit of immunity to dense clusters of micro-price-bars. The next mode, Mode-3 introduces adaptability to volatility. In situations where the trend stretches and contracts at a rhythm, in this mode we adjust the base price basing on the standard deviation. We implement this as follows:

    //+------------------------------------------------------------------+
    //| Mode 3: Volatility Adjusted (Mean shifted by Std Dev)            |
    //+------------------------------------------------------------------+
    double CTrailingReservoirLinReg::ReservoirModeVolatility(double price)
      {
       int count = MathMin(m_samples_seen, m_res_size);
       if(count < 2)
          return price;
       double mean = ReservoirModeMean(price);
       double variance_sum = 0.0;
       for(int i = 0; i < count; i++)
          variance_sum += MathPow(m_reservoir[i] - mean, 2);
       double std_dev = MathSqrt(variance_sum / count);
    //--- Stabilize price by pulling it toward the mean minus/plus volatility buffer
       return (price > mean) ? mean + std_dev : mean - std_dev;
      }
    

    We work out the sample's variance by using 'MathPow(m_reservoir[i] - mean, 2)'. We sum it and then extract the square root via 'MathSqrt'. We dynamically pull the trailing base price up or down by a standard deviation from the ternary operator:

    (price > mean) ? mean + std_dev : mean - std_dev

    This adjustment tightens the stop when volatility spikes. For Mode-4, we deploy a recent-biased decay. What this means is that we try to bridge the continuous sample (reservoir) to the recent momentum. We calculate the mean of the fair sample and then blend it with immediate price by using the following weighting:

    (mean * 0.3) + (price * 0.7)

    This heavily ends up favoring responsiveness when a continuous trend starts to pick up, effectively bringing the stop-loss closer to the active price. With that addressed we now move to the network filter and dynamic array shifting. The predictive filter is operated by two functions. The first is 'UpdateLinReg()' that maintains a rolling window of prices whereby if the 'm_lr_prices' array is full, it executes a for-loop that shifts all the array elements by one index to the left:

    m_lr_prices[i] = m_lr_prices[i + 1]

    This makes more room with which to append at the very end as follows:

    m_lr_prices[m_lr_period - 1] = price

    Secondly, the 'GetLinRegPrediction()' function computes the Ordinary Least Squares vector, and this is listed as follows:

    //+------------------------------------------------------------------+
    //| Linear Regression Prediction (Y = mX + b)                        |
    //+------------------------------------------------------------------+
    double CTrailingReservoirLinReg::GetLinRegPrediction()
      {
       if(m_lr_count < m_lr_period)
          return m_lr_prices[m_lr_count - 1];
       double sum_x = 0, sum_y = 0, sum_xy = 0, sum_x2 = 0;
       int n = m_lr_period;
       for(int i = 0; i < n; i++)
         {
          double x = (double)(i + 1);
          double y = m_lr_prices[i];
          sum_x  += x;
          sum_y  += y;
          sum_xy += x * y;
          sum_x2 += x * x;
         }
    //--- Calculate Slope (m) and Intercept (b)
       double denominator = (n * sum_x2) - (sum_x * sum_x);
       if(denominator == 0.0)
          return m_lr_prices[n - 1];
       double m = ((n * sum_xy) - (sum_x * sum_y)) / denominator;
       double b = (sum_y - (m * sum_x)) / n;
    //--- Predict the *next* data point in the series (X = n + 1)
       return m * (n + 1) + b;
      }

    This network model performs iterations to work out the summations ('sum_x', 'sum_y', 'sum_xy', 'sum_x2'). Importantly, we also have a division-by-zero protection check:

    if(denominator == 0.0)

    Where if this check fails we simply return the last known price in order to avert runtime crashes. After this we then solve Y = mX + b, in order to predict the next Y or price data point when X is n + 1. Execution then checks the result of the following condition:

    //--- 1. Linear Regression Network (Predictive Filter)
       if(m_use_linreg)
         {
          UpdateLinReg(price);
          // If the trend is predicted to rise above current price, hold stop
          if(m_lr_count >= m_lr_period && GetLinRegPrediction() > price)
             return false;
         }

    If the network forecasts a vector that moves against the current position for instance if Y < current price for a buy position or Y > current price for shorts, the trailing stop adjustment would halt and the suggested base price would be ignored. In addition we also have extra protection to avert invalid stops. This can be pivotal because any well intentioned trailing stop system that only yields errors when trying to adjust the stop loss would not be worth much. We minimize the likelihood of this error by wrapping the execution logic in a dynamic boundary that observes the broker's absolute limits, as follows:

    //--- 3. Execution Logic
       double new_sl = NormalizeDouble(fmin(base_price, m_symbol.Bid()) - (((2 * m_symbol.Spread()) + m_symbol.FreezeLevel() + m_symbol.StopsLevel()) * m_symbol.Point()), m_symbol.Digits());
       double current_sl = position.StopLoss();
       sl = EMPTY_VALUE;
       tp = EMPTY_VALUE;
       if(new_sl > current_sl || current_sl == 0.0)
         {
          sl = new_sl;
          return true;
         }

    Rather than resorting to a static 'if' clause, we use:

    fmin(base_price, m_symbol.Bid())

    That would be applicable with long positions, to force our model to choose a lower price value. That way if our reservoir base price was above this value, we do not run into conflicts. As an extra best-practice step, we also deduct the stops level, the freeze level and at least twice the spread from this least price. This way we are better positioned to handle any slippage that characterizes news releases and major macro events. These values are stored as integers within the symbol class so, we have to multiply them by a point size in order to get them to price units. Finally we normalize the computed value to have only as many decimals as those required by the symbol whose stop loss is being adjusted, also a best-practice that prevents the invalid price error. 

    If we had a short position, the logic would cleanly mirror this protection step by using:

    fmax(base_price, m_symbol.Ask())

    This gets us the highest valid possible ceiling, again averting from possible modifications that would give prices below this that would be invalid. As above we also add broker required price distances instead of subtracting them as we did with the long position. By putting caps on the output of the reservoir algorithm we are setting up our price to meet current market reality. The mandatory broker price buffers then serve to almost 'bullet-proof' our Expert Advisor from invalid stops errors. In theory, this lets the Expert Advisor trail positions and halt updates only when the Linear Regression filter flags the next interval.


    Test Results and Analysis

    In order to show the merits of this trailing stop we test an Expert Advisor with the symbol EUR JPY over our typical period spanning from 2025.01.01 to 2026.05.01 on the 4-hour timeframe. As has been the case, we use the year 2025 for optimization while the forward walk is done for the first 4 months of 2026. In performing these tests we not only want to get a sense of our model's efficacy but also how its two main parts, the algorithm and the network, can fare independently. The network cannot be used alone, so the two modes we test in are with just the algorithm and then with the algorithm and the network.

    Reservoir Algorithm

    In our first test run, the Expert Advisor was set up to use only the algo with the parameter 'Trailing_ReservoirLinReg_UseLinReg' assigned to false. From the optimization over 2025, we picked '20' to be the ideal reservoir size. Our best use mode for the algorithm was also tuned to 1, of the available 4. With these main settings, and other typical considerations for opening and closing thresholds, the forward walk in the year 2026 elicited 8 trades in total, a decent profit factor of 3.03 bringing in a net profit of USD 545.83. This profit came at the back of a drawdown of 4.84% which amounts to a numerical value of just over USD 480. 

    r1

    It is thus apparent that without a predictive filter, the pure reservoir's mean is reasonably profitable; however, it could absorb more pain when we have sudden pullbacks as is bound to happen over a more extensive testing window. Even though it trails as expected, one can argue that it lacks 'forward-looking awareness', necessary to halt stop-loss adjustment in special situations which is where the network comes into play.

    Linear Regression Network

    In the second run, we activate the network filter by having the input parameter 'Trailing_ReservoirLinReg_UseLinReg' set to true. After adapting to this new logic, from the optimization we can see that the reservoir pool size expands to 56 data points implying a wider statistical base while the operation mode was maintained at 1 (of the available 4). Also the selected linear regression period was 83. 

    r2

    These changes, that chiefly are the inclusion of the network, led to an increase in the total number of trades placed, a reduction in net profit to USD 391.20, a reduction in the profit factor to 2.57, and perhaps beneficially a reduction in the equity drawdown to 4.46% or about USD 458. While these results on the face of it may seem like a downgrade, the improved equity drawdown I think invites more testing and fleshing out over longer periods and with a wider pool of trade symbols.


    Conclusion

    To sum up, algorithmic trading does not have a 'universal' solution out of the box to trailing stop placement. Contrasting our approach here to what we had in the Skip-List and Hopfield network article highlights the case for matching mathematical tools to specific market conditions. While associative memory from the Hopfield network did excel in fragmented, gap-heavy chaotic markets that aggressive or less-risk-averse traders may favor; the statistical normalization provided by the reservoir sampling could be better suited for trend-followers particularly in continuous algorithm-heavy markets.

    By maintaining a probability based fair sample of price data in RAM we bypass significant compute overhead that is common in large data arrays. Pairing this with predictive smoothing of a Linear Regression filter results in a strategy that swapped higher net profit for less drawdown and higher Sharpe ratio. It is thus apparent that finding the best model should not be the objective but rather correctly diagnosing market conditions and using the algorithm/network better suited to exploit them, could be a better approach.


    name description
    wz_94.mq5 Wizard Assembled Expert Advisor
    TrailingReservoirLinReg.mqh Custom Trailing Class necessary for Wizard Assembly
    r1.set Input settings in 1st Test Run
    r2.set Input Settings in 2nd Test Run
    Expert Advisor was assembled with MQL5 library entry signals of Envelopes and RSI
    Attached files |
    MQL5.zip (8.11 KB)
    Quantum Neural Network in MQL5 (Part I): Creating the Include File Quantum Neural Network in MQL5 (Part I): Creating the Include File
    The article presents a new approach to creating trading systems based on quantum principles and artificial intelligence. The author describes the development of a unique neural network that goes beyond classical machine learning by combining quantum mechanics with modern AI architectures.
    Step-by-Step Implementation of a Local Stop Loss System in MQL5 Step-by-Step Implementation of a Local Stop Loss System in MQL5
    This article shows how to build a local stop-loss system in an MQL5 Expert Advisor that keeps stop levels on the terminal side. It walks through the execution logic, event handlers, inputs, and an OOP design using CTrade, CPositionInfo, CHashMap/CHashSet, and chart objects. You will implement multi-position tracking, draggable stops, visual spacers and labels, plus cleanup and disconnection behavior to create a practical risk-control utility.
    Features of Experts Advisors Features of Experts Advisors
    Creation of expert advisors in the MetaTrader trading system has a number of features.
    CSV Data Analysis (Part 2): Building a Production-Grade CSV Export and Parsing Pipeline for Quantitative Strategy Analysis CSV Data Analysis (Part 2): Building a Production-Grade CSV Export and Parsing Pipeline for Quantitative Strategy Analysis
    MQL5's file system operates within a strict sandbox. Understanding its access flags and path resolution rules is the foundation of any reliable export pipeline. This article builds a CCSVExporter class that handles file creation, safe appending, and error recovery. It also covers CSV parsing, field tokenization, concurrent access conflicts, and write-buffering strategies for high-frequency optimization runs.