preview
MQL5 Wizard Techniques you should know (Part 100): Sliding Window Median and Bidirectional LSTM for a Custom Trailing Stop

MQL5 Wizard Techniques you should know (Part 100): Sliding Window Median and Bidirectional LSTM for a Custom Trailing Stop

MetaTrader 5Trading systems |
191 0
Stephen Njuki
Stephen Njuki

Introduction

We resume our deep dive into MQL5 where we pitch new ideas and models that can be prototyped easily and proven thanks to the MQL5 Wizard. In this series we rotate between implementations of trailing stops, entry signals, and money management. The last article we published was money management so we look at a trailing stop model for this article. Within the articles where we have only looked at trailing stops, we have utilized algorithms such as Skip Lists, Reservoir Sampling, and the Convex Hull. These tools were very broad and not identical however they did tend to map persistent, long-term macroeconomic trends and were adept in "Z-type" (Whipsawed) markets.

Today, we consider a different model that is powered by a Sliding Window Median aka Hampel Filter and a Bidirectional LSTM neural network. In all these articles we do not aim to give the silver bullet, but rather try to expand the MQL5 Wizard toolkit by introducing a special situation model, that we can contrast with previous models already covered. We are making the case that algorithmic trading success could be dependent on pairing the right tools to the "right market" as perceived by the trader who is using this model(s).

Past Foundations: What was Covered

Previously on our journey, we looked at the interplay of memory-intense data structures and recurrent neural networks. We have used Skip Lists for efficient search-and-sort; Reservoir Sampling to manage and better track representative subsets of massive datasets; and recently the Convex Hull algorithm that helped map geometric boundaries around price action.

These algorithms were matched with the neural networks: Hopfield, Linear-Regression, and GRU respectively that we engaged because of their ability to make sense of persistent historical patterns. The pairings were in essence a "Macro-Perspective" engine. On the whole, we can say they are good at spotting long-term structural integrity in market data. They store deep history and observe the trajectory price distribution envelopes. These tools give a stable, albeit slightly lagging, anchor for trailing stops. 

Nonetheless, these approaches were a bit too granular. For instance in the last custom trailing class, the Convex Hull algorithm when on smaller timeframes (or high-frequency) may not tighten effectively, given the fluid setting, and this could lead to "giving back" profits. In situations where the market changes faster than the Reservoir-based memory has time to reorganize, our model today can be helpful.

Target: Environment

This model should on paper thrive in "Z-type" markets. Which traders could use this? Intraday scalpers, breakout specialists, and liquidity sweep hunters. If a trader is waiting for rapid momentum bursts or is trying to fade institutional stop-hunts then this model would fit his reality.

"Z-type" markets are marked by high-frequency noise and sudden liquidity spikes. Whereas we have looked at long-term trend environments we studied previously, these have the premise that they oscillate around a mean, and then breaking through standard support/resistance levels before mean reverting. In this environment, it can be argued that a "macro-perspective" that was a feature in the previous models - where filtering out short-term noise was important - would be a hindrance here. In this setting we do not need to ignore noise or create a filter that represents structural change or a transient outlier. Today's algorithm is intended to assist Z-type traders identify which price action needs tighter stops from the "fake-out" wicks that need to be ignored.

The "Whipsaw" Trap

Regular trailing stops fail often because they treat every price tick equally. They are not able to separate cleanly structural change from transient noise. By using the Sliding Window Median, we try to address this by viewing the market as a distribution instead of a sequence of static points. We essentially query whether "the current price bar is statistically consistent or an anomaly?" If we can identify and isolate anomalies, we would be in a position to protect the "real price action" and thus avoid prematurely adjusting the stop loss which can result in exiting winning positions too soon.


Model Description

In order to spot these price anomalies we are maintaining the dual-architecture approach of algorithm and neural-network. With the complete source code attached below, our model can always be tweaked to add other algorithms and networks. The MQL5 Wizard only allows a solo trailing stop system, unlike entry signals that can be multiple and get to "vote" on whether to go long or short. This is why tweaking our custom class is the best alternative for readers looking to incorporate extra ideas on how the trailing stop is placed/adjusted. For our part though we have 2 engines.

Engine 1: The Algorithm

