preview
From Static MA to Adaptive Filtering (Part 1): Introducing SAMA with NLMS in MQL5

From Static MA to Adaptive Filtering (Part 1): Introducing SAMA with NLMS in MQL5

MetaTrader 5Indicators |
187 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Introduction

Traders know the pattern: an SMA or EMA can look perfect for a while — until the market regime shifts and the same fixed-period filter either lags badly in impulses or chops accounts in sideways ranges. The issue is not that moving averages are poorly coded, but that their period is static while the market is not. That mismatch forces endless manual retuning: shorten the period and you get noise; lengthen it and you get lag.

This series offers a concrete alternative: a chart-ready Self-Adaptive Moving Average (SAMA) implemented in MQL5, powered by the Normalized Least Mean Squares (NLMS) adaptive-FIR framework. Crucially, this is written with an explicit success criterion: produce a live, non-repainting indicator that adapts to regime changes without per‑regime period tuning, remains stable during price spikes and news, updates weights only on closed bars, and is usable directly on charts, in EAs, and in backtests. Part 1 explains the motivation, the math, and the core NLMS loop. Part 2 builds the full MQL5 indicator; Part 3 benchmarks it against classical filters.


Quickstart: The Core Idea in Code

Before any theory, here is the minimal working version of the adaptive update loop. This is the heart of the entire indicator:

//--- 1. Calculate input vector energy (sum of squares of PAST prices)

double energy = 0.0;
for(int j = 0; j < inp_filter_length; j++)
  {
   double p = GetSourcePrice(close, high, low, i - 1 - j);
   energy += p * p;
  }

//--- Safeguard against division by zero
if(energy < 1e-8)
   energy = 1e-8;

//--- 2. Compute filter output (predicted value for current bar i) using past data
double predicted = 0.0;
for(int j = 0; j < inp_filter_length; j++)
  {
   predicted += g_weights[j] * GetSourcePrice(close, high, low, i - 1 - j);
  }

//--- 3. Compute error on the current bar
double src_price = GetSourcePrice(close, high, low, i);
double error = src_price - predicted;

//--- 4. Update weights via Normalized LMS rule (only permanently commit closed bars)
if(i < rates_total - 1)
  {
   double normalized_lr = inp_learning_rate / energy;
   for(int j = 0; j < inp_filter_length; j++)
     {
      g_weights[j] += normalized_lr * error * GetSourcePrice(close, high, low, i - 1 - j);
     }
  }

//--- 5. Store output for plotting
g_sama_buffer[i] = predicted;

The core logic is a fast inline sequence. First, it scales the adaptation step by the energy of past prices. Next, it computes a prediction and measures the error. Then, it updates the weights — and crucially, only on finalized bars. If you internalize just these five steps, the rest of the series is a guided expansion of this loop into a stable, deployable indicator.


Section 1: Theoretical Basis of the NLMS Filter

What Is an Adaptive FIR Filter?

Think of a standard simple moving average as a filter: it takes the last N prices, assigns each an equal weight of 1/N, and outputs their sum. The weights never change.

An adaptive FIR (Finite Impulse Response) filter does the same thing — but the weights change on every bar. It continuously tries to minimize the difference between its output and the actual price.

Conceptual Framework: SAMA as an Adaptive FIR Filter 

While commonly referred to as a moving average, the Self-Adaptive Moving Average (SAMA) is more accurately classified as an Adaptive Finite Impulse Response (FIR) filter. Traditional moving averages apply static, hardcoded weights to historical price data.

In contrast, SAMA operates on the Normalized Least Mean Squares (NLMS) algorithm. It dynamically recalculates its internal coefficients (weights) on every tick or bar to minimize the error between its projected value and the actual incoming price. This means SAMA does not passively smooth data; it actively learns the current market structure. If the market shifts from a trending phase to a ranging phase, the FIR filter's weights adapt to the new frequency profile. This allows the algorithm to maintain relevance without requiring the user to manually change the period length.

The NLMS Algorithm

Normalized Least Mean Squares (NLMS) enhances the standard adaptive approach by scaling the adjustment step based on the total energy of the input data vector. It operates via three primary steps per bar:

  1. Predict: Compute output as the dot product of the weight vector and past prices.
  2. Measure error: error = actual_price - predicted_output
  3. Update weights: Shift each weight based on a normalized learning rate to maintain scale-invariant convergence.

The weight update formula is:

w[j] ← w[j] + (μ / E) · e · x[j]

Where:

  • w[j] is the j-th weight (coefficient) in the filter.
  • μ (mu) is the normalized learning rate, kept within a stable range of (0, 2.0), controlling how fast weights adapt safely across varied price metrics.
  • E — the input energy: the sum of squared historical prices across the lookback window, E = Σ x[j]². Dividing by E is what normalizes the step and prevents weight divergence.
  • e — the prediction error, e = actual_price − predicted_output.
  • x[j] — the historical input value at lookback offset j.

 Fig. 1: The recursive NLMS optimization cycle processing real-time market data through continuous prediction error correction.

