preview
GoertzelBrain: Adaptive Spectral Cycle Detection with Neural Network Ensemble in MQL5

GoertzelBrain: Adaptive Spectral Cycle Detection with Neural Network Ensemble in MQL5

MetaTrader 5Examples |
222 0
Max Brown
Max Brown

Introduction

Cycle analysis has a long history in financial markets. Traders have always sought to identify repeating patterns — periodic structures in price that, if reliably detected, can provide an edge in timing entries and exits. The challenge is that financial cycles are non-stationary: they appear, shift, strengthen, weaken, and vanish in ways that defeat static measurement tools.

The Goertzel algorithm, first introduced by Gerald Goertzel in 1958, provides an efficient method for computing individual frequency components of the Discrete Fourier Transform. Its application to financial markets was explored in the earlier MQL5 article written by F. Dube and titled "Cycle analysis using the Goertzel algorithm", which presented the CGoertzel and CGoertzelCycle classes for MQL5. That work demonstrated how the algorithm can identify dominant cycles in price data with greater computational efficiency than the full FFT, and with superior noise handling compared to Ehlers' MESA technique.

However, knowing which cycle is dominant at any given moment is only half the problem. The real question is: what does the cycle tell us about what happens next? A 40-bar cycle at peak amplitude might mean a reversal is imminent — or it might mean the cycle is about to break down entirely. Context matters, and context is exactly what simple spectral analysis cannot provide alone.

This article presents GoertzelBrain — an indicator that combines Goertzel spectral analysis with an ensemble of self-training neural networks to produce an adaptive, context-aware cycle signal. Rather than simply reporting which cycle is present, GoertzelBrain learns to interpret the spectral features in the context of recent price behavior and produces a directional confirmation signal that adapts as market conditions change.

We will cover the mathematical foundation, the complete MQL5 implementation, the architecture of the neural network ensemble, and practical applications for using the indicator as a directional filter.



The Problem with Static Cycle Detection

Traditional cycle detection methods share a fundamental limitation: they tell you what is happening in the frequency domain but not what it means for the next bar. Consider the output of a standard Goertzel spectrum analyzer. At any given bar, it might report that a 34-bar cycle is dominant with strong amplitude. But this information alone cannot answer the trader's question: should I be long or short?

The difficulty arises from several factors:

  • Phase ambiguity. The Goertzel algorithm computes amplitude and phase for each frequency bin, but the phase value is notoriously unstable at the boundary of the analysis window. Small changes in price can produce large phase shifts, making it unreliable as a direct trading signal.
  • Cycle regime transitions. Financial cycles do not switch cleanly from one period to another. There are transition zones where multiple cycles overlap, compete, and interfere. A static spectral snapshot cannot distinguish between a stable dominant cycle and one that is in the process of breaking down.
  • Non-stationarity. The statistical properties of price data change continuously. A cycle that was highly significant during a trending regime may become meaningless during a range-bound period. Any useful cycle indicator must somehow account for this.

GoertzelBrain addresses these problems by extracting a multi-dimensional feature vector from the Goertzel output and feeding it into an ensemble of neural networks that learn to interpret the spectral context over time.

Figure 1 — GoertzelBrain on USDCAD M15: green histogram confirms long bias during the December–January uptrend; red bars appear as the cycle regime shifts bearish.

Figure 1. GoertzelBrain on USDCAD M15: green histogram confirms long bias during the December–January uptrend; red bars appear as the cycle regime shifts bearish


Architecture Overview

The indicator consists of three layers:

  • Layer 1 — Spectral Feature Extraction. For each bar, a rolling window of 3 × MaxPeriod bars is extracted and passed through the Goertzel DFT. The dominant cycle period, its amplitude, spectral confidence, and their rates of change are computed. Combined with a simple price slope and volatility measure, this produces a 7-dimensional feature vector.
  • Layer 2 — Neural Network Ensemble. Ten small multi-layer perceptrons (7 inputs, 12 hidden neurons, 1 output) process the feature vector independently. Each MLP has randomly initialized weights and is retrained online every RetrainEvery bars using single-sample stochastic gradient descent. The diversity of random initialization means each MLP develops a slightly different interpretation of the spectral features.
  • Layer 3 — Ensemble Output and Confirmation Logic. The outputs of all ten MLPs are averaged to produce a single ensemble value. A confirmation signal is then derived: when the ensemble is above zero and rising, long direction is confirmed. When below zero and falling, short direction is confirmed.