Our first engine is essentially the "shock absorber". It works by using a sliding window of recent prices. The main goal here is robust statistical rejection. By working out the Median and the Median Absolute Deviation (MAD) of the price window, we get a dynamic, adaptive "safe zone" that a stop loss could be placed. With this algorithm we do not dwell on trade direction, only the validity of the price data. When a price-bar is outside the calculated statistical bound the Hampel filter would mark it as an outlier.  This implies it gets ignored and we do not process the next stop loss by considering it.

Engine 2: The Network

When the Hampel filter is absorbing shocks, the BiLSTM would be "navigating". An LSTM is a Recurrent Neural Network (RNN) that is intended to handle sequential data. A  bi-directional LSTM is even more potent. This variation processes the sliding window twice. The first time is forward in chronological time, and then the second time is in reverse. By bringing together these two perspectives, this network creates a contextual "trend score" that is range-bounded [-1, 1].

The BiLSTM is used to track the market heartbeat. When this network spots that price momentum is sizeable, say a structural reversal instead of noise, this information gets passed onto our model, to approve any adjustments to the trailing stop that would have been already picked up by the algorithm. On the flipside when the BiLSTM sees prevalent momentum as aligned with the current open position, it would confirm the Hampel outlier rejection and the Expert Advisor would accordingly adjust the trailing stop. This method prevents the trailing stop from being "too stubborn" in actual market shifts.

Possible Metaphors

To better visualize how these two engines synergize, we could think of the market as a car maneuvering on a road with many potholes. In this analogy, the vehicle would be a trader's open position with the road being the influx of price ticks.

The Hampel Filter (Shock absorber):

As one drives over a pothole, for the typical car the tires should drop into the ditches before jolting up. In a market, such momentary shocks that do not change overall trend can be referred to as liquidity wicks. When one's chassis (trailing stop) is rigidly bolted to every tire, this shock can prematurely stop the car's drive. The Hampel filter serves as an advanced shock absorber. The sudden impact of the potholes (outlier ticks) is buffered from the chassis (stop loss) by maintaining it level and stable. Our algorithm does not steer the car, we simply avert damage from road imperfections.

The BiLSTM (Navigating Front and Rear GPS):

While the shock absorber manages unexpected impacts, the BiLSTM acts as our nav-system. We not only look at our current position, but we also look at the rearview camera (backward pass) to learn how we got to our current point, and a front-facing (forward pass) to anticipate our likely next trajectory. For instance if this nav-system detects that we are not in a pothole environment, then the shock absorbers do get tuned to be more relaxed and less shock averse, or if we are at a sharp bend and need to change course. 

These two thus work with a synergy where the vehicle knows how to ignore the road's noise and when to also acknowledge that the landscape has considerably changed.


Model's Formulae

Before we build this dual engine, it is always informative to translate the "shock absorber" and "navigator" into concrete mathematics.

Algorithm: Sliding Window & MAD

We define our sliding window over prices that are recent as an array P of size N. When we sort this array, we can easily retrieve the median value, m, as the middle value or the mean of the two middle values when N is even. Unlike a moving average, medians are not swayed by outlier wicks. Once we have done this, we then calculate the Median Absolute Deviation (MAD) by getting the absolute difference between every price in P and our median m. We would then proceed to get the median of these differences as follows:

f1

Where:

  • MAD is the Median Absolute Deviation, where it is our outlier-resistant metric of the present market volatility in the sliding window
  • Median is a Math function that sorts an array of values, isolating middle value while ignoring the extremums
  • p_i stands for a single price p data point i
  • m is the median of the sliding window
  • |p_i - m| stands for the absolute distance between every price bar and the window's median.

Our acceptable price boundary is defined as

f3

With K typically set to 3. Any price bar that falls outside this statistical bound would be rejected as an anomaly.

The Network: BiLSTM

To give directional context, the BiLSTM processes the data window P in two directions. This is done via regular LSTM logic of: forget, input, and candidate cell states. The forward pass iterates chronologically from t is 1 up to t is N giving a forward hidden state, h. Subsequently, the backward pass iterates in reverse chronological order from t = N down to t is 1, giving us a backward hidden state -h. We apply the hyperbolic tangent function to restrict the range of these memory states. The final context trend score (T) takes the mean of both the forward and backward pass:

f2

