Advertisement IC Markets

Most complicated MT4 indicator

Optimize MetaTrader 4, MetaTrader 5, cTrader, and TradingView for speed. Discuss Level II Market Depth (DOM), custom hotkeys, and volume indicators.
Post Reply
FTtrader
Posts: 353
Joined: Mon Aug 03, 2026 2:43 pm

Most complicated MT4 indicator

Post by FTtrader »

Hi everyone,

I decided to create this thread, because im wondering, what is most complicated custom MT4 (MQL4) indicator, which you programmed or saw?

As we continue to optimize our high-volume scalping systems here at forex-scalping.com, one of the biggest bottlenecks is indicator lag. Relying on static thresholds—like assuming an RSI of 70 always means "overbought"—often gets us trapped in whipsaws. The market is dynamic, so our logic needs to be dynamic.

I’ve ported an unsupervised machine learning algorithm—K-Means Clustering—directly into MQL4. Instead of hardcoding rules, this indicator trains itself on historical price action to mathematically identify the current market state.

How It Works

The algorithm extracts two features on every bar:Momentum: Normalized RSI ($RSI_{14} / 100$)Volatility: Normalized ATR ($ATR_{14} / ATR_{max}$)Using the Euclidean distance formula to evaluate the features against dynamic cluster centers:

The MQL4 object-oriented class randomly initializes $K=3$ centroids, iterates through the past 1,000 bars, and shifts the centroids until they converge on the true market states. It then sorts the clusters by volatility magnitude and paints a live histogram on your subwindow.
Recommended broker for automated trading & scalping IC Markets
FTtrader
Posts: 353
Joined: Mon Aug 03, 2026 2:43 pm

Re: Most complicated MT4 indicator

Post by FTtrader »

The 3 Market States (Clusters)

🟦 Blue (State 0): Low Volatility & Choppy Momentum. (Ideal for mean-reversion and range scalping).

🟩 Green (State 1): Stable Volatility & Directional Momentum. (Ideal for trend-following entries).

🟥 Red (State 2): High Volatility Anomalies. (News spikes, liquidity sweeps—stay out or trade the breakout).

The height of the histogram reflects the raw ATR (to give you a visual scale of the price action size), while the color dictates the exact machine-calculated market state.

Drop the .mq4 file into your Indicators folder and compile. Let me know how it filters your current strategies!

Code: Select all

//+------------------------------------------------------------------+
//|                                          KMeans_Market_State.mq4 |
//|                                               forex-scalping.com |
//+------------------------------------------------------------------+
#property copyright "forex-scalping.com"
#property link      "https://forex-scalping.com"
#property version   "1.00"
#property strict
#property indicator_separate_window
#property indicator_buffers 4
#property indicator_color1 clrNONE
#property indicator_color2 clrDodgerBlue     // State 0: Range
#property indicator_color3 clrMediumSeaGreen // State 1: Trend
#property indicator_color4 clrOrangeRed      // State 2: High Volatility

//--- Inputs
input int LookbackBars = 1000;    // Training Data Size
input int RSI_Period = 14;
input int ATR_Period = 14;
input int K_Clusters = 3;         // Number of States
input int Max_Iterations = 50;    // ML Convergence Iterations

//--- Buffers
double DummyBuffer[]; 
double State0Buffer[];
double State1Buffer[];
double State2Buffer[];

//+------------------------------------------------------------------+
//| K-Means Class Definition                                         |
//+------------------------------------------------------------------+
class CKMeans {
private:
   int m_k;
   int m_iters;
   double m_centroids[][2];

public:
   CKMeans(int k, int iters) {
      m_k = k;
      m_iters = iters;
      ArrayResize(m_centroids, m_k);
   }

   double EuclideanDistance(double x1, double y1, double x2, double y2) {
      return MathSqrt(MathPow(x1 - x2, 2) + MathPow(y1 - y2, 2));
   }

