Adaptive Malaysian Engulfing Indicator (Part 2): Optimized Retest Bar Range
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:

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
//+------------------------------------------------------------------+ //| 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:

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:

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.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Features of Custom Indicators Creation
Building a Trade Analytics System (Part 3): Storing MetaTrader 5 Trades in SQLite
Features of Experts Advisors
Manual Backtesting with On-Chart Buttons in the MetaTrader 5 Strategy Tester
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use