preview
Adaptive Malaysian Engulfing Indicator (Part 2): Optimized Retest Bar Range

Adaptive Malaysian Engulfing Indicator (Part 2): Optimized Retest Bar Range

MetaTrader 5Examples |
223 0
Chukwubuikem Okeke
Chukwubuikem Okeke

Introduction

You already have a working Malaysian Engulfing detector and a retest-confirmation routine, but a single practical problem blocks reliable scaling: the retest bar range cannot be “set once” for all markets and timeframes. On one symbol a fixed range captures valid pullbacks; on another it misses setups or admits noisy false confirmations. As volatility, momentum and structural rhythm change, manual tuning becomes trial-and-error and strategy performance drifts.

This article adds a data-driven adaptive layer that chooses the retest range objectively for the current context. Instead of guessing, we evaluate historical setups using MFE (Maximum Favorable Excursion) and MAE (Maximum Adverse Excursion) measured from a consistent entry rule, score candidate retest distances (avgMFE − avgMAE) with a constrained brute-force search over a single parameter, and select per-symbol/per-timeframe optimal values. The output is reproducible: optimalBullishRetestRange and optimalBearishRetestRange (and a run-time summary) that can be validated and applied automatically.

Adaptive Layer

Up to this point, the indicator successfully identifies Malaysian Engulfing patterns and tracks their pullback behavior using a fixed retest range. While this works in controlled conditions, it quickly breaks down in live markets where volatility, momentum, and structural rhythm vary across symbols and timeframes. A static parameter becomes a bottleneck—either too tight to capture valid retests or too loose, introducing noise and false confirmations.

This section addresses that limitation by introducing a self-adaptive layer that removes the need for manual parameter tuning or trial-and-error. Instead of assuming a universal retest range, the indicator evaluates how different ranges would have performed historically and selects the most effective one for the current market context. This adaptation is driven by MFE (Maximum Favorable Excursion) and MAE (Maximum Adverse Excursion), which provide objective performance measurements for each setup. By comparing how far price moves in favor of a trade versus against it, we establish a consistent basis for evaluating different retest configurations. 

This layer uses constrained brute-force optimization: it tests predefined retest ranges on historical setups, scores each range using MFE/MAE, and selects the best one. Because the problem is limited to a single parameter, this exhaustive approach remains efficient while guaranteeing an optimal choice within the tested bounds. While simple in design, this brute-force method is both transparent and effective. It guarantees that the chosen parameter is optimal within the tested bounds, making it well-suited for a single-variable problem like retest range selection—without manual intervention.

Having established our optimization approach, it is important to define the structure of the data being evaluated.

Every executed Malaysian Engulfing setup can be decomposed into a consistent set of measurable properties, which form the foundation of our MFE/MAE analysis and subsequent optimization:

Structure

Fig. 1. Adaptive Layer—Structure

  • Formation time: The exact timestamp at which the engulfing pattern is confirmed.
  • Zone high and low: Price boundaries of the engulfing structure, defining the retest zone within which pullbacks are validated.
  • Retest time: The moment price returns to the defined retest zone following pattern formation.
  • Retest bar range: The number of bars between formation and retest, representing the delay before validation.
  • MFE and MAE: The maximum favorable and adverse price excursions observed after entry, used to evaluate the effectiveness of a given retest range.

These metrics form the foundation of our adaptive evaluation model. However, to extract meaningful insights, they must be captured programmatically.

MQL5 Implementation

We now proceed to extend the Malaysian Engulfing indicator to support adaptive retest optimization. Rather than starting from scratch, we build directly on the retest validation indicator developed in the previous article. The adaptive layer is then incorporated through a series of incremental adjustments:

Step 1: Preprocessor directives

This step defines compile-time metadata and constants—such as program version (VERSION "2.00"), name identifiers, properties, and included libraries—so the indicator is properly configured, versioned, and reusable before compilation.

//+------------------------------------------------------------------+
//|                      Malaysian Engulfing - Self Optimizing.mq5   |
//|                                             © 2026, ChukwuBuikem |
//|                             https://www.mql5.com/en/users/bikeen |
//+------------------------------------------------------------------+
#property copyright "© 2026, ChukwuBuikem"
#property link      "https://www.mql5.com/en/users/bikeen"

#define VERSION "2.00"

#property version   VERSION
#property indicator_chart_window
#property indicator_plots 0

#include <ChartObjects\ChartObjectsShapes.mqh>