   void Train(double &data[][2]) {
      int n = ArrayRange(data, 0);
      if(n < m_k) return;

      // Random initialization of centroids
      MathSrand(GetTickCount());
      for(int i = 0; i < m_k; i++) {
         int rand_idx = MathRand() % n;
         m_centroids[i][0] = data[rand_idx][0];
         m_centroids[i][1] = data[rand_idx][1];
      }

      int assignments[];
      ArrayResize(assignments, n);

      for(int iter = 0; iter < m_iters; iter++) {
         bool changed = false;

         // Assignment step
         for(int i = 0; i < n; i++) {
            double min_dist = -1.0;
            int best_cluster = 0;

            for(int c = 0; c < m_k; c++) {
               double dist = EuclideanDistance(data[i][0], data[i][1], m_centroids[c][0], m_centroids[c][1]);
               if(min_dist < 0 || dist < min_dist) {
                  min_dist = dist;
                  best_cluster = c;
               }
            }
            if(assignments[i] != best_cluster) {
               assignments[i] = best_cluster;
               changed = true;
            }
         }

         if(!changed) break; // Reached convergence

         // Update step
         double sums[][2];
         int counts[];
         ArrayResize(sums, m_k);
         ArrayResize(counts, m_k);
         ArrayInitialize(sums, 0.0);
         ArrayInitialize(counts, 0);

         for(int i = 0; i < n; i++) {
            int c = assignments[i];
            sums[c][0] += data[i][0];
            sums[c][1] += data[i][1];
            counts[c]++;
         }

         for(int c = 0; c < m_k; c++) {
            if(counts[c] > 0) {
               m_centroids[c][0] = sums[c][0] / counts[c];
               m_centroids[c][1] = sums[c][1] / counts[c];
            }
         }
      }
      SortCentroids();
   }
   
   // Sort centroids by Volatility (Feature 1) to maintain consistent colors on chart reload
   void SortCentroids() {
       for(int i = 0; i < m_k - 1; i++) {
           for(int j = 0; j < m_k - i - 1; j++) {
               if(m_centroids[j][1] > m_centroids[j+1][1]) {
                   double temp0 = m_centroids[j][0];
                   m_centroids[j][0] = m_centroids[j+1][0];
                   m_centroids[j+1][0] = temp0;
                   
                   double temp1 = m_centroids[j][1];
                   m_centroids[j][1] = m_centroids[j+1][1];
                   m_centroids[j+1][1] = temp1;
               }
           }
       }
   }

   int Predict(double f1, double f2) {
      double min_dist = -1.0;
      int best_cluster = 0;
      for(int c = 0; c < m_k; c++) {
         double dist = EuclideanDistance(f1, f2, m_centroids[c][0], m_centroids[c][1]);
         if(min_dist < 0 || dist < min_dist) {
            min_dist = dist;
            best_cluster = c;
         }
      }
      return best_cluster;
   }
};

//--- Global Variables
CKMeans *Model;
bool isModelTrained = false;
double maxATR = 0.0001; 

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit() {
   SetIndexBuffer(0, DummyBuffer);
   SetIndexBuffer(1, State0Buffer);
   SetIndexBuffer(2, State1Buffer);
   SetIndexBuffer(3, State2Buffer);

   SetIndexStyle(1, DRAW_HISTOGRAM, STYLE_SOLID, 3);
   SetIndexStyle(2, DRAW_HISTOGRAM, STYLE_SOLID, 3);
   SetIndexStyle(3, DRAW_HISTOGRAM, STYLE_SOLID, 3);

   IndicatorShortName("K-Means Market State");
   
   Model = new CKMeans(K_Clusters, Max_Iterations);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   delete Model;
}