Figure 2 — Architecture of GoertzelBrain: from price data to confirmation signal

Figure 2. Architecture of GoertzelBrain: from price data to confirmation signal


Implementation

File Structure

The indicator depends on two include files from the original Goertzel article:

  • Goertzel.mqh — The CGoertzel class implementing the core Goertzel DFT algorithm
  • GoertzelCycle.mqh — The CGoertzelCycle class providing cycle peak detection and spectrum analysis

The indicator itself is a single file: GoertzelBrain.mq5 .

Indicator Properties and Inputs

The indicator draws in a separate window with three visible plots and one hidden calculation buffer:

#property strict
#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   3

#property indicator_label1  "Ensemble"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2

#property indicator_label2  "LongConfirm"
#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  clrLime
#property indicator_style2  STYLE_SOLID
#property indicator_width2  3

#property indicator_label3  "ShortConfirm"
#property indicator_type3   DRAW_HISTOGRAM
#property indicator_color3  clrRed
#property indicator_style3  STYLE_SOLID
#property indicator_width3  3

The first plot is the blue ensemble line — the raw averaged output of all MLPs. The second and third plots are green and red histogram bars that appear only when the confirmation conditions are met. This visual design allows the trader to immediately see both the underlying spectral signal and the specific bars where directional confirmation is active.

The input parameters control the spectral analysis range and the learning behavior of the neural network ensemble:

input uint   MinPeriod    = 10;      // Minimum cycle period to analyze
input uint   MaxPeriod    = 80;      // Maximum cycle period to analyze
input uint   RetrainEvery = 200;     // Bars between MLP retraining steps
input double LearnRate    = 0.001;   // SGD learning rate
input int    VolWindow    = 20;      // Lookback for volatility feature

MinPeriod and MaxPeriod define the frequency band examined by the Goertzel algorithm. These should be set based on the timeframe and instrument. For example, on M5 charts, a range of 10–80 covers cycles from 50 minutes to approximately 6.5 hours. RetrainEvery controls how frequently the MLPs update their weights. Lower values produce faster adaptation but risk overfitting to noise. LearnRate controls the step size of gradient descent — too high causes instability, too low makes adaptation sluggish.

Four buffers are declared:

double EnsembleBuffer[];      // Plot 0: raw ensemble output
double LongConfirmBuffer[];   // Plot 1: green histogram when long confirmed
double ShortConfirmBuffer[];  // Plot 2: red histogram when short confirmed
double ConfirmBuffer[];       // Buffer 3: hidden, +1/-1/0 for EA access via iCustom

The fourth buffer ( ConfirmBuffer ) is not plotted but is accessible via iCustom() at buffer index 3, making it straightforward to incorporate the confirmation signal into an Expert Advisor.

The TinyMLP Class

The neural network component is implemented as a self-contained class with no external dependencies. Each MLP has a simple two-layer architecture: 7 inputs → 12 hidden neurons (tanh activation) → 1 linear output.

