From Static MA to Adaptive Filtering (Part 2): Implementing the SAMA_NLMS Indicator in MQL5
Introduction
You already saw the NLMS idea in Part 1. In practice, however, transplanting the update equation from a notebook into MQL5 often breaks in ways traders and developers care about: the model can train on the still-forming bar, blow up on flat data, be skewed by single-bar spikes, and produce different live values after a terminal reload because its internal weight state depends on the exact history processed. This part stops treating SAMA as a formula and treats it as production software. It embeds NLMS into a chart-ready MQL5 indicator with proper buffers, input validation, ATR-based error clamping, an Efficiency‑Ratio adaptive μ, weight leakage and optional normalization, a mandatory warm-up phase, and a rule to train only on closed bars. The goal is explicit: provide a SAMA_NLMS.mq5 you can compile, attach to a chart, and use in trading scenarios without obvious instability or surprise re‑rendering.
Section 1: Full Indicator Code
Create a new Indicator file in MetaEditor named SAMA_NLMS.mq5 and paste the following:
//+------------------------------------------------------------------+ //| SAMA_NLMS.mq5 | //| Self-Adaptive Moving Average via NLMS Online | //+------------------------------------------------------------------+ #property description "Self-Adaptive Moving Average using Normalized LMS (NLMS) algorithm." #property indicator_chart_window #property indicator_buffers 2 #property indicator_plots 1 //--- Plot 0: SAMA Line with dynamic slope color formatting #property indicator_label1 "SAMA" #property indicator_type1 DRAW_COLOR_LINE #property indicator_color1 clrMediumSeaGreen,clrCrimson,clrSlateGray #property indicator_width1 2 #property indicator_style1 STYLE_SOLID //+------------------------------------------------------------------+ //|Enumerations for Input Transformations | //+------------------------------------------------------------------+ enum ENUM_INPUT_MODE { INPUT_PRICE = 0, // Option A: Raw Prices INPUT_DIFF = 1, // Option B: Differences (Delta) INPUT_RET = 2 // Option C: Returns (Percentage Change) }; //+------------------------------------------------------------------+ //| Input Parameters | //+------------------------------------------------------------------+ input ENUM_INPUT_MODE inp_mode = INPUT_PRICE; // Data transformation input mode input int inp_filter_length = 14; // Filter length (Adaptive weight count) input double inp_learning_rate = 0.05; // Learning rate (Base Step Size Mu) input bool inp_use_close = true; // Price: true = Close; false = Typical (H+L+C)/3 input double inp_epsilon = 1e-8; // Epsilon for numerical division safety input double inp_leak = 0.0001; // Forgetting factor / Leakage weight clamp input bool inp_use_atr_clamp = true; // Use ATR error clamping option input int inp_atr_period = 14; // ATR Period for error boundaries input double inp_error_atr_mult = 3.0; // Max allowed error multiplier (Mult * ATR) input bool inp_use_adaptive_lr = true; // Dynamic step size adjustment via ER input int inp_er_period = 10; // Efficiency Ratio window period input bool inp_normalize_weights = true; // Force adaptive weights sum to 1.0 input int inp_warmup_bars = 500; // Historical background initialization loop //+------------------------------------------------------------------+ //| Indicator Buffers | //+------------------------------------------------------------------+ double g_sama_buffer[]; // Main display buffer double g_color_buffer[]; // Derivative slope color assignment matrix //+------------------------------------------------------------------+ //| Global State Variables | //+------------------------------------------------------------------+ double g_weights[]; // Memory matrix containing adaptive weight vector int g_atr_handle = INVALID_HANDLE; // Managed calculation pointer handle for core system ATR double g_atr_buffer[]; // Direct memory block array for streaming ATR inputs //+------------------------------------------------------------------+ //| Helper: Get the raw localized price | //+------------------------------------------------------------------+ double GetSourcePrice(const double &close[], const double &high[], const double &low[], const int i) { return(inp_use_close ? close[i] : (high[i] + low[i] + close[i]) / 3.0); } //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Parameter Bounds Sanitization Checking if(inp_filter_length < 2 || inp_learning_rate <= 0.0 || inp_learning_rate > 2.0 || inp_warmup_bars < 0 || inp_er_period < 2) { Alert("SAMA_NLMS Error: Invalid configuration values detected."); return(INIT_PARAMETERS_INCORRECT); } //--- Initialize ATR Handle if required if(inp_use_atr_clamp) { g_atr_handle = iATR(_Symbol, _Period, inp_atr_period); if(g_atr_handle == INVALID_HANDLE) { Print("SAMA_NLMS: Failed to open system iATR structural handle."); return(INIT_FAILED); } } //--- Bind structural index buffer handles to terminal engine SetIndexBuffer(0, g_sama_buffer, INDICATOR_DATA); SetIndexBuffer(1, g_color_buffer, INDICATOR_COLOR_INDEX); //--- Shift processing view beyond window boundaries to hide processing artifacts PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, inp_filter_length + inp_warmup_bars + inp_er_period); //--- Allocate structural filter depth to weight vectors ArrayResize(g_weights, inp_filter_length); ArrayInitialize(g_weights, 1.0 / (double)inp_filter_length); IndicatorSetString(INDICATOR_SHORTNAME, StringFormat("SAMA_NLMS(Len=%d, Leak=%.5f)", inp_filter_length, inp_leak)); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int 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 int &spread[]) { int structural_minimum = inp_filter_length + 2; if(inp_use_adaptive_lr) structural_minimum = MathMax(structural_minimum, inp_er_period + 2); if(rates_total <= (structural_minimum + inp_warmup_bars)) return(0); //--- Linear continuous buffer optimization strategy double source[], tr_input[]; if(ArrayResize(source, rates_total) < 0 || ArrayResize(tr_input, rates_total) < 0) return(0); //--- Map transformations into contiguous execution memory cache for(int i = 0; i < rates_total; i++) { source[i] = GetSourcePrice(close, high, low, i); if(i > 0) { if(inp_mode == INPUT_PRICE) tr_input[i] = source[i]; else if(inp_mode == INPUT_DIFF) tr_input[i] = source[i] - source[i - 1]; else if(inp_mode == INPUT_RET) tr_input[i] = (source[i - 1] != 0.0) ? ((source[i] - source[i - 1]) / source[i - 1]) : 0.0; } else tr_input[i] = 0.0; } //--- Handle core execution loops state initializations int start_idx; if(prev_calculated == 0) { ArrayInitialize(g_weights, 1.0 / (double)inp_filter_length); start_idx = structural_minimum; //--- Wipe stale arrays out of drawing tracks for(int i = 0; i < start_idx; i++) { g_sama_buffer[i] = EMPTY_VALUE; g_color_buffer[i] = EMPTY_VALUE; } } else { start_idx = prev_calculated - 1; } //--- Safely retrieve ATR mapping parameters (Non-Series Normal Handling) if(inp_use_atr_clamp) { ArrayResize(g_atr_buffer, rates_total); if(CopyBuffer(g_atr_handle, 0, 0, rates_total, g_atr_buffer) <= 0) return(0); } //--- Main Core Adaptive Filtering Compute Execution for(int i = start_idx; i < rates_total; i++) { double target = tr_input[i]; //--- Step 1: Compute total vector transformation energy double energy = 0.0; for(int j = 0; j < inp_filter_length; j++) { double p = tr_input[i - 1 - j]; energy += p * p; } //--- Step 2: Extract current structural adaptation state prediction double predicted = 0.0; for(int j = 0; j < inp_filter_length; j++) { predicted += g_weights[j] * tr_input[i - 1 - j]; } //--- Step 3: Quantify dynamic estimation error deviation metric double error = target - predicted; //--- Strict ATR volatility boundary error calculation clamp if(inp_use_atr_clamp && i >= inp_atr_period) { double atr = g_atr_buffer[i]; double max_error = inp_error_atr_mult * atr; if(inp_mode == INPUT_RET && source[i - 1] != 0.0) max_error /= source[i - 1]; error = MathMin(MathMax(error, -max_error), max_error); } //--- Step 4: Perform optimization adjustment updates on completed intervals if(i < rates_total - 1) { double mu = inp_learning_rate; //--- Process Kaufman Efficiency Ratio Adaptive Engine if(inp_use_adaptive_lr && i >= inp_er_period) { double net_direction = MathAbs(source[i] - source[i - inp_er_period]); double net_volatility = 0.0; for(int k = 0; k < inp_er_period; k++) { net_volatility += MathAbs(source[i - k] - source[i - k - 1]); } double efficiency_ratio = (net_volatility > 0.0) ? (net_direction / net_volatility) : 0.0; mu *= (0.5 + efficiency_ratio); } //--- Standardized Classical NLMS normalization step double normalized_lr = mu / (inp_epsilon + energy); //--- Apply optimization weight corrections for(int j = 0; j < inp_filter_length; j++) { g_weights[j] = (1.0 - inp_leak) * g_weights[j] + (normalized_lr * error * tr_input[i - 1 - j]); } //--- Dynamic execution bounds weight normalization protection if(inp_normalize_weights) { double weight_sum = 0.0; for(int j = 0; j < inp_filter_length; j++) weight_sum += g_weights[j]; if(MathAbs(weight_sum) > 1e-12) { for(int j = 0; j < inp_filter_length; j++) g_weights[j] /= weight_sum; } } } //--- Step 5: Convert state matrix evaluations to absolute price coordinates double transformed_output = 0.0; if(inp_mode == INPUT_PRICE) transformed_output = predicted; else if(inp_mode == INPUT_DIFF) transformed_output = source[i - 1] + predicted; else if(inp_mode == INPUT_RET) transformed_output = source[i - 1] * (1.0 + predicted); //--- Sanitize anomalous computations if(!MathIsValidNumber(transformed_output)) g_sama_buffer[i] = (i > 0) ? g_sama_buffer[i - 1] : source[i]; else g_sama_buffer[i] = transformed_output; //--- Multi-Buffer dynamic derivative trend color classification if(i > structural_minimum && g_sama_buffer[i - 1] != EMPTY_VALUE) { double slope = g_sama_buffer[i] - g_sama_buffer[i - 1]; if(slope > 0.0) g_color_buffer[i] = 0.0; else if(slope < 0.0) g_color_buffer[i] = 1.0; else g_color_buffer[i] = 2.0; } else g_color_buffer[i] = 2.0; } return(rates_total); } //+------------------------------------------------------------------+
Input Parameter Reference
Before stepping through the logic, here is what every input does and how it changes the filter's behavior. Read this as the configuration map you'll return to when tuning the indicator on different instruments.
| Parameter | Type | Default | Valid range | Practical effect |
|---|---|---|---|---|
| inp_mode | enum | INPUT_PRICE | PRICE / DIFF / RET | Selects what the filter trains on. PRICE = raw price; DIFF = bar-to-bar change (momentum); RET = percentage return (scale-invariant). |
| inp_filter_length | int | 14 | ≥ 2 | Number of adaptive weights (the lookback). Longer = smoother but heavier; SAMA stays effective at short lengths because it adapts rather than widening. |
| inp_learning_rate | double | 0.05 | (0, 2.0) | Base step size μ. Higher tracks faster but risks oscillation; ≤ 1.0 is the safe responsive zone. |
| inp_use_close | bool | true | — | Price source: true = Close, false = Typical price (H+L+C)/3. |
| inp_epsilon | double | 1e-8 | small > 0 | Division-by-zero floor in the normalization step; matters only when input energy collapses to near zero. |
| inp_leak | double | 0.0001 | (0, ~0.01) | Forgetting factor γ. Larger values decay weights faster toward zero, trading some responsiveness for long-run stability. |
| inp_use_atr_clamp | bool | true | — | Enables capping the error at a multiple of ATR so a single spike can't distort the weights. |
| inp_atr_period | int | 14 | ≥ 1 | Lookback for the ATR used by the clamp. |
| inp_error_atr_mult | double | 3.0 | > 0 | How many ATRs of error are allowed before clamping. Lower = stricter spike rejection. |
| inp_use_adaptive_lr | bool | true | — | Scales μ each bar by Kaufman's Efficiency Ratio. |
| inp_er_period | int | 10 | ≥ 2 | Window for the Efficiency Ratio calculation. |
| inp_normalize_weights | bool | true | — | Forces the weight vector to sum to 1.0 each update, keeping output on the price scale. Strongly recommended in PRICE mode. |
| inp_warmup_bars | int | 500 | ≥ 0 | Bars processed silently before plotting, hiding the convergence transient. |
Table 1: Input parameter reference
A practical starting profile for most FX symbols: keep the defaults, but switch inp_mode to INPUT_RET on instruments with very different price scales or during thin sessions.
Section 2: Code Walkthrough — Key Design Decisions
Streamlined Architecture: Inline Execution
All structural vector operations run directly inside the main OnCalculate loop. This delivers two concrete benefits:
- Execution efficiency — processing inline eliminates function-call overhead and external circular buffers, cutting CPU use on fast tick feeds.
- Direct offset indexing — querying historical bars with simple index arithmetic (i − 1 − j) avoids array-shifting overhead and improves calculation stability.
Inline Weight Seeding
Initializing an adaptive model with zeroed states causes erratic tracking and slow convergence. On the first pass (prev_calculated == 0) the code seeds g_weights with a uniform prior of 1/N. This creates a neutral baseline equivalent to a simple moving average, eliminating warm-up artifacts and producing clean plots as soon as the lookback requirement is met.
Real-Time Stability and Multi-Tick Safeguards
The condition if(i < rates_total - 1) updates weights only on closed bars. The active, still-forming bar is used to predict but never to train. This separation prevents intra-bar weight drift and eliminates the data corruption caused by rapid back-and-forth ticks within a single candle — the historical line stays reproducible while the live bar still gets a prediction from the established weights.
Learning-Rate Sensitivity and Stability Bounds
The normalized learning rate μ governs tracking responsiveness. Because the update is scaled by input energy, the step stays structurally scale-invariant. In DIFF and RET modes this yields strong consistency across asset classes. In INPUT_PRICE mode, some price-scale sensitivity remains because the ATR clamp is denominated in price units. However, energy normalization still prevents numerical overflow on both low-priced (EURUSD) and high-priced (gold, indices) instruments.
While the mathematical stability limit of NLMS extends to a learning rate of 2.0, keeping μ at or below 1.0 delivers highly responsive, stable tracking.
Table 2: Sensitivity analysis of the normalized learning rate μ (mu).
| μ Value | Behavior |
|---|---|
| 0.001 – 0.010 | Conservative adaptation, smooth output curves |
| 0.010 – 0.100 | Balanced configuration — optimal configuration for standard market regimes |
| 0.100 – 1.000 | Highly responsive tracking, tightly adjusting during major breakouts |
| 1.000 – 2.000 | Aggressive adaptation approaching the theoretical boundary limit |
Sensitivity analysis of the normalized learning rate (μ). This guide categorizes the filter’s behavioral response from conservative smoothing to aggressive tracking. By normalizing step adjustments dynamically against price vector energy, the engine maintains scale-invariant stability across varied instruments. The mathematical boundary extends safely up to 2.0. Setting values above this limit may cause weight oscillations or gradient explosions.
Section 3: Installing and Viewing the Indicator
- Open MetaEditor (F4 in MetaTrader 5).
- Go to File → New → Custom Indicator.
- Name it SAMA_NLMS, paste the full code, and press Compile (F7).
- In MetaTrader 5, open any chart, go to Insert → Indicators → Custom, and select SAMA_NLMS.
- Set your parameters in the input dialog and click OK.
The SAMA line will appear on the chart with slope-based color coding. It renders in MediumSeaGreen when the slope is rising (uptrend), Crimson when the slope is falling (downtrend), and SlateGray during flat or transitional states. It will look similar to an EMA at first glance, but observe its behavior across regime changes — you will notice it tightens during strong trends and widens its lag during choppy periods.