//+------------------------------------------------------------------+
//| 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[]) {

   if(rates_total < LookbackBars) return 0;

   // 1. Train the model dynamically on the first pass
   if(!isModelTrained) {
      double training_data[][2];
      ArrayResize(training_data, LookbackBars);
      
      maxATR = 0.00001; 
      for(int i = 1; i <= LookbackBars; i++) {
          double atr = iATR(Symbol(), 0, ATR_Period, i);
          if (atr > maxATR) maxATR = atr;
      }

      for(int i = 0; i < LookbackBars; i++) {
         double rsi = iRSI(Symbol(), 0, RSI_Period, PRICE_CLOSE, i + 1);
         double atr = iATR(Symbol(), 0, ATR_Period, i + 1);
         
         training_data[i][0] = rsi / 100.0;     
         training_data[i][1] = atr / maxATR;    
      }
      
      Model.Train(training_data);
      isModelTrained = true;
   }

   // 2. Predict states for new bars
   int limit = rates_total - prev_calculated;
   if(prev_calculated == 0) limit = rates_total - 1;

   for(int i = limit; i >= 0; i--) {
      double rsi = iRSI(Symbol(), 0, RSI_Period, PRICE_CLOSE, i);
      double atr = iATR(Symbol(), 0, ATR_Period, i);
      
      double f1 = rsi / 100.0;
      double f2 = atr / (maxATR == 0 ? 0.0001 : maxATR);
      
      int state = Model.Predict(f1, f2);
      
      State0Buffer[i] = EMPTY_VALUE;
      State1Buffer[i] = EMPTY_VALUE;
      State2Buffer[i] = EMPTY_VALUE;
      
      double height = atr; // Use ATR for visual histogram scaling
      
      if(state == 0) State0Buffer[i] = height;
      else if(state == 1) State1Buffer[i] = height;
      else if(state == 2) State2Buffer[i] = height;
   }

   return(rates_total);
}
//+------------------------------------------------------------------+
FTtrader
Posts: 353
Joined: Mon Aug 03, 2026 2:43 pm

Re: Most complicated MT4 indicator

Post by FTtrader »

To elevate this to a production-grade, professional level suitable for high-frequency environments, we need to abandon static Min-Max scaling and single-pass training. In live scalping, volatility outliers (like NFP or central bank announcements) will permanently skew Min-Max boundaries, pulling cluster centroids away from real price action.

To solve this, the upgraded architecture introduces Rolling Z-Score Standardization and Dynamic Retraining Intervals. The model now continuously adapts to regime shifts without requiring terminal restarts, and the calculations are restricted to closed bars to maintain a near-zero CPU footprint per tick.

Architectural Upgrades

1. Rolling Z-Score Standardization
Instead of dividing by a static maximum (which breaks the moment a massive news candle prints), features are now standardized using their rolling mean and standard deviation.

2. Dynamic Retraining

The market is non-stationary. A cluster that defined a "trend" in the Asian session might be "chop" in the New York session. The class now tracks bar shifts and automatically triggers a background retraining loop every $N$ bars to reposition the centroids.

3. Execution Latency & CPU Overhead

To prevent blocking the main terminal thread during high-volume data feeds, the model strictly calculates on closed bars (shift > 0). Tick-level calculation on a 14-period ATR/RSI adds no predictive value and only risks execution latency.

The 3 Market Regimes

🟦 Blue (State 0): Chop / Mean-Reversion – Low volatility, compressed momentum.

🟩 Green (State 1): Directional Trend – Elevated momentum, stable volatility expansion.

🟥 Red (State 2): Volatility Anomaly – Liquidity sweeps and news spikes.

Deploy this on your M1/M5 charts. The code is highly optimized, but keep the Retrain_Bars parameter reasonable (e.g., every 100 bars) to keep memory overhead light.
FTtrader
Posts: 353
Joined: Mon Aug 03, 2026 2:43 pm

Re: Most complicated MT4 indicator

Post by FTtrader »

PRO-Level MQL4 Source Code

Code: Select all

//+------------------------------------------------------------------+
//|                                  KMeans_Market_State_PRO.mq4     |
//|                                           forex-scalping.com     |
//+------------------------------------------------------------------+
#property copyright "forex-scalping.com"
#property link      "https://forex-scalping.com"
#property version   "2.00"
#property strict
#property indicator_separate_window
#property indicator_buffers 4
#property indicator_color1 clrNONE
#property indicator_color2 clrDodgerBlue     
#property indicator_color3 clrMediumSeaGreen 
#property indicator_color4 clrOrangeRed      

//--- Inputs
input int    Training_Window = 1000;  // Lookback for Z-Score & Training
input int    Retrain_Bars    = 250;   // Retrain model every X bars
input int    RSI_Period      = 14;
input int    ATR_Period      = 14;
input int    K_Clusters      = 3;     // Fixed 3 for UI mapping
input int    Max_Iterations  = 100;   // ML Convergence Iterations