class TinyMLP
  {
public:
   int    input_dim;
   int    hidden_dim;
   double W1[H_DIM][IN_DIM];   // input-to-hidden weights
   double b1[H_DIM];           // hidden biases
   double W2[H_DIM];           // hidden-to-output weights
   double b2;                  // output bias

Weights are initialized to small random values after MathSrand() is called in OnInit() . The Init() method is separated from the constructor deliberately — MQL5 constructs global objects before OnInit() runs, so MathSrand() has not yet been called when constructors execute. If weights were randomized in the constructor, all MLPs would share the same pseudo-random seed.

   void Init()
     {
      for(int j = 0; j < hidden_dim; j++)
        {
         b1[j] = (MathRand() / 32767.0 - 0.5) * 0.2;
         for(int i = 0; i < input_dim; i++)
            W1[j][i] = (MathRand() / 32767.0 - 0.5) * 0.2;
        }
      b2 = (MathRand() / 32767.0 - 0.5) * 0.2;
      for(int j = 0; j < hidden_dim; j++)
         W2[j] = (MathRand() / 32767.0 - 0.5) * 0.2;
     }

The weight initialization scale of ±0.1 is chosen to keep initial outputs near zero while providing enough variance for the MLPs to diverge during training. This divergence is important — an ensemble of identical networks provides no benefit.

The tanh activation function includes overflow clamping to prevent numerical issues when large feature values propagate through the network:

   static double Tanh(double x)
     {
      if(x > 20.0)  return  1.0;
      if(x < -20.0) return -1.0;
      return 2.0 / (1.0 + MathExp(-2.0 * x)) - 1.0;
     }

The forward pass computes the hidden layer activations and produces a single scalar output:

double Forward(double &x[])
     {
      double hidden[];
      ArrayResize(hidden, hidden_dim);
      for(int j = 0; j < hidden_dim; j++)
        {
         double s = b1[j];
         for(int i = 0; i < input_dim; i++)
            s += W1[j][i] * x[i];
         hidden[j] = Tanh(s);
        }
      double out = b2;
      for(int j = 0; j < hidden_dim; j++)
         out += W2[j] * hidden[j];
      return out;
     }

Training uses standard backpropagation with mean squared error loss. Each training step processes a single sample — there is no batch accumulation. This is intentional: the indicator processes bars sequentially, and single-sample SGD provides the fastest adaptation to changing conditions:

void TrainStep(double &x[], double target, double lr)
     {
      // ... forward pass identical to Forward() ...

      double d_out = out - target;     // MSE gradient at output

      // Output layer weight update
      for(int j = 0; j < hidden_dim; j++)
         W2[j] -= lr * d_out * hidden[j];
      b2 -= lr * d_out;

      // Hidden layer backpropagation
      for(int j = 0; j < hidden_dim; j++)
        {
         double d_h = d_out * W2[j] * (1.0 - hidden[j] * hidden[j]);  // tanh derivative
         b1[j] -= lr * d_h;
         for(int i = 0; i < input_dim; i++)
            W1[j][i] -= lr * d_h * x[i];
        }
     }   

The tanh derivative (1 - tanh²(x)) is computed directly from the cached hidden activation values, avoiding redundant computation.

Heap Allocation of CGoertzelCycle

A critical implementation detail concerns the CGoertzelCycle object. The class internally allocates a CGoertzel pointer using new in its constructor. If the object is declared globally and then re-assigned in OnInit() , the temporary object's destructor deletes the internal pointer, leaving the global object with a dangling pointer. This causes silent failures when GetSpectrum() or GetDominantCycles() are called.

The solution is to allocate the object on the heap:

CGoertzelCycle *goertzel = NULL;

int OnInit()
  {
   if(goertzel != NULL)
      delete goertzel;
   goertzel = new CGoertzelCycle(true, false, false, MinPeriod, MaxPeriod);
   // ...
  }

void OnDeinit(const int reason)
  {
   if(goertzel != NULL)
     {
      delete goertzel;
      goertzel = NULL;
     }
  }

This pattern ensures proper lifetime management and prevents the dangling pointer issue.

Feature Extraction

The BuildFeatures() function constructs the 7-dimensional input vector for the neural networks. For each bar, it extracts a sub-window of 3 × MaxPeriod bars and passes it through the Goertzel stack:

bool BuildFeatures(
const double &price[], int barIdx, double &X[], double in_prev_amp, double in_prev_period )
  {
   int winLen = (int)(MaxPeriod * 3);
   int winStart = barIdx - winLen + 1;
   if(winStart < 0) { ArrayInitialize(X, 0.0); return false; }

   // Extract chronological sub-window for this specific bar
   double window[];
   ArrayResize(window, winLen);
   ArrayCopy(window, price, 0, winStart, winLen);

   // Goertzel spectrum
   double amplitude[];
   goertzel.GetSpectrum(window, amplitude);

   // Dominant cycle detection
   double cycles[];
   uint ncycles = goertzel.GetDominantCycles(true, window, cycles);
   double dominant_period = (ncycles > 0 ? cycles[0] : 0.0);

The per-bar sub-window extraction is essential. Without it, the Goertzel algorithm always analyzes the same global window regardless of which bar is being processed, producing identical features for every bar.

The seven features are:

Index
Feature
Description
0Dominant Period
The cycle length with the highest spectral peak
1Dominant Amplitude
The strength of the dominant cycle
2Phase ProxySpectral confidence multiplied by the sign of the price slope
3Normalized Confidence
Dominant amplitude as a fraction of total band energy
4Amplitude SlopeBar-to-bar change in dominant cycle strength
5Period SlopeBar-to-bar change in dominant cycle length
6VolatilityStandard deviation of price over VolWindow bars

Features 0–3 describe the current spectral state. Features 4–5 capture the dynamics of the spectral state — whether the dominant cycle is strengthening or weakening, and whether it is shifting to a longer or shorter period. Feature 6 provides price context that the spectral features alone cannot capture.

The phase proxy (feature 2) deserves special mention. The CGoertzelCycle class computes phase values internally but stores them in private members that are not directly accessible. Rather than modifying the library, we construct a proxy by multiplying the normalized confidence by the sign of the recent price direction. This captures the essential information: whether the current cycle regime is aligned with bullish or bearish price movement.

The OnCalculate Loop

The main calculation loop processes bars in chronological order (index 0 = oldest, consistent with the default non-series array direction in the single-price OnCalculate signature):

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   int minBars = (int)(MaxPeriod * 3);
   if(rates_total < minBars) return 0;

   int start = (prev_calculated == 0) ? minBars : prev_calculated - 1;
   if(start < minBars) start = minBars;

   for(int i = start; i < rates_total; i++)
     {
      double X[IN_DIM];
      if(!BuildFeatures(price, i, X))
        { /* set all buffers to 0.0, continue */ }

      // Target: next-bar direction
      double target = 0.0;
      if(i + 1 < rates_total)
        {
         double diff = price[i + 1] - price[i];
         target = (diff >= 0.0 ? 1.0 : -1.0);
        }

      // Retrain periodically
      if(bars_since_train >= (int)RetrainEvery)
        {
         for(int m = 0; m < NUM_MLPS; m++)
            mlp[m].TrainStep(X, target, LearnRate);
         bars_since_train = 0;
        }

      // Ensemble inference
      double sum = 0.0;
      for(int m = 0; m < NUM_MLPS; m++)
         sum += mlp[m].Forward(X);
      double ensemble = sum / (double)NUM_MLPS;
      EnsembleBuffer[i] = ensemble;

Note the deliberate separation between training frequency and inference frequency. Training occurs only every RetrainEvery bars, but inference runs on every bar. This prevents the weights from chasing every tick while still producing continuous output.

Confirmation Logic

The confirmation signal implements the two-condition filter:

      double prevEnsemble = (i > minBars) ? EnsembleBuffer[i - 1] : 0.0;
      double delta = ensemble - prevEnsemble;

      if(ensemble > 0.0 && delta > 0.0)
        {
         LongConfirmBuffer[i] = ensemble;    // green histogram
         ConfirmBuffer[i]     = 1.0;         // +1 for EA
        }
      else if(ensemble < 0.0 && delta < 0.0)
        {
         ShortConfirmBuffer[i] = ensemble;   // red histogram
         ConfirmBuffer[i]      = -1.0;       // -1 for EA
        }

Long confirmation requires the ensemble to be above zero and increasing. This means the spectral regime favors the upside and the conviction is building. Short confirmation is the mirror: below zero and decreasing. All other states produce zero — no confirmation in either direction.

This design ensures that the signal is not just about position (above/below zero) but also about momentum (direction of change). A declining positive ensemble means the bullish cycle is fading — not a good time to enter longs even though the raw signal is still positive.



Practical Usage

As a Directional Filter

GoertzelBrain is designed to be used as a confirmation filter, not a standalone entry signal. The intended workflow is:

  1. Your primary system generates a trade signal (e.g., based on price action, orderflow, or another indicator)
  2. Before executing, check GoertzelBrain's confirmation buffer
  3. Only take the trade if the confirmation aligns with your signal direction

To read the confirmation signal from an EA:

int hGBrain = iCustom(_Symbol, _Period, "GoertzelBrain",
                       MinPeriod, MaxPeriod, RetrainEvery, LearnRate, VolWindow);

double confirm[];
CopyBuffer(hGBrain, 3, 0, 1, confirm);  // buffer 3 = confirmation

if(confirm[0] > 0.5)   // long confirmed
   // ... proceed with buy logic
if(confirm[0] < -0.5)  // short confirmed
   // ... proceed with sell logic

Reading the Ensemble Line

The blue ensemble line provides additional context beyond the binary confirmation signal:

  • Magnitude indicates conviction. A value of ±0.03 is weak; ±0.15 is strong.
  • Zero crossings mark regime transitions. When the ensemble crosses zero, the dominant cycle's directional influence has reversed.
  • Flatline near zero suggests no dominant cycle is present or the MLPs cannot extract a reliable signal from the current spectral features. This is valuable information — it tells you the market lacks cyclical structure at the moment.

Parameter Optimization

The five input parameters can be optimized in the Strategy Tester:

  • MinPeriod / MaxPeriod — Defines which cycle frequencies are visible. Narrower bands focus on specific cycle regimes; wider bands capture more but dilute the signal.
  • RetrainEvery — Controls adaptation speed. Values of 50–200 work well for most instruments on M5–H1.
  • LearnRate — Values between 0.0001 and 0.005 are the practical range. Higher values risk weight explosion; lower values make adaptation too slow.
  • VolWindow — Should roughly match the typical swing duration on your timeframe.


Why This Approach Is Different

Several aspects distinguish GoertzelBrain from existing cycle indicators:

  • Adaptive interpretation. Traditional cycle indicators output raw spectral data — period, amplitude, phase — and leave interpretation entirely to the trader or a fixed set of rules. GoertzelBrain uses neural networks that learn to interpret spectral features in context, adapting their interpretation as market behavior changes.
  • Self-training. The MLPs retrain online during indicator calculation. There is no offline training phase, no external data pipeline, and no model file to manage. The indicator is self-contained and adapts automatically when applied to any symbol or timeframe.
  • Ensemble robustness. A single neural network can converge to a local optimum that produces poor predictions. By using ten independently initialized networks and averaging their outputs, the ensemble smooths out individual errors and produces a more stable signal.
  • Confirmation rather than prediction. The indicator does not attempt to predict specific price targets or turning points. Instead, it answers a simpler and more reliable question: does the current spectral regime confirm a directional bias? This makes it a natural complement to any existing trading system rather than a replacement.
  • Spectral dynamics as features. Most cycle indicators report a snapshot of the spectrum at each bar. GoertzelBrain includes the rate of change of spectral features (amplitude slope and period slope), capturing whether the cycle regime is strengthening, weakening, or transitioning. This temporal context is invisible to static spectral analysis.

Limitations and Considerations

  • Repainting. Like all Goertzel-based indicators, the spectral analysis recalculates when new data arrives. The ensemble output on the current bar may change as new bars form. For backtesting purposes, only use values from fully formed bars.
  • Random initialization. Because MLP weights are randomly initialized, the indicator will produce slightly different outputs each time it is applied to a chart. The ensemble mitigates this, but users should be aware that two instances of the indicator on the same chart will not produce identical results.
  • Computational cost. The Goertzel DFT runs for every bar in the calculation range, and with ten MLPs performing forward passes, the indicator is more computationally intensive than simple oscillators. On modern hardware this is negligible for live trading, but large backtests on M1 data may be noticeably slower.
  • No guaranteed edge. The indicator detects spectral structure and learns to interpret it, but the existence of a dominant cycle does not guarantee that it will continue. All cycle detection methods face this fundamental uncertainty. GoertzelBrain should be treated as one input among many in a trading decision, not as a crystal ball.



Conclusion

GoertzelBrain combines the precision of the Goertzel algorithm with the adaptive capacity of neural network ensembles to produce a cycle-aware directional filter for MetaTrader 5. By extracting multi-dimensional spectral features and learning to interpret them through online training, it bridges the gap between raw frequency analysis and actionable trading signals.

The indicator is designed as a confirmation tool — a go/no-go gate for trades generated by other systems. Its self-training architecture means it requires no external configuration beyond the five input parameters, and it adapts automatically to any instrument and timeframe.

All source code referenced in this article is available in the attached files. The indicator builds upon the CGoertzel and CGoertzelCycle classes from the article "Cycle analysis using the Goertzel algorithm", which must be present in the Include directory for compilation.


Files Attached

FileDescription
GoertzelBrain.mq5The complete indicator source code
Goertzel.mqhCore Goertzel DFT algorithm class
GoertzelCycle.mqhCycle analysis and peak detection class
Attached files |
GoertzelBrain.mq5 (21.64 KB)
Goertzel.mqh (6.31 KB)
GoertzelBrain.zip (10.33 KB)
MQL5 Wizard Techniques You should know (Part 86): Speeding Up Data Access with a Sparse Table for a Custom Trailing Class MQL5 Wizard Techniques You should know (Part 86): Speeding Up Data Access with a Sparse Table for a Custom Trailing Class
We revamp our earlier articles on testing trade setups with the MQL5 Wizard by putting a bit more emphasis on input data quality, cleaning, and handling. In the earlier articles we had looked at a lot of custom signal classes, usable by the wizard, so we now shift our focus to a custom trailing class, given that exiting is also a very important part in any trading system. Our broad theme for this particular piece data-efficiency and the O(1) range-query; the core ‘tech’ is MQL5, SQLite, Python-Polars; the Algorithm is the Sparse-Table while we will seek validation from the ATR Indicator.
Feature Engineering for ML (Part 1): Fractional Differentiation — Stationarity Without Memory Loss Feature Engineering for ML (Part 1): Fractional Differentiation — Stationarity Without Memory Loss
Integer differentiation forces a binary choice between stationarity and memory: returns (d=1) are stationary but discard all price-level information; raw prices (d=0) preserve memory but violate ML stationarity assumptions. We implement the fixed-width fractional differentiation (FFD) method from AFML Chapter 5, covering get_weights_ffd (iterative recurrence with threshold cutoff), frac_diff_ffd (bounded dot product per bar), and fracdiff_optimal (binary search for minimum stationary d*).
Fractal-Based Algorithm (FBA) Fractal-Based Algorithm (FBA)
The article presents a new metaheuristic method based on a fractal approach to partitioning the search space for solving optimization problems. The algorithm sequentially identifies and separates promising areas, creating a self-similar fractal structure that concentrates computing resources on the most promising areas. A unique mutation mechanism aimed at better solutions ensures an optimal balance between exploration and exploitation of the search space, significantly increasing the efficiency of the algorithm.
Formulating Dynamic Multi-Pair EA (Part 8): Time-of-Day Capital Rotation Approach Formulating Dynamic Multi-Pair EA (Part 8): Time-of-Day Capital Rotation Approach
This article presents a Time-of-Day capital rotation engine for MQL5 that allocates risk by trading session instead of using uniform exposure. We detail session budgets within a daily risk cap, dynamic lot sizing from remaining session risk, and automatic daily resets. Execution uses session-specific breakout and fade logic with ATR-based volatility confirmation. Readers gain a practical template to deploy capital where session conditions are statistically strongest while keeping exposure controlled throughout the day.