#define PROG_NAME "Malaysian Engulfing - Self Optimizing"
#define ZONE_BULL PROG_NAME + "BullishEngulfing"
#define ZONE_BEAR PROG_NAME + "BearishEngulfing"

Step 2: Data structures

The st_Engulfer structure, which neatly bundles all key information about an engulfing setup, is extended to include MFE, MAE, and the retest bar range (retestBars), as illustrated in Fig. 1. While the constructor ensures each new instance starts with safe default values for consistent tracking.

//--- Data Structure
struct st_Engulfer
  {
   ENUM_SYSTEM_STATE  state;
   datetime          time, retestTime;
   double            high, low;
   double   mfe,// Max favorable excursion
            mae;// Max adverse excursion
   int      retestBars;// Bars until retest

   //--- Constructor
                     st_Engulfer(): state(SEARCH_STATE), time(0), retestTime(0),
                                    high(EMPTY_VALUE), low(EMPTY_VALUE), mfe(EMPTY_VALUE),
                                    mae(EMPTY_VALUE), retestBars(INT_MIN) {}
  };

Step 3: Input parameters (configurable settings)

This section introduces adjustable inputs that let the user fine-tune how the adaptive optimization works—such as the time range for analysis, how far to search for retests, and the forward window for measuring MFE/MAE—alongside runtime preferences like zone colors and wick sensitivity, all without modifying the core code.

//--- Configurable parameters
input group "+== Optimization Settings ==+"
input datetime startDate =  D'2025.06.01';    // Analysis start date
input datetime endDate =  D'2025.07.18';      // Analysis end date
input int barsRetestRange = 50;               //Retest search horizon (bars)
input int maxLookahead = 20;                  //Forward window (bars) for MFE/MAE
input group "+== Runtime settings ==+"
input color bullishZoneColor = clrGreen;      //Bullish retest zone color
input color bearishZoneColor = clrRed;        //Bearish retest zone color
input int wickThreshold = 35;                 //Minimum wick ratio (%)

Step 4: Global variables

Next, our global state is modified to incorporate the MqlRates price series, set up arrays, and supporting variables. This enables persistent access to historical data and supports the optimization process for deriving the most effective bullish and bearish retest ranges within the specified date window.

//--- Global variables
int start = -1;
CChartObjectRectangle rect;
st_Engulfer bullishEngulfer, bearishEngulfer;
MqlRates rates[];
st_Engulfer bullishSetups[], bearishSetups[];
int bullishRetestIndex = -1, bearishRetestIndex = -1;
int optimalBullishRetestRange = -1, optimalBearishRetestRange = -1;
datetime dateStart = 0, dateEnd = 0;

Step 5: Helper functions

These routines handle the heavy lifting behind validation, measurement, and optimization. Rather than cluttering the main algorithm with repeated logic, they isolate key responsibilities such as retest confirmation, excursion tracking, and parameter evaluation into reusable, testable units. In this section, each helper function contributes to a specific stage of the adaptive pipeline:

  • Retest Validation

Retest validation scans the price series to find a qualified pullback. It rejects invalid breaks beyond the engulfing range, confirms zone interaction, and applies a wick-ratio filter. The result is a retest index and bar distance used for optimization.

//+------------------------------------------------------------------+
//|              Adaptive Layer                                      |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//|                Bullish retest validation                         |
//+------------------------------------------------------------------+
int getBullishRetestIndex(const int engIndex, const MqlRates &prices[],
                          const double engHigh, const double engLow, int &engRetest)
  {
//---
   int barIndex = -1;
   for(int w = 1; w <= barsRetestRange && (barIndex = engIndex - w) >= 0; w++)
     {
      //--- Check break
      if(prices[barIndex].close <= engLow)
         return -1;
      //--- Validate pullback
      if((prices[barIndex].low <= engHigh && prices[barIndex].close > engHigh)
         || (prices[barIndex].low <= engLow && prices[barIndex].close > engLow))
        {
         //--- Check lower wick ratio
         double candleRange = prices[barIndex].high - prices[barIndex].low;
         double ratio = (MathMin(prices[barIndex].open, prices[barIndex].close) - prices[barIndex].low) / candleRange;
         ratio = (NormalizeDouble(ratio, 2) * 100);
         //--- Utilize run-time wick parameter
         if(ratio >= wickThreshold)
           {
            engRetest = engIndex - barIndex;
            return barIndex;
           }
         return -1;
        }
     }
   return -1;
  }