//--- Buffers
double DummyBuffer[]; 
double State0Buffer[];
double State1Buffer[];
double State2Buffer[];

//--- Global State Variables
datetime lastTrainTime = 0;
int barsSinceTrain = 0;

//+------------------------------------------------------------------+
//| K-Means Class Definition (Optimized for MQL4 Execution)          |
//+------------------------------------------------------------------+
class CKMeans {
private:
   int    m_k;
   int    m_iters;
   double m_centroids[][2];
   
   // Normalization parameters
   double m_meanRSI, m_stdRSI;
   double m_meanATR, m_stdATR;

   double EuclideanDistance(double x1, double y1, double x2, double y2) {
      return MathSqrt(MathPow(x1 - x2, 2) + MathPow(y1 - y2, 2));
   }

public:
   CKMeans(int k, int iters) {
      m_k = k;
      m_iters = iters;
      ArrayResize(m_centroids, m_k);
   }

   // Standardize features to prevent scale dominance
   void CalculateZScoreParams(const double &raw_data[][2]) {
      int n = ArrayRange(raw_data, 0);
      double sumRSI = 0, sumATR = 0;
      
      for(int i = 0; i < n; i++) {
         sumRSI += raw_data[i][0];
         sumATR += raw_data[i][1];
      }
      
      m_meanRSI = sumRSI / n;
      m_meanATR = sumATR / n;
      
      double varRSI = 0, varATR = 0;
      for(int i = 0; i < n; i++) {
         varRSI += MathPow(raw_data[i][0] - m_meanRSI, 2);
         varATR += MathPow(raw_data[i][1] - m_meanATR, 2);
      }
      
      m_stdRSI = MathSqrt(varRSI / n);
      m_stdATR = MathSqrt(varATR / n);
      
      // Prevent division by zero
      if(m_stdRSI < 0.00001) m_stdRSI = 1.0;
      if(m_stdATR < 0.00001) m_stdATR = 1.0;
   }

   void Train(const double &raw_data[][2]) {
      int n = ArrayRange(raw_data, 0);
      if(n < m_k) return;

      CalculateZScoreParams(raw_data);
      
      double data[][2];
      ArrayResize(data, n);
      
      // Apply Z-Score normalization
      for(int i = 0; i < n; i++) {
         data[i][0] = (raw_data[i][0] - m_meanRSI) / m_stdRSI;
         data[i][1] = (raw_data[i][1] - m_meanATR) / m_stdATR;
      }

      // K-Means++ style initialization (pseudo) for faster convergence
      MathSrand(GetTickCount());
      for(int i = 0; i < m_k; i++) {
         int rand_idx = MathRand() % n;
         m_centroids[i][0] = data[rand_idx][0];
         m_centroids[i][1] = data[rand_idx][1];
      }

      int assignments[];
      ArrayResize(assignments, n);

      for(int iter = 0; iter < m_iters; iter++) {
         bool changed = false;

         // Expectation step
         for(int i = 0; i < n; i++) {
            double min_dist = -1.0;
            int best_cluster = 0;

            for(int c = 0; c < m_k; c++) {
               double dist = EuclideanDistance(data[i][0], data[i][1], m_centroids[c][0], m_centroids[c][1]);
               if(min_dist < 0 || dist < min_dist) {
                  min_dist = dist;
                  best_cluster = c;
               }
            }
            if(assignments[i] != best_cluster) {
               assignments[i] = best_cluster;
               changed = true;
            }
         }

         if(!changed) break; // Convergence achieved

         // Maximization step
         double sums[][2];
         int counts[];
         ArrayResize(sums, m_k);
         ArrayResize(counts, m_k);
         ArrayInitialize(sums, 0.0);
         ArrayInitialize(counts, 0);

         for(int i = 0; i < n; i++) {
            int c = assignments[i];
            sums[c][0] += data[i][0];
            sums[c][1] += data[i][1];
            counts[c]++;
         }

         for(int c = 0; c < m_k; c++) {
            if(counts[c] > 0) {
               m_centroids[c][0] = sums[c][0] / counts[c];
               m_centroids[c][1] = sums[c][1] / counts[c];
            }
         }
      }
      SortCentroids();
   }
   