Fig. 1: The recursive NLMS optimization cycle processing real-time market data through continuous prediction error correction.

Why Incremental Processing (Instead of Batch Learning)

Batch learning utilizes a static historical dataset and deploys the result as a fixed artifact. It cannot respond to shifting market dynamics without manual intervention or periodic re-training.

Incremental processing updates the model’s parameters with each incoming data point. The NLMS algorithm exemplifies this approach. This method aligns perfectly with a live price feed where data arrives sequentially, one bar at a time. The model is effectively "always on," requiring no re-training and no redeployment.

The NLMS algorithm leverages this incremental cycle to compute weight adjustments natively within OnCalculate(). This architectural efficiency eliminates external dependencies. It ensures stable, real-time adaptation entirely within the MQL5 environment.


Section 2: Architecture Overview (Conceptual)

Before implementing the full code in Part 2, we will review the indicator's skeleton and the safety features needed to run the NLMS loop on live ticks.

Project Structure

Table 1: Structural composition of the Self-Adaptive Moving Average (SAMA) indicator. 

Block Responsibility
#property declarations Chart plotting and single data buffer configuration
input parameters User-controlled normalized learning parameters
Global state arrays Persistent weight vector array
OnCalculate() loop Energy calculation → Predict → Error → Closed-bar Update → Output

Table 1 outlines the structural composition of the Self-Adaptive Moving Average (SAMA) indicator. It delineates the four primary functional blocks required to manage data processing and visual output within the MQL5 environment.

Architectural Defenses: ATR-Clamping and Weight Leakage

A known vulnerability of standard adaptive filters is their overreaction to sudden, anomalous price spikes, such as high-impact macroeconomic news events. To solve this, SAMA implements an ATR-Clamped error function. Before the NLMS algorithm updates its weights, the absolute error between the prediction and the price is evaluated against a multiplier of the Average True Range (ATR). If the error exceeds this dynamic threshold, the error signal is clamped. This prevents a single massive price anomaly from distorting the filter's weights, ensuring the indicator remains stable and resumes tracking the true trend once the shock dissipates.

To reduce long-term numerical drift, SAMA uses a leakage factor (γ). Without regularization, weights may become unstable during low-variance or highly collinear input. The leakage factor is a practical stabilization heuristic, not a strict mathematical guarantee of boundedness. By introducing a small leak, the algorithm applies a continuous, slight decay to the weights, pulling them gently toward zero:

w[j] ← (1 − γ) · w[j] + (μ/E) · e · x[j]

This acts as a regularization mechanism, preventing weight saturation and ensuring the filter remains responsive to new data over years of continuous operation.

Input Transformation Modes

SAMA can train on three different representations of price, selectable at input:

  • INPUT_PRICE — raw price values. The default; produces a standard smoothing line anchored to the price axis.
  • INPUT_DIFF — first-order differences between consecutive bars. This centers learning on momentum rather than absolute level; the output is reconstructed back to price by accumulating predicted differences from the previous bar.
  • INPUT_RET — percentage returns. This process makes the filter scale-invariant by normalizing the training signal across different price scales. To reconstruct the final output, the predicted return is simply applied to the previous close.

The reconstruction step always converts the internal representation back into native price units. This ensures the chart line plots correctly, regardless of which mode trained the weights.

Adaptive Learning Rate via Efficiency Ratio

When inp_use_adaptive_lr is enabled, the base learning rate (μ) is scaled dynamically using Kaufman's Efficiency Ratio (ER) on each bar. The ER measures how directional recent price movement is relative to total path length: a value near 1.0 signals a clean, efficient trend, while a value near 0.0 indicates choppy, directionless noise.

The scaling formula used is:

efficiency_ratio = net_direction / net_volatility

μ_effective = μ · (0.5 + efficiency_ratio)

During a sustained trend, the ER approaches 1.0 and the effective learning rate rises toward 1.5× the base value, allowing the filter to track price tightly. During consolidation, the ER collapses and the learning rate is suppressed toward 0.5× base, causing the filter to smooth aggressively and resist noise. This replaces the static μ with a market-state-aware adaptation throttle.

Weight Normalization

When enabled, the algorithm enforces that the filter's internal weight vector sums to exactly 1.0 after every update cycle. This constraint prevents the raw NLMS update rule from producing weight configurations that, when applied to raw prices, generate outputs that drift from the price axis. With normalization active, the weighted sum of historical prices always produces a value on the same scale as the input, making the indicator numerically stable and visually coherent without requiring manual gain tuning.

Dynamic Slope Color Classification