//+------------------------------------------------------------------+
//|                Bearish retest validation                         |
//+------------------------------------------------------------------+
int getBearishRetestIndex(const int engIndex, const MqlRates &prices[],
                          const double engHigh, const double engLow, int &engRetest)
  {
//---
   int barIndex = -1;
   for(int w = 1; w <= barsRetestRange && (barIndex = engIndex - w) >= 0; w++)
     {
      //--- Check break
      if(prices[barIndex].close >= engHigh)
         return -1;
      //--- Validate pullback
      if((prices[barIndex].high >= engHigh && prices[barIndex].close < engHigh)
         || (prices[barIndex].high >= engLow && prices[barIndex].close < engLow))
        {
         //--- Check upper wick ratio
         double candleRange = prices[barIndex].high - prices[barIndex].low;
         double ratio = (prices[barIndex].high - MathMax(prices[barIndex].open, prices[barIndex].close)) / candleRange;
         ratio = (NormalizeDouble(ratio, 2) * 100);
         //--- Utilize run-time wick parameter
         if(ratio >= wickThreshold)
           {
            engRetest = engIndex - barIndex;
            return barIndex;
           }
         return -1;
        }
     }
   return -1;
  }
  • Excursion Computation

This function evaluates how far price moved in favor (MFE) and against (MAE) a setup after a retest, by scanning forward a defined number of bars from the entry point, updating the best favorable and worst adverse movements, and then converting the results into points for standardized comparison across setups.

//+------------------------------------------------------------------+
//|                   Compute MFE and MAE                            |
//+------------------------------------------------------------------+
void computeExcursions(const int retestIndex, const MqlRates &prices[],
                       const bool buySetup, st_Engulfer &destEng)
  {
//---
   double entry = NormalizeDouble(prices[retestIndex - 1].open, _Digits);// Entry price
   double mfe = 0, mae = 0;
   double favorableMove = 0, adverseMove = 0;
   int index = -1;

   for(int w = 1; w <= maxLookahead && (index = retestIndex - w) >= 0; w++)
     {
      if(buySetup)
        {
         favorableMove = NormalizeDouble(prices[index].high - entry, _Digits);
         adverseMove = NormalizeDouble(entry - prices[index].low, _Digits);

         if(favorableMove > mfe)
            mfe = favorableMove;
         if(adverseMove > mae)
            mae = adverseMove;
        }
      else
        {
         favorableMove = entry - prices[index].low;
         adverseMove = prices[index].high - entry;

         if(favorableMove > mfe)
            mfe = favorableMove;
         if(adverseMove > mae)
            mae = adverseMove;
        }

     }
//--- Convert to points
   destEng.mae = NormalizeDouble((mae / _Point), _Digits);
   destEng.mfe = NormalizeDouble((mfe / _Point), _Digits);
  }

Note: For consistency in evaluation, this adaptive layer assumes trade execution occurs at the open of the bar immediately following the retest bar. All MFE and MAE measurements are computed relative to this entry point.

  • Core Optimization Engine

Here, the engine applies a brute-force evaluation of all possible retest ranges by grouping historical setups according to their retestBars, computing the average MFE and MAE for each group, and scoring them via a reward–risk differential (avgMFE - avgMAE) to identify the most statistically favorable configuration.

//+------------------------------------------------------------------+
//|                 MFE and MAE analysis engine                      |
//+------------------------------------------------------------------+
int getOptimalRetestRange(st_Engulfer &eng[])
  {
//--- Best results tracking variable
   double bestScore = -DBL_MAX;
   int bestRange = 1;//--- Fallback

//--- Define variables / accumulators
   double totalMAE = 0, totalMFE = 0;
   double avgMAE = 0, avgMFE = 0;
   double score = 0;
   int count = 0;

//--- Try every possible retest bars value
   for(int r = 1; r <= barsRetestRange; r++)
     {
      //--- Initialize accumulators
      totalMFE = 0;
      totalMAE = 0;
      count = 0;
      //--- Scan dataset
      for(int w = 0; w < ArraySize(eng); w++)
        {
         //--- Group data
         if(eng[w].retestBars == r)
           {
            //--- Accumulate values for this group
            totalMFE += eng[w].mfe;
            totalMAE += eng[w].mae;
            count++;
           }
        }
      //--- Handle empty groups: Prevents division by zero
      if(count < 1)
         continue;
      //--- Compute averages
      avgMFE = totalMFE / count;
      avgMAE = totalMAE / count;

      //--- Edge: Compute score (exhaustive search))
      score = avgMFE - avgMAE;
      //--- Select best candidate
      if(score > bestScore)
        {
         bestScore = score;
         bestRange = r;
        }
     }
//--- Return best result
   return bestRange;
  }

