We all know the traditional methods for plotting S/R levels—looking left, finding double tops, drawing discretionary lines, or relying on arbitrary round numbers. But when you are running high-frequency or sub-M5 scalping systems, discretion is your enemy. You need mathematically quantifiable liquidity zones.
Lately, I’ve been stripping away standard pivot/fractal logic and applying Unsupervised Machine Learning—specifically Agglomerative Hierarchical Clustering (AHC)—directly in MQL4 to identify institutional accumulation/distribution zones.
Here is a breakdown of the logic, the MQL4 source code, and how to weaponize it for scalping.
The Quantitative Logic:
Why AHC?Agglomerative clustering is a "bottom-up" approach. Instead of guessing where levels are, we let the price data speak for itself:Initialization: We extract the last $N$ swing highs and lows (local extrema) and treat every single price point as its own independent cluster.
Distance Metric: We use 1D Euclidean distance (simple absolute price difference).Centroid Linkage: The algorithm finds the two closest price points and merges them into a single cluster. The new cluster's price is the weighted average (centroid) of the merged points.Termination: This looping merge process continues until no two clusters are closer than a defined Threshold (e.g., 3 pips).
The MQL4 Implementation (Expert Level)
I’ve written a lightweight, non-blocking MQL4 indicator. It runs a 1D AHC algorithm over the recent history of fractals.Note: Since AHC has an algorithmic complexity of $O(n^3)$ in its naive state, I restrict the dataset to the last X swing points. For MT4, $N < 500$ processes in sub-milliseconds, meaning it won't freeze your terminal tick thread.
Code: Select all
//+------------------------------------------------------------------+
//| AHC_Levels_Pro.mq4 |
//| forex-scalping.com |
//+------------------------------------------------------------------+
#property copyright "Open Source for forex-scalping.com"
#property indicator_chart_window
input int LookbackBars = 1000; // Bars to scan for extrema
input int SwingPeriod = 5; // Bars for local high/low (Fractal logic)
input double ClusterThresholdPips = 4.0; // Merge threshold in Pips
input int MinClusterWeight = 2; // Minimum touches to plot a line
input color LevelColor = clrDodgerBlue;
struct Cluster {
double price;
int weight;
};
Cluster clusters[];
//+------------------------------------------------------------------+
int OnInit() {
ObjectsDeleteAll(0, "AHC_Level_");
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason) {
ObjectsDeleteAll(0, "AHC_Level_");
}
//+------------------------------------------------------------------+
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[])
{
// Only recalculate on a new bar to save CPU
if (prev_calculated == rates_total) return rates_total;
ObjectsDeleteAll(0, "AHC_Level_");
ArrayResize(clusters, 0);
// 1. Gather Data Points (Local Extrema)
int count = 0;
for(int i = SwingPeriod; i < LookbackBars && i < rates_total - SwingPeriod; i++) {
bool isHigh = true;
bool isLow = true;
for(int j = 1; j <= SwingPeriod; j++) {
if(high[i] <= high[i+j] || high[i] <= high[i-j]) isHigh = false;
if(low[i] >= low[i+j] || low[i] >= low[i-j]) isLow = false;
}
if(isHigh) {
ArrayResize(clusters, count + 1);
clusters[count].price = high[i];
clusters[count].weight = 1;
count++;
}
if(isLow) {
ArrayResize(clusters, count + 1);
clusters[count].price = low[i];
clusters[count].weight = 1;
count++;
}
}
// 2. Agglomerative Hierarchical Clustering (Centroid Linkage)
double thresholdPts = ClusterThresholdPips * 10 * Point; // Adjust for 5-digit brokers
if(StringFind(Symbol(), "JPY") >= 0) thresholdPts = ClusterThresholdPips * Point; // JPY fix (simplistic)
bool merged = true;
while(merged && count > 1) {
merged = false;
double min_dist = 999999.0;
int merge_i = -1, merge_j = -1;
// Find closest pair
for(int i = 0; i < count; i++) {
if(clusters[i].weight == 0) continue;
for(int j = i + 1; j < count; j++) {
if(clusters[j].weight == 0) continue;
double dist = MathAbs(clusters[i].price - clusters[j].price);
if(dist < min_dist) {
min_dist = dist;
merge_i = i;
merge_j = j;
}
}
}
// Merge if within threshold
if(min_dist <= thresholdPts && merge_i != -1) {
// Calculate new weighted centroid
double total_weight = clusters[merge_i].weight + clusters[merge_j].weight;
clusters[merge_i].price = ((clusters[merge_i].price * clusters[merge_i].weight) +
(clusters[merge_j].price * clusters[merge_j].weight)) / total_weight;
clusters[merge_i].weight += clusters[merge_j].weight;
clusters[merge_j].weight = 0; // Mark j as dead
merged = true;
}
}
// 3. Render the S/R Clusters
int drawn = 0;
for(int i = 0; i < count; i++) {
if(clusters[i].weight >= MinClusterWeight) {
string objName = "AHC_Level_" + IntegerToString(drawn);
ObjectCreate(0, objName, OBJ_HLINE, 0, 0, clusters[i].price);
ObjectSetInteger(0, objName, OBJPROP_COLOR, LevelColor);
// Thicker line for higher clustered weights
int width = clusters[i].weight > 4 ? 2 : 1;
ObjectSetInteger(0, objName, OBJPROP_WIDTH, width);
// Optional: Add text label with cluster weight
ObjectSetString(0, objName, OBJPROP_TEXT, " W:" + IntegerToString(clusters[i].weight));
drawn++;
}
}
return rates_total;
}
//+------------------------------------------------------------------+Threshold Tuning: The ClusterThresholdPips parameter is the secret sauce. On the EURUSD M1/M5, a 3.0 to 4.5 pip threshold clusters the noise into highly reactive institutional levels. If you trade higher volatility pairs (like GBPJPY or Gold), you must scale this up (e.g., 8-12 pips) to account for structural market depth.
The "Weight" Variable: The script outputs a weight (W:X) on each line. A weight of 2 means a standard double top/bottom. A weight of 5+ indicates a massive liquidity shelf.
Execution Strategy: Do not place blind limit orders. Use these nodes as your areas of interest. When price approaches a heavy cluster (Weight 4+), switch to your order flow/tick chart and look for:
Deceleration (Delta divergence).
A micro-break of structure (mBOS) on the 1-minute chart.
Enter with a tight stop just outside the cluster's algorithmic threshold.
Open for Discussion
I intentionally kept the linkage method as Centroid Linkage because it's fast in C/MQL4. Has anyone here experimented with Ward's Method or Complete Linkage for S/R modeling via external Python scripts/ZMQ? I'm curious if minimizing the variance of the clusters yields sharper bounce zones than simply pulling the weighted mean.
Let me know if you run this through the strategy tester or throw it on a live chart. Happy hunting.