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.
Most complicated MT4 indicator
Re: Most complicated MT4 indicator
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!
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);
}
//+------------------------------------------------------------------+Re: Most complicated MT4 indicator
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.
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
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.
Re: Most complicated MT4 indicator
PRO-Level MQL4 Source Code
Please give me feedback if you will try it 
What do you think? Do you know any more complicated one?
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);
}
//+------------------------------------------------------------------+What do you think? Do you know any more complicated one?