Return value: The function returns the bestRange—the retest bar value that yields the highest score—representing the optimally balanced range where favorable excursion consistently outweighs adverse movement across the dataset.

  • Assemble Adaptive Model
This routine iterates through historical data to detect bullish and bearish engulfing setups with confirmed retests. It aggregates MFE/MAE (via computeExcursions()) and then runs brute-force optimization to find optimal retest parameters for buy and sell setups.
//+------------------------------------------------------------------+
//|                 Core adaptive layer engine                       |
//+------------------------------------------------------------------+
void buildAdaptiveModel(const MqlRates &prices[], st_Engulfer &buySetup[], st_Engulfer &sellSetup[],
                        int &optimalBuyParameter, int &optimalSellParameter)
  {
//---
   int index = -1;
   for(int w = ArraySize(prices) - (barsRetestRange + 2); (index = w + barsRetestRange) >= 0 && w >= 0 ; w--)
     {
      //--- Detect a perfect bullish engulfing pattern with valid retest
      if(prices[index + 1].close < prices[index + 1].open && prices[index].close > prices[index].open
         && prices[index].open <= prices[index + 1].close && prices[index].close > prices[index + 1].high
         && (bullishRetestIndex = getBullishRetestIndex(index, prices, prices[index + 1].high, prices[index + 1].low,
                                  bullishEngulfer.retestBars)) > -1)
        {
         //--- Resize array and append values
         if(ArrayResize(buySetup, ArraySize(buySetup) + 1))
           {
            computeExcursions(bullishRetestIndex, prices, true, bullishEngulfer);
            bullishEngulfer.time = prices[index].time;
            bullishEngulfer.high = prices[index + 1].high;
            bullishEngulfer.low = prices[index + 1].low;
            bullishEngulfer.retestTime = prices[bullishRetestIndex].time;
            buySetup[ArraySize(buySetup) - 1] = bullishEngulfer;
           }
        }
      //--- Detect a perfect bearish engulfing pattern with valid retest
      if(prices[index + 1].close > prices[index + 1].open && prices[index].close < prices[index].open
         && prices[index].open >= prices[index + 1].close && prices[index].close < prices[index + 1].low
         && (bearishRetestIndex = getBearishRetestIndex(index, prices, prices[index + 1].high, prices[index + 1].low,
                                  bearishEngulfer.retestBars)) > -1)
        {
         //--- Resize array and append values
         if(ArrayResize(sellSetup, ArraySize(sellSetup) + 1))
           {
            computeExcursions(bearishRetestIndex, prices, false, bearishEngulfer);
            bearishEngulfer.time = prices[index].time;
            bearishEngulfer.high = prices[index + 1].high;
            bearishEngulfer.low = prices[index + 1].low;
            bearishEngulfer.retestTime = prices[bearishRetestIndex].time;
            sellSetup[ArraySize(sellSetup) - 1] = bearishEngulfer;
           }
        }
     }
//--- Set optimal parameters
   optimalBuyParameter = getOptimalRetestRange(buySetup);
   optimalSellParameter = getOptimalRetestRange(sellSetup);
  }
  • Run-time Summary

After optimizing the parameters, this function prints a console report showing the indicator's current runtime details—such as version, symbol, timeframe, analysis period, and the computed optimal retest ranges for both buy and sell setups—so the adaptive results can be quickly reviewed during execution.

//+------------------------------------------------------------------+
//|                   Run-time summary display                       |
//+------------------------------------------------------------------+
void showRunSummary(const datetime sDate, const datetime eDate)
  {
   Print("==========================================");
   PrintFormat("[RUN] %s v%s", PROG_NAME, VERSION);

//--- Context
   PrintFormat("[DATA] Symbol: %s (%s) | Period: %s → %s",
               _Symbol,  EnumToString(_Period),
               TimeToString(sDate, TIME_DATE),
               TimeToString(eDate, TIME_DATE));

   PrintFormat("[RESULT] Optimal Retest Range (Buy):  %d bars", optimalBullishRetestRange);
   PrintFormat("[RESULT] Optimal Retest Range (Sell): %d bars", optimalBearishRetestRange);

   Print("==========================================");
  }
  • Date Range Clamping