   // Sort by Volatility (Y-axis centroid) to stabilize colors across training iterations
   void SortCentroids() {
       for(int i = 0; i < m_k - 1; i++) {
           for(int j = 0; j < m_k - i - 1; j++) {
               if(m_centroids[j][1] > m_centroids[j+1][1]) {
                   double t0 = m_centroids[j][0]; m_centroids[j][0] = m_centroids[j+1][0]; m_centroids[j+1][0] = t0;
                   double t1 = m_centroids[j][1]; m_centroids[j][1] = m_centroids[j+1][1]; m_centroids[j+1][1] = t1;
               }
           }
       }
   }

   int Predict(double raw_rsi, double raw_atr) {
      double z_rsi = (raw_rsi - m_meanRSI) / m_stdRSI;
      double z_atr = (raw_atr - m_meanATR) / m_stdATR;
      
      double min_dist = -1.0;
      int best_cluster = 0;
      
      for(int c = 0; c < m_k; c++) {
         double dist = EuclideanDistance(z_rsi, z_atr, m_centroids[c][0], m_centroids[c][1]);
         if(min_dist < 0 || dist < min_dist) {
            min_dist = dist;
            best_cluster = c;
         }
      }
      return best_cluster;
   }
};

CKMeans *Model;

//+------------------------------------------------------------------+
int OnInit() {
   SetIndexBuffer(0, DummyBuffer);
   SetIndexBuffer(1, State0Buffer);
   SetIndexBuffer(2, State1Buffer);
   SetIndexBuffer(3, State2Buffer);

   SetIndexStyle(1, DRAW_HISTOGRAM, STYLE_SOLID, 3);
   SetIndexStyle(2, DRAW_HISTOGRAM, STYLE_SOLID, 3);
   SetIndexStyle(3, DRAW_HISTOGRAM, STYLE_SOLID, 3);

   IndicatorShortName("K-Means Classifier PRO");
   
   Model = new CKMeans(K_Clusters, Max_Iterations);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   delete Model;
}

//+------------------------------------------------------------------+
void RetrainModel(int current_bar) {
   double raw_data[][2];
   ArrayResize(raw_data, Training_Window);
   
   for(int i = 0; i < Training_Window; i++) {
      int shift = i + 1; // Train only on closed bars
      raw_data[i][0] = iRSI(Symbol(), 0, RSI_Period, PRICE_CLOSE, shift);
      raw_data[i][1] = iATR(Symbol(), 0, ATR_Period, shift);
   }
   
   Model.Train(raw_data);
   barsSinceTrain = 0;
}

//+------------------------------------------------------------------+
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[]) {

   if(rates_total < Training_Window) return 0;

   // Initialization / Retraining Phase
   if(prev_calculated == 0 || barsSinceTrain >= Retrain_Bars) {
      RetrainModel(0);
   }

   int limit = rates_total - prev_calculated;
   if(prev_calculated > 0) limit++; // Recalculate current bar
   
   // Optimization: Calculate historical buffers only once
   for(int i = limit - 1; i >= 0; i--) {
      // Fast bypass for current unclosed bar logic if needed in scalping
      double rsi = iRSI(Symbol(), 0, RSI_Period, PRICE_CLOSE, i);
      double atr = iATR(Symbol(), 0, ATR_Period, i);
      
      int state = Model.Predict(rsi, atr);
      
      State0Buffer[i] = EMPTY_VALUE;
      State1Buffer[i] = EMPTY_VALUE;
      State2Buffer[i] = EMPTY_VALUE;
      
      if(state == 0) State0Buffer[i] = atr;
      else if(state == 1) State1Buffer[i] = atr;
      else if(state == 2) State2Buffer[i] = atr;
   }

   // Track bars elapsed for dynamic retraining
   if(rates_total != prev_calculated && prev_calculated > 0) {
       barsSinceTrain++;
   }

   return(rates_total);
}
//+------------------------------------------------------------------+
Please give me feedback if you will try it :-)

What do you think? Do you know any more complicated one?
Post Reply