Where:

  • T is the trend score, our final contextual output of the Expert Advisor. Given the use of the hyperbolic tangent function our inputs are bound to the range [-1.0, +1.0]
  • h is the forward hidden state. It is the final memory output got by the LSTM from chronological evaluation
  • arrow-h is the backward hidden state, the final memory output from the LSTM got by reverse chronological order.


MQL5 Implementation

We now transition from theory to working code. Building a testable dual-engine trailing stop class would usually require a lot of careful coding, to avoid not just syntax bugs, which sometimes can hide even with compilation, but more importantly to avoid logical bugs. We are not merely using a moving average, but rather we are building a highly adaptive array-processing algorithm and a sequential memory network all combined into a custom class 'CTrailingSlidingMedianBiLSTM'. The relatively complex model we have means we need to be inheriting from already coded classes in order to avoid unnecessary syntax and logical bugs. Before we go over the code though, let us look at the indicator selection for this model.

Indicator Rationale

Before we can use the arrays introduced above, we have to initialize our indicators that form part of their input data. If we had the Hampel filter only depending on raw price data, it would be working in a vacuum. To better provide context  we introduce two indicators. The Bollinger Bands and the Relative Strength Index. These two are almost repetitive to our choice of entry signals, however they are picked for their ability to manage in Z-type environments, where as introduced above they are characterized by high-frequency mean reversion and abrupt volatility expansions. In these situations it would make sense to use standard deviation boundaries and momentum exhaustion metrics.

Bollinger Bands are by design linked to standard deviation. When the markets are calm, its bands compress. On the other hand news releases are marked by the expansion of these bands. From this indicator, therefore, we use the Bands' width to dynamically scale our Hampel's filter rejection threshold. When the market is naturally volatile, our "shock absorber" would need loosening up, when the markets are calm or too quiet it would be tightened.

The RSI is used as a directional momentum gauge. When we get a wick outlier at 80, a sign the markets could be overbought, this has very different implications than if the RSI is at 50 (neutral territory). We therefore use the RSI to move the "center of gravity" of our trailing stop.

To have the Hampel filter implementation, we cannot depend on standard time-series buffers. We need to build our own sliding window that acts by First-In-First-Out (FIFO) logic when queuing price data. We achieve this in the following three steps:

Step 1: Managing the Window

//+------------------------------------------------------------------+
//| Update Sliding Window Buffer                                     |
//+------------------------------------------------------------------+
void CTrailingSlidingMedianBiLSTM::UpdateWindow(double price)
  {
   if(m_samples_seen < m_window_size)
     {
      m_price_window[m_samples_seen] = price;
      m_samples_seen++;
     }
   else
     {
      for(int i = 0; i < m_window_size - 1; i++)
        {
         m_price_window[i] = m_price_window[i + 1];
        }
      m_price_window[m_window_size - 1] = price;
     }
  }

The first thing we do is manually shift the array where for every new 'price' (whether bid or ask) that we receive, we check whether our window ('m_window_size', at 20 periods by default) is full. If it still has space, we fill it. Once it is filled we run the 'for-loop'  by shifting every element by one index to the left, discarding the oldest price and putting the latest price at the very end ('m_window_size - 1'). The array parameter 'm_price_window' represents our continuous road surface that our "shock absorber" analyzes.

Step 2: The Robust Median

//+------------------------------------------------------------------+
//| Algorithm Math: Get Median of an Array                           |
//+------------------------------------------------------------------+
double CTrailingSlidingMedianBiLSTM::GetMedian(const double &arr[])
  {
   int count = MathMin(m_samples_seen, m_window_size);
   if(count == 0)
      return 0.0;
   double temp[];
   ArrayResize(temp, count);
   ArrayCopy(temp, arr, 0, 0, count);
   ArraySort(temp);
   if(count % 2 == 0)
      return (temp[count / 2 - 1] + temp[count / 2]) / 2.0;
   return temp[count / 2];
  }

This method is also important. Why not use an average? Because just one huge liquidity spike can pull the mean significantly from the real structural price. The median is more statistically robust against outlier extremes. Noteworthy as well are the MQL5 mechanics we are using here, where we pass the array by reference 'const double &arr[]'. This is memory efficient. We copy the 'temp[]' array given that in order to get the median, we have to sort the array in ascending order via 'ArraySort()'. Changing the previous sliding window array would alter the chronological sequence, and this would ruin our inputs to the BiLSTM. When the count is even, we get the average of the two middle values; if it is odd we simply return the middle value.