User inputs should be validated to avoid malfunctions. In this step, the provided inputs(startDate , endDate) are first mapped into the global state(dateStart, dateEnd). The date range is then validated and clamped against the available historical data using SeriesInfoInteger, ensuring the analysis window stays within valid bounds while logging any adjustments made.

//+------------------------------------------------------------------+
//|                      Date input clamping                         |
//+------------------------------------------------------------------+
void clampDateRange()
  {
//---
   dateEnd = endDate;
   dateStart = startDate;

   datetime firstDate = (datetime)SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_FIRSTDATE);
   datetime lastDate = (datetime)SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_LASTBAR_DATE);
//--- Attempt to clamp dates
   if(dateStart < firstDate)
     {
      PrintFormat("[ADJUSTMENT]: Analysis start date -> %s (history start)",
                  TimeToString(firstDate));
      dateStart = firstDate;
     }
   if(dateEnd < firstDate)
     {
      MqlDateTime dt;
      TimeToStruct(firstDate, dt);
      dt.year += 1;
      dateEnd = StructToTime(dt);// Fall back: first date + one year
      PrintFormat("[ADJUSTMENT]: Analysis end date  -> %s ",
                  TimeToString(dateEnd));
     }

   if(dateEnd > lastDate)
     {
      PrintFormat("[ADJUSTMENT]: Analysis end date  -> %s (last bar)",
                  TimeToString(lastDate));
      dateEnd = lastDate;
     }
  }
//+------------------------------------------------------------------+

Step 6: Initialization (OnInit)

The adaptive layer runs in OnInit() because optimization should be completed before the indicator resumes its calculations. The initialization routine validates and clamps the user inputs against available market data, checks for logical and data-size errors (such as invalid dates or excessive lookback range), waits for full data synchronization, then loads price data with CopyRates before triggering the adaptive model to compute optimal retest parameters, followed by producing a final run-time summary.

//+------------------------------------------------------------------+
//|                 Core adaptive engine                             |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Validate and clamp inputs (if necessary)
   if(startDate <= 0 || endDate <= 0)
     {
      Print("Invalid date: zero or uninitialized");
      return INIT_PARAMETERS_INCORRECT;
     }
   if(startDate >= endDate)
     {
      PrintFormat("[INPUT ERROR]: Analysis start (%s) must be < end date (%s)",
                  TimeToString(startDate), TimeToString(endDate));
      return(INIT_PARAMETERS_INCORRECT);
     }
   clampDateRange();
   if(barsRetestRange <= 0)
     {
      PrintFormat("[INPUT ERROR]: Retest search horizon (%d) less than Zero(0) ", barsRetestRange);
      return(INIT_PARAMETERS_INCORRECT);
     }
   if((barsRetestRange * 3) >= (int)SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_BARS_COUNT))
     {
      PrintFormat("[INPUT ERROR]: Retest search horizon (%d) too large for available bars ",
                  barsRetestRange);
      return(INIT_PARAMETERS_INCORRECT);
     }
   while(!SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_SYNCHRONIZED) && !IsStopped())
     {
      Sleep(100);
     }
//--- Copy data and build adaptive model
   ArraySetAsSeries(rates, true);
   if(CopyRates(_Symbol, PERIOD_CURRENT, dateStart, dateEnd, rates) > 0)
     {
      buildAdaptiveModel(rates, bullishSetups, bearishSetups, optimalBullishRetestRange, optimalBearishRetestRange);
      ArrayPrint(bearishSetups);//--- Observation
      Print("Optimal bearish retest range: ", optimalBearishRetestRange);
     }
   return(INIT_SUCCEEDED);
  }

Observation

While the implementation remains under active development, the indicator can be deployed on a live chart for observation. By leveraging ArrayPrint() and Print() within OnInit(), we can inspect the computed data structures after initialization. When attached to XAUUSD with default settings, OnInit() runs validation, synchronization, and adaptive modeling. It then prints bearishSetups to the Experts log.

This allows us to verify that the adaptive engine is correctly identifying and storing retest ranges. A sample output from the log is shown below:

Adaptive Layer—Observation

Fig. 2. Adaptive Layer—Observation

The diagram above shows how getOptimalRetestRange() operates. Given a list of parameter sets, it evaluates each one and selects the configuration with the most optimal outcome, confirming that the function works as intended.

Having verified that the adaptive layer functions as intended, we integrate showRunSummary() into OnInit() to enable runtime summary reporting.