The indicator uses a DRAW_COLOR_LINE plot with three color states. On every completed bar, the derivative (slope) of the SAMA line is evaluated: 

  • A positive slope assigns MediumSeaGreen — indicating an upward trend phase.
  • A negative slope assigns Crimson — indicating a downward trend phase.
  • A zero slope assigns SlateGray — indicating a flat or transitional state.

This provides an instant visual regime classifier directly on the line itself, without requiring a separate sub-window indicator.

Linear Buffer Optimization

Inside OnCalculate, the indicator pre-allocates two local arrays — source[] and tr_input[] — sized to rates_total at the start of each call. All price and transformation data is mapped into these contiguous memory blocks before the main compute loop begins. This pattern avoids repeated pointer dereferences into the terminal's series arrays during the inner loop, improving cache locality and reducing per-bar overhead on large datasets.

Warmup Bars

The inp_warmup_bars parameter controls how many bars are processed silently before the indicator line begins drawing. The PLOT_DRAW_BEGIN offset is set to inp_filter_length + inp_warmup_bars + inp_er_period, hiding the period during which the NLMS weight vector is still converging from its flat initial state. This prevents the visually misleading transient behavior that would otherwise appear at the left edge of the chart when the indicator is first applied to a large historical dataset.


Conclusion

What you should now have from this part is a clarified problem statement and a precise engineering target: replace a fixed lookback period with an adaptive weight vector and a market-aware adaptation throttle. The NLMS core performs predict → error → normalized weight update on closed bars, and the adaptive mechanism that substitutes for “period tuning” is a combination of per-weight adaptation plus an optional efficiency-scaled learning rate.

Equally important, we established the minimal, deployable protections any adaptive chart indicator needs: ATR-clamping of extreme errors to survive macro shocks, a small leakage term to prevent long‑term drift, optional weight normalization to keep outputs on the price scale, multiple input modes (price/diff/return) with correct reconstruction, an ER-based adaptive μ, and a warmup period to hide convergence transients. Together these form the practical SAMA recipe: an algorithmic skeleton that is robust in real time and ready to be implemented in MQL5. In Part 2 we translate this design into the full SAMA_NLMS.mq5 source: buffers, weight seeding, closed-bar update logic, plotting, and deployment notes so you can drop it onto a chart or reuse the core in an EA or backtest without hidden repainting or unstable learning.

Swing Extremes and Pullbacks (Part 4): Dynamic Pullback Depth Using Volatility Models Swing Extremes and Pullbacks (Part 4): Dynamic Pullback Depth Using Volatility Models
This article replaces binary swing validation with a volatility‑normalized pullback model. Retracement depth is measured as a ratio of the prior impulse and calibrated to a rolling ATR regime, while entries require a minimum quality score and confirmation by structure or liquidity signals. The five‑layer design integrates detection, validation, liquidity mapping, regime‑aware scoring, and execution, helping you filter weak corrections and size stops dynamically to current conditions.
MQL5 Trading Tools (Part 36): Adding Shape and Annotation Tools with In-Place Label Editing to the Canvas Drawing Layer MQL5 Trading Tools (Part 36): Adding Shape and Annotation Tools with In-Place Label Editing to the Canvas Drawing Layer
We add eight shape tools and nine annotation tools to the canvas and implement a full in-place label-editing system. The article walks through geometry, AA rendering, shared word-wrap and supersampled text helpers, and the caret-driven state machine for typing, navigation, and selection. This yields a complete, consistent annotation toolkit with editable labels that plugs into the prior interaction pipeline.
CSV Data Analysis (Part 4): Building an Automated Python-Driven Comparative Analysis Module for MQL5 Strategy Validation CSV Data Analysis (Part 4): Building an Automated Python-Driven Comparative Analysis Module for MQL5 Strategy Validation
The article presents a reproducible MetaTrader 5 to Python pipeline for large-scale indicator research. An MQL5 export schema captures fixed columns, including custom lag and whipsaw counters. A baseline module performs parameter-matched comparisons across symbols and timeframes, while a walk-forward module locks the InSample optimum and evaluates it on unseen data. Readers gain unbiased robustness measurements and automation that removes manual selection bias.
A Generic Object Pool in MQL5: Eliminating Heap Fragmentation in High-Frequency Indicators A Generic Object Pool in MQL5: Eliminating Heap Fragmentation in High-Frequency Indicators
High-frequency MQL5 indicators that instantiate objects on every tick accumulate allocation overhead and timing jitter in OnCalculate(). This article constructs a generic templated object pool using a free-list index array, delivering O(1) Acquire() and Release() operations. The design includes double-release protection, strict separation of payload state from pool metadata in Reset(), and a fixed-capacity free list with no heap fallback. A dual-path custom indicator benchmark measures per-tick overhead difference using GetMicrosecondCount().