Step 3: Median Absolute Deviation (MAD)

//+------------------------------------------------------------------+
//| Algorithm Math: Get Median Absolute Deviation (MAD)              |
//+------------------------------------------------------------------+
double CTrailingSlidingMedianBiLSTM::GetMAD(const double &arr[], double median)
  {
   int count = MathMin(m_samples_seen, m_window_size);
   if(count == 0)
      return 0.0;
   double deviations[];
   ArrayResize(deviations, count);
   for(int i = 0; i < count; i++)
     {
      deviations[i] = MathAbs(arr[i] - median);
     }
   return GetMedian(deviations);
  }

Our MAD listing above is the heart of the Hampel filter. We iterate through the price window, working out the absolute distance ('MathAbs()') for every price point from the prior computed median. We store these distances in a new array 'deviations[]'. At the end, we recursively call the 'GetMedian()' function on these new deviations. Doing this gives us the "median of deviations" that we use as a robust and outlier-resistant metric to current market volatility. Our Hampel algorithm can be executed in up to four different modes. The mode to be used is chosen by the user via the input parameter 'm_hampel_mode'.

Implementing the Algorithm

Mode 1: The Standard Hampel Filter:

//+------------------------------------------------------------------+
//| Mode 1: Standard Hampel Filter (Pure Price Action Rejection)     |
//+------------------------------------------------------------------+
double CTrailingSlidingMedianBiLSTM::HampelModeStandard(double price, bool is_long)
  {
   if(m_samples_seen < 3)
      return price;
   double median = GetMedian(m_price_window);
   double mad = GetMAD(m_price_window, median);
//--- K factor of 3 is standard for Hampel filter outlier rejection
   if(MathAbs(price - median) > 3.0 * mad)
      return median; // Reject outlier, trail on median
   return price;
  }

Our first mode is a raw statistical rejection where we work out the 'median' and the 'MAD', with the core logic being embedded in the closing 'if' statement. When the absolute difference between the current live tick and the historical median is above '3.0 * MAD' (standard K-factor for Hampel), the algorithm would mark this as a price anomaly. Instead of returning the live price to the trailing stop, it returns the 'median', thus protecting the stop loss.

Mode 2: Bollinger Band Outlier Rejection:

//+------------------------------------------------------------------+
//| Mode 2: BB Outlier Mode (Rejects spikes outside Volatility Bands)|
//+------------------------------------------------------------------+
double CTrailingSlidingMedianBiLSTM::HampelModeBands(double price, bool is_long)
  {
   if(m_samples_seen < 3)
      return price;
   m_bands.Refresh();
   double bb_upper = m_bands.Upper(1);
   double bb_lower = m_bands.Lower(1);
   double median = GetMedian(m_price_window);
//--- If price spikes outside the Bollinger Bands, it's considered an extreme outlier
   if(is_long && price < bb_lower)
      return median;
   if(!is_long && price > bb_upper)
      return median;
   return price;
  }

For traders that prefer regular standard deviation over MAD, this mode substitutes the mathematical limits of the Bollinger Bands' bounds. If one is trailing a Long position, and price abruptly wicks beneath the bottom Bollinger Band ('price < bb_lower'), the system would reject the latest price and anchor the trailing stop to the sliding median.

Mode 3: RSI Momentum Bias:

//+------------------------------------------------------------------+
//| Mode 3: RSI Momentum Bias (Median Shift)                         |
//+------------------------------------------------------------------+
double CTrailingSlidingMedianBiLSTM::HampelModeRSI(double price, bool is_long)
  {
   if(m_samples_seen < 3)
      return price;
   m_rsi.Refresh();
   double rsi_val = m_rsi.Main(1);
   double median = GetMedian(m_price_window);
   double mad = GetMAD(m_price_window, median);
//--- Shift the acceptable median bound dynamically based on momentum
   if(is_long)
     {
      if(rsi_val > 70.0)
         return price;                  // Allow wide trail in bullish momentum
      if(rsi_val < 30.0)
         return median - (1.5 * mad);   // Tighten to median bottom bound
     }
   else
     {
      if(rsi_val < 30.0)
         return price;                  // Allow wide trail in bearish momentum
      if(rsi_val > 70.0)
         return median + (1.5 * mad);   // Tighten to median top bound
     }
   return median; // Default to neutral median
  }