Fig. 1: The SAMA indicator plotted on a EURUSD H1 chart, demonstrating tight tracking velocity during an aggressive uptrend and smooth stabilization within a flat consolidation range.
Section 4: Practical Usage Patterns
As a Dynamic Trend Filter
Use SAMA as a structural regime classifier. If the price is above the SAMA line, look for long entries only. If the price falls below the line, focus purely on short positions. Because the filter adapts dynamically, it tightens automatically during fast trends. It also flattens during messy ranges. This behavior eliminates false regime flips compared to traditional, fixed-period averages.
//+------------------------------------------------------------------+ //| Example: Querying the SAMA line from a prior indicator handle | //+------------------------------------------------------------------+ double sama_value[1]; if(CopyBuffer(sama_handle, 0, 1, 1, sama_value) > 0) { if(close_price > sama_value[0]) { //--- Bullish regime: Allow long execution pathways only } else { //--- Bearish regime: Allow short execution pathways only } }
As a Dynamic Support/Resistance Reference
The SAMA weights evolve continuously toward recent price action. This behavior causes the indicator line to gravitate naturally toward structural price acceptance zones. In trending markets, the line acts as a highly responsive trailing support or resistance curve. In ranging markets, it clusters cleanly near the asset's structural mean price.
Combining With a Fixed MA for Crossover Signals
You can create robust crossover signals by combining SAMA with a standard, fixed 50-period SMA. This dual-indicator structure generates trend-change signals that are highly resistant to whipsaws. The primary advantage is that SAMA adjusts its internal tracking speed dynamically. It does not trail behind the market at a rigid, static pace.
Section 5: Understanding the Limitations
No algorithmic indicator provides a flawless trading solution. The SAMA_NLMS architecture has specific mathematical properties you must account for before deployment:
- Heavy Dependency on Training History Paths: SAMA uses an online, recursive optimization engine. The precise structure of the weight matrix on the current bar depends entirely on the unbroken sequence of historical updates processed since chart initialization. This creates a critical operational subtlety for practical trading:
- History Depth: Loading a chart with 5,000 bars will yield slightly different current weight vectors than loading a chart with 50,000 bars.
- Timeframe Switching: Switching timeframes forces a complete memory purge. The algorithm must rebuild its adaptation history from zero on the new time intervals.
- Terminal Recalculations: If your broker connection drops and the terminal forces a chart data update, the internal weight trajectory will recalculate from the beginning of the available history.
- Trading Implications and Mitigations: Because of this path dependency, live execution signals can temporarily diverge from historical backtest lines if the historical depth does not match precisely. To mitigate this risk, the indicator implements a strict, mandatory initialization loop via inp_warmup_bars. This parameter forces the algorithm to process a minimum background buffer of 500 bars before plotting execution values. This background training phase stabilizes the weight array, neutralizing initial condition variance and ensuring signal uniformity across terminal reloads.
- Over-Adaptation in Low-Volume Markets: If you use raw price mode (INPUT_PRICE) during illiquid, flat market holiday hours, the weights can adjust too aggressively to minor structural noise. This issue occurs because the underlying data lacks variance. You can easily fix this by switching the transformation mode to percentage returns (INPUT_RET).
- Dependence on Epsilon for Division Safety: The NLMS normalization step divides the learning rate by the input vector energy. If the market becomes completely flat, this energy approaches zero. The calculation relies entirely on your epsilon input (inp_epsilon) to prevent catastrophic divide-by-zero errors.
Section 6: Extending the Indicator
The SAMA_NLMS.mq5 framework provides an excellent, modular foundation for further quantitative research:
- Alternative Data Transformation Pipelines: The inp_mode enumeration can be expanded. You can easily integrate advanced math transformations. These include logarithmic price returns, Z-score normalizations, or detrended price series before passing arrays to the core filter.
- Asymmetric Error Penalization: You can modify the weight update loop to treat positive and negative errors differently. This adjustment creates an asymmetric adaptive filter. It tracks rapid bearish liquidations faster than gradual bullish expansions.
- Multi-Timeframe Regime Hierarchies: You can initialize multiple SAMA handles across different timeframes inside a single Expert Advisor. This structure lets you use an H4 SAMA for macro-trend direction, an H1 SAMA for structural pullback tracking, and an M15 SAMA for execution triggers.
Conclusion
After this part you have a working SAMA NLMS.mq5: it compiles, plots with slope-based coloring, and implements the engineering protections required for practical deployment. Concretely, the indicator enforces input-parameter bounds, initializes weights to a neutral prior, updates weights only on closed bars (no intra-bar learning), clamps extreme errors using ATR, optionally normalizes the weight vector to keep outputs on price scale, and supports an Efficiency‑Ratio scaled learning rate. These measures mitigate the most common failure modes but do not eliminate a fundamental property: SAMA's weights are path-dependent. To reduce divergence between sessions, use the warm-up bars parameter to force background training (default 500), prefer INPUT RET for scale-invariance on thin or widely scaled instruments, and keep μ in the conservative-to-balanced band (≤ 1.0) unless you intentionally need very aggressive tracking.
Operational checklist before deployment: compile and load SAMA NLMS, verify no training on the open bar, inspect behavior during a reconnect or timeframe change (use warm-up), and test parameter sensitivity (inp mode, inp filter length, inp learning rate, inp leak, inp error atr mult). In the final part we will quantify whether these adaptive behaviors change trading outcomes: we will build a parameter-matched EA benchmarking SAMA vs SMA/EMA/KAMA, export diagnostics to CSV, and run a Python analysis pipeline for baseline and walk‑forward comparisons across multiple assets.
Program used in the article:
| # | Name | Type | Description |
|---|---|---|---|
| 1 | SAMA_NLMS.mq5 | Custom Indicator | The native custom indicator source code implementing the Normalized LMS adaptive filtering engine. It features built-in ATR error clamping, input transformation modes, and dynamic learning rate scaling using the Kaufman Efficiency Ratio. |
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.
Price Action Analysis Toolkit Development (Part 73): Building a Weekend Gap Trading Signal System in MQL5
Overcoming Accessibility Problems in MQL5 Trading Tools (Part V): Gesture-Based Trading With Computer Vision
MQL5 Trading Tools (Part 37): Adding a Per-Object Property-Editing Ribbon to the Canvas Drawing Layer
Feature Engineering for ML (Part 6): Microstructural Features in MQL5
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use