//+------------------------------------------------------------------+
//|                 Core adaptive engine                             |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Validate and clamp inputs (if necessary)
  . . . . . . 
   if(CopyRates(_Symbol, PERIOD_CURRENT, dateStart, dateEnd, rates) > 0)
     {
      buildAdaptiveModel(rates, bullishSetups, bearishSetups, optimalBullishRetestRange, optimalBearishRetestRange);
      showRunSummary(dateStart, dateEnd);
     }
   return(INIT_SUCCEEDED);
  }

Step 7: Core Engine (OnCalculate())

With the adaptive layer in place, only minimal modifications are required in the indicator’s iteration function (OnCalculate()). This adjustment enables the indicator to operate using the optimal retest range derived from the optimization process.

//+------------------------------------------------------------------+
//|             Core iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//---
   if(prev_calculated != rates_total && prev_calculated > 0)
     {
      start = (prev_calculated == 0) ? 1 : prev_calculated - 1;
      for(int w = start; w < rates_total - 1 && !IsStopped(); w++)
        {
         //--- Search state
         if(bullishEngulfer.state == SEARCH_STATE)
           {
            if(isBullishEngulfing(w, open, high, low, close))
              {
               bullishEngulfer.state = FOUND_STATE;
               bullishEngulfer.time = time[w - 1];
               bullishEngulfer.high = high[w - 1];
               bullishEngulfer.low = low[w - 1];
               bullishEngulfer.retestTime = time[w] + (PeriodSeconds() * optimalBullishRetestRange);// Set retest time window
               return rates_total;
              }
           }
         if(bearishEngulfer.state == SEARCH_STATE)
           {
            if(isBearishEngulfing(w, open, high, low, close))
              {
               bearishEngulfer.state = FOUND_STATE;
               bearishEngulfer.time = time[w - 1];
               bearishEngulfer.high = high[w - 1];
               bearishEngulfer.low = low[w - 1];
               bearishEngulfer.retestTime = time[w] + (PeriodSeconds() * optimalBearishRetestRange); // Set retest time window
               return rates_total;
              }
           }
         //--- Found state
         . . . . 
        }
     }
   return(rates_total);
  }

Final Test

To evaluate the indicator’s adaptability across varying market conditions, it was deployed on two distinct currency pairs. The runtime summary output is shown below:

Adaptive Layer—Initialization Test

Fig. 3. Adaptive Layer—Initialization Test

Conclusion

We implemented an adaptive, verifiable enhancement to the Malaysian Engulfing framework. Each detected setup is represented by a fixed set of properties (formation time, zone high/low, retest time, retest bar distance) and is evaluated by scanning a fixed forward window to compute MFE and MAE from a standardized entry (the open after the retest). Setups are grouped by retestBars and scored via avgMFE − avgMAE. A constrained brute-force search over a single parameter identifies the best retest distance separately for buy and sell setups.

Practically, the MQL5 implementation computes these optimalBullishRetestRange and optimalBearishRetestRange during OnInit (over a user-specified historical window) and applies them in OnCalculate to govern retest validation at runtime. The approach is simple, transparent, and efficient: it replaces manual parameter selection with a reproducible, data-driven choice tied to the chosen symbol, timeframe and date range. Results are visible in the run-time summary and inspectable arrays, enabling straightforward verification and further iteration.

Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Building a Trade Analytics System (Part 3): Storing MetaTrader 5 Trades in SQLite Building a Trade Analytics System (Part 3): Storing MetaTrader 5 Trades in SQLite
This article extends a Flask backend to reliably receive, validate, and store closed trade data from MetaTrader 5 using SQLite and Flask‑SQLAlchemy. It implements required‑field checks, timestamp conversion, transaction‑safe persistence, and working retrieval endpoints for all trades and single records, plus a basic summary. The result is a complete data pipeline with local testing that records trades and exposes them through a structured API for further analysis.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Manual Backtesting with On-Chart Buttons in the MetaTrader 5 Strategy Tester Manual Backtesting with On-Chart Buttons in the MetaTrader 5 Strategy Tester
Learn how to build a manual backtesting EA for MetaTrader 5's visual tester by adding chart buttons with CButton, executing orders through CTrade, and filtering positions with a magic number. The article implements Buy/Sell and Close All controls, configurable lot size and initial SL, and a trailing stop via CPositionInfo. You will also see how to load indicators with tester.tpl to validate ideas faster before automation and narrow optimization ranges.