This mode is meant for trend exhaustion setups. If a trader is Long and the RSI pushes north of 70, the market would be in strong momentum. This in theory would allow the trailing stop to track price normally. However, when the RSI drops below 30, with momentum turning bearish, we would proactively tighten the defensive posture. Rather than returning the median as the suitable stop, we aggressively move the anchor to the bottom of the median bound ('median - (1.5*MAD)'). This locks in profits before the reversal completely materializes.

Mode 4: Adaptive Fusion:

//+------------------------------------------------------------------+
//| Mode 4: Adaptive Fusion (BB Width scaling + RSI Output)          |
//+------------------------------------------------------------------+
double CTrailingSlidingMedianBiLSTM::HampelModeAdaptive(double price, bool is_long)
  {
   if(m_samples_seen < 3)
      return price;
   m_bands.Refresh();
   m_rsi.Refresh();
   double bb_upper = m_bands.Upper(1);
   double bb_lower = m_bands.Lower(1);
   double rsi_val = m_rsi.Main(1);
   double median = GetMedian(m_price_window);
   double mad = GetMAD(m_price_window, median);
//--- Calculate Bandwidth to act as dynamic 'K' threshold scaler
   double bandwidth = (bb_upper - bb_lower) / m_symbol.Point();
   double k_factor = fmax(1.0, bandwidth / 100.0);
   if(MathAbs(price - median) > k_factor * mad)
     {
      // If it breaks the adaptive MAD threshold, apply RSI directional bias
      return is_long ? (median + (rsi_val / 100.0 * mad)) : (median - ((100.0 - rsi_val) / 100.0 * mad));
     }
   return price;
  }

This mode could be the crown jewel of this algorithm. With this mode, we fuse both the indicators. First though, we work out the Bollinger 'bandwidth' that we use to dynamically work out the 'k_factor'. This replaces the static '3.0' in Mode-1. When markets are expanding wildly, the 'k_factor' grows and this widens acceptable limits. In a market that is contracting, it would shrink. When an outlier is spotted breaking this dynamic bound, we do not simply return the median but we apply an 'RSI-directional-bias'. We apply the RSI percentage (e.g. 'rsi_val/100.0') to move the median perfectly in line with the underlying momentum's exhaustion state. 

Implementing the Network

While the Hampel filter manages the immediate, high-frequency shocks, we get our overarching context from the Bidirectional LSTM. We initialize this as follows:

//+------------------------------------------------------------------+
//| Initialize Deterministic Weights for BiLSTM Filter               |
//+------------------------------------------------------------------+
void CTrailingSlidingMedianBiLSTM::InitBiLSTMWeights()
  {
//--- Forward emulation weights
   Wf = 0.6;
   Uf = 0.3;
   bf = 0.1;
//--- Backward emulation weights
   Wb = -0.5;
   Ub = 0.4;
   bb = 0.05;
  }

For our testing purposes, for this Expert Advisor to allow immediate backtesting compilation, we hardcode deterministic weights into it. In a production setting these weights would be loaded from Python (TensorFlow/ PyTorch) into MQL5 via a JSON array or a CSV file. The 'W' stands for our input weight; the 'U' is for the hidden recurrence weight, and 'b' is the bias. We have a set for the forward pass ('Wf') and another set for the backward pass ('Wb').

//+------------------------------------------------------------------+
//| Calculate Network Sequence (Returns output -1 to 1)              |
//+------------------------------------------------------------------+
double CTrailingSlidingMedianBiLSTM::GetBiLSTMTrend()
  {
   if(m_samples_seen < m_window_size)
      return 0.0;
   double h_forward = 0.0;
   double c_forward = 0.0;
   double h_backward = 0.0;
   double c_backward = 0.0;
//--- 1. Forward Pass
   for(int i = 0; i < m_window_size; i++)
     {
      double x_f = (m_price_window[i] - m_price_window[0]) / m_symbol.Point();
      double f_gate = Sigmoid((Wf * x_f) + (Uf * h_forward) + bf);
      double i_gate = Sigmoid((Wf * x_f) + (Uf * h_forward) + bf);
      double c_candidate = MathTanh((Wf * x_f) + (Uf * h_forward) + bf);
      c_forward = (f_gate * c_forward) + (i_gate * c_candidate);
      h_forward = MathTanh(c_forward);
     }

Above is our core network loop. The forward pass reads our sliding window chronologically ('from i = 0 to m_window_size'). We begin by normalizing the input price 'x_f' by calculating its deviation from the starting price in the sliding window, that is divided by 'Point()'. This helps convert the price into a standard  point-delta, and this is vital for neural network stability. Following this, we compute the LSTM gates. The forget gate ('f_gate') sets how much of the old memory to discard. The input gate ('i_gate') sets what proportion of the new 'c_candidate' info should be accepted. We use the Sigmoid function to squash the output to a range [0, 1]. With this, we update the cell state ('c_forward'), that acts as the long-term memory conveyor belt. Finally we calculate the hidden state ('h_forward') by using MathTanh that stands for the immediate, short-term momentum output.

//--- 2. Backward Pass
   for(int i = m_window_size - 1; i >= 0; i--)
     {
      double x_b = (m_price_window[i] - m_price_window[m_window_size - 1]) / m_symbol.Point();
      double f_gate = Sigmoid((Wb * x_b) + (Ub * h_backward) + bb);
      double i_gate = Sigmoid((Wb * x_b) + (Ub * h_backward) + bb);
      double c_candidate = MathTanh((Wb * x_b) + (Ub * h_backward) + bb);
      c_backward = (f_gate * c_backward) + (i_gate * c_candidate);
      h_backward = MathTanh(c_backward);
     }
//--- 3. Output Concatenation/Average
   return (h_forward + h_backward) / 2.0;
  }

The brilliance of the Bidirectional architecture lies in the Backward Pass. We run the same logic, but with the loop reversed:

for i = m_window_size - 1; i >= 0; i--

Essentially we are asking the network to check the present, and read backward into the past. This gives us a very different contextual gradient. Finally, we bring together the two views by finding their mean. <code: '(h_forward + h_backward) / 2.0'/>. Given that both these outputs go through 'MathTanh', the final returned score is strictly bound to [-1.0, +1.0] that represent volatile confirmed trend and volatile confirmed uptrend respectively. What follows next would be merging the algorithm and network into a model within the Long/Short base class functions.

//+------------------------------------------------------------------+
//| Check Trailing Stop Long                                         |
//+------------------------------------------------------------------+
bool CTrailingSlidingMedianBiLSTM::CheckTrailingStopLong(CPositionInfo *position, double &sl, double &tp)
  {
   if(position == NULL)
      return false;
   double current_bid = m_symbol.Bid();
//--- 1. Update Sliding Window State
   UpdateWindow(current_bid);

First thing we do, is capture the live Bid price and immediately feed this into 'UpdateWindow()' in order to keep our chronological arrays current.

//--- 2. Network: Bidirectional LSTM Filter
   if(m_use_bilstm && m_samples_seen >= m_window_size)
     {
      // If BiLSTM predicts strong downward momentum, hold the stop
      if(GetBiLSTMTrend() < -0.4)
         return false;
     }

Next we get to the portion where the "navigator" intervenes. Before the algorithm does its math, the BiLSTM processes the landscape. If we are Long but then the BiLSTM returns a trend score of ' < 0.40', this would imply that the network sees a severe, structurally valid breakdown that is happening in the micro-structure. This result would force the function to return false, freezing the trailing stop, preventing a stoploss tightening that would have prematurely stopped out the position.

//--- 3. Algorithm: Select Hampel Filter Mode
   double base_price = current_bid;
   switch(m_hampel_mode)
     {
      case 1:
         base_price = HampelModeStandard(current_bid, true);
         break;
      case 2:
         base_price = HampelModeBands(current_bid, true);
         break;
      case 3:
         base_price = HampelModeRSI(current_bid, true);
         break;
      case 4:
         base_price = HampelModeAdaptive(current_bid, true);
         break;
      default:
         base_price = HampelModeStandard(current_bid, true);
         break;
     }

If the "navigator" gives the all-clear, we would pass the current price into our chosen Hampel "shock absorber". The switch statement would route the tick to the appropriate mode. When the tick is a genuine price move, 'base_price' would remain 'current_bid'. However when we have a liquidity spike 'base_price' would get changed into a safe 'median'.

//--- 4. Execution Logic
   double new_sl = NormalizeDouble(fmin(base_price, current_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;
//--- Only move stop up
   if(new_sl > current_sl || current_sl == 0.0)
     {
      sl = new_sl;
      return true;
     }
   return false;
  }

To conclude, we compute the actual Stop Loss level. Noteworthy is the use of  'fmin(base_price, current_bid)'. When the Hampel filter detects an anomaly, 'base_price' will be the lower median, which ensures that the computed 'new_sl' is sufficiently buffered from the wick. We also factor in the broker's spread, freeze levels, and stop levels. This avoids unnecessary order request rejections by the broker. We have to perform a verification that our new stop loss protects our profits without surrendering them and since in our case Long trailing stops only move upwards, we have 'new_sl > current_sl'. If this check is passed w assign the referenced 'sl' the value of our new stop loss and the function would return true. 


Post-Optimization Testing

To validate the working mechanics of the 'CTrailingSlidingMedianBiLSTM' class within the MQL5 environment, we ran sample tests on the EURUSD pair on the 2-hour timeframe from 2026.01.01 to 2026.05.01. It is always important to keep in mind that these reports are not definitive proofs of profitability, but strictly showcase usability, structural integration and the importance of independent diligence. 

Test Run 1: Algorithm only

r1

Test Run 2: Algorithm and Network

r2

Even though neither test had a profitable forward walk, it could be argued that the network-included forward run showed some value in limiting risk given that the net loss saved almost 10% of our starting balance when compared to the results of the first run. Used Signals for the Expert Advisor were RSI and Envelopes.


Conclusion

In this exploration we have tried to engineer a custom trailing stop by using the sliding window median (Hampel filter) for intermediary absorption of shocks, paired with a Bidirectional LSTM for overarching trend context. This dual engine mode was intended to navigate high noise "Z-type" markets where traditional rigid trailing stops could struggle to cope. 

We presented the argument for this and its MQL5 code, above, and progressed to test it over our standard test window that spans from the start of 2025 to the end of April 2026. Our testing that was intended to demonstrate usability was marked by failure to forward walk which we have noted points to the need for more independent testing on wider test windows across more test symbols before more definitive conclusions can be drawn. Nonetheless possible improvements on our model could be made by transitioning the deterministic BiLSTM weights into a dynamic loadable JSON configuration file. This could allow for live market retraining. Also factoring tick volume into the Hampel MAD calculations could make our shock absorber more volume-aware.

namedescription
wz_100.mq5Wizard Assembled Expert Advisor
TrailingSlidingMedianBiLSTM.mqhCustom Trailing Class prerequisite for Wizard Assembly
r1.setInput settings in first run
r2.setInput settings of second run

Attached files |
MQL5.zip (8.76 KB)
Creating an EMA Crossover Forward Simulation (Culmination): Interactive Synthetic Candles Creating an EMA Crossover Forward Simulation (Culmination): Interactive Synthetic Candles
This article finalizes the Forward Simulation Engine for MetaTrader 5 by calibrating synthetic candles to recent market volatility instead of using slope-only sizing. It samples average body, upper wick, and lower wick from closed bars, applies a sine-envelope with decay, proportional wicks, gaps between candles, and periodic counter-trend injections. The result is a live projection that advances one bar ahead, with code you can reuse for calibrated, anchor-based forward rendering and automatic cleanup.
Automatic Session Volume Profile Builder in MQL5: Rendering POC and Value Area Without Third-Party Tools Automatic Session Volume Profile Builder in MQL5: Rendering POC and Value Area Without Third-Party Tools
Implement a session-focused volume profile in MQL5: acquire ticks with CopyTicksRange(), bin prices, and compute POC, VAH, and VAL by the 70% approach. The indicator renders directly on the chart as native objects, supports fixed-width scaling for consistent geometry across timeframes, and refreshes on each new session. This provides objective reference levels without external dependencies.
Dream Optimization Algorithm (DOA) Dream Optimization Algorithm (DOA)
A population-based optimization algorithm inspired by a controversial and little-studied phenomenon - the mechanism of human dreams. Agent groups with different "memory", cosine-wave modulation of motion, and an unusual 99/1 phase distribution — learn how these features affect the optimization efficiency of your trading strategies.
Duelist Algorithm Duelist Algorithm
What if your trading strategies could learn from each other, like real fighters? Duelist Algorithm is a new optimization method where trading system parameters literally duel for the right to be called the best.