Cluster Money Flow Index [UAlgo]Cluster Money Flow Index is a zone based MFI structure tool designed to detect repeated Money Flow Index turning points and group them into meaningful reaction areas. Instead of treating every isolated MFI pivot as a standalone event, the script searches for clusters of nearby pivots that occur around similar MFI levels. When enough touches accumulate in the same area, the indicator promotes that region into a live zone.
The main idea is simple. If MFI repeatedly turns down from a similar high region, that area can behave like an overbought supply style zone inside the oscillator. If MFI repeatedly turns up from a similar low region, that area can behave like an oversold demand style zone. By clustering these repeated reactions, the script attempts to map oscillator structure in the same way traders often map support and resistance on price.
What makes this indicator especially useful is that the zones are not static. They can expand when fresh touches appear, they gain visual strength as more reactions accumulate, and they can later be invalidated if MFI decisively breaks beyond them. This creates a much more dynamic view than a simple overbought line, oversold line, or ordinary pivot marker.
The script also includes a smoothed MFI reference, optional center lines, zone labels, a live dashboard, and alert conditions when MFI enters active cluster zones. This makes the indicator useful both for structural oscillator analysis and for workflow monitoring.
In practical use, Cluster Money Flow Index can help highlight repeated MFI rejection areas, repeated MFI support areas, transition zones near the middle range, and regions where oscillator behavior has historically clustered before reversal or pause.
🔹 Features
🔸 Pivot Based MFI Structure Detection
The script detects confirmed MFI pivot highs and pivot lows using user defined left and right pivot settings. This means clusters are built only from confirmed oscillator turning points rather than from every small fluctuation.
🔸 Cluster Logic Instead of Single Pivot Logic
A new pivot does not automatically create a new zone. The script first checks whether that pivot is close enough to an existing valid cluster. If it is, the cluster gains another touch. If it is not, a new cluster is created.
🔸 Adaptive Proximity Threshold
Cluster sensitivity is based on MFI volatility. The script calculates the standard deviation of raw MFI and multiplies it by the user selected proximity multiplier. This makes zone grouping adapt to the current oscillator environment.
🔸 Minimum Touch Confirmation
A cluster is displayed only after it reaches the required minimum number of touches. This helps filter out weak one time reactions and focuses attention on repeated oscillator behavior.
🔸 Optional Zone Expansion
When enabled, the zone can expand with each new retest. If a fresh pivot extends beyond the current cluster boundary, the top or bottom is updated and the center is recalculated. This allows the zone to evolve naturally as more data arrives.
🔸 Dynamic Zone Strength Visualization
Zones become slightly more visible as touch count increases. This gives stronger clusters more visual weight and helps the user quickly distinguish weak from strong oscillator regions.
🔸 Overbought, Oversold, and Mid Context
Zone color is chosen from the zone center. Clusters centered high in the MFI range use the overbought color, clusters centered low use the oversold color, and clusters near the middle range use the mid color.
🔸 Optional Center Line and Labels
Each displayed cluster can include a center line and an information label showing whether the zone is an upper or lower type cluster, its approximate center level, and its total touch count.
🔸 Invalidation Logic
A zone remains valid until MFI breaks clearly beyond it. Upper clusters are invalidated if MFI pushes decisively above the zone. Lower clusters are invalidated if MFI drops decisively below it.
🔸 Dashboard Summary
A built in dashboard can show current MFI state, number of active upper and lower zones, strongest cluster strength, and the nearest upper and lower cluster centers.
🔸 Alert Support
Alerts are provided for:
MFI entering an upper cluster zone,
MFI entering a lower cluster zone,
MFI crossing above 80,
and MFI crossing below 20.
🔹 Calculations
1) Calculating Raw and Smoothed MFI
float rawMFI = ta.mfi(hlc3, mfiLen)
float smoothedMFI = ta.ema(rawMFI, mfiSmooth)
This is the starting point of the indicator.
The script first calculates the standard Money Flow Index from hlc3 using the selected MFI length. Then it applies an EMA smoothing pass to create a softer reference line.
The raw MFI is used for all pivot detection, clustering, invalidation, zone interaction, and alerts. The smoothed MFI is mainly a visual aid that helps the user see the broader oscillator path more clearly.
So the indicator always builds its logic from raw MFI structure while also giving the user a smoother secondary guide.
2) Defining the Cluster Object
type MFICluster
float top
float bottom
float center
bool isOB
int touches
int firstBarTime
int lastTouchTime
int firstBarIdx
bool isValid
bool isDisplayed
box zoneBox
line centerLine
label infoLabel
This object stores the full lifecycle of one MFI cluster zone.
It contains:
the zone top,
the zone bottom,
the center level,
whether the zone came from an upper pivot or lower pivot,
how many touches it has,
when it first formed,
when it was last touched,
whether it is still valid,
whether it has already been drawn,
and its visual objects.
So the script is not just plotting shapes. It is managing structured oscillator zones that have state, memory, and display properties.
3) Calculating the Adaptive Proximity Threshold
float mfiStd = ta.stdev(rawMFI, 50)
float proximity = math.max(2.0, mfiStd * proxMult)
This is the sensitivity engine of the clustering logic.
The script measures the standard deviation of raw MFI over the last fifty bars. It then multiplies that volatility measure by the user selected proximity multiplier. Finally, it enforces a minimum threshold of 2.0.
This means a new pivot is considered close enough to an existing cluster only if it lies within a volatility adjusted distance from the cluster center.
So the zone grouping automatically adapts to how noisy or how compressed the MFI environment currently is.
4) Detecting Confirmed MFI Pivot Highs and Lows
float mfiPH = ta.pivothigh(rawMFI, pivotLeft, pivotRight)
float mfiPL = ta.pivotlow(rawMFI, pivotLeft, pivotRight)
This is the pivot discovery step.
The script finds confirmed pivot highs and pivot lows directly on the raw MFI series. A pivot high becomes an upper type candidate cluster. A pivot low becomes a lower type candidate cluster.
Because the pivots are confirmed using both left and right bars, the script avoids reacting too early to temporary oscillator wiggles.
So all clustering logic is based on confirmed structure rather than live unconfirmed turns.
5) Checking Whether a Pivot Belongs to an Existing Cluster
method checkProximity(MFICluster this, float pivotVal, bool isOB, float threshold) =>
bool result = false
if this.isOB == isOB and this.isValid
if math.abs(pivotVal - this.center) <= threshold
result := true
result
This method decides whether a new pivot should strengthen an existing cluster.
A pivot can only join a cluster if:
the cluster is of the same type,
the cluster is still valid,
and the distance between the pivot value and the cluster center is less than or equal to the current threshold.
This is important because upper pivot highs are never mixed with lower pivot lows, and stale invalidated clusters are ignored.
So this method is the actual grouping filter that turns repeated nearby pivots into one shared zone.
6) Adding a New Touch to a Cluster
method addTouch(MFICluster this, float pivotVal, int pTime, bool shouldExpand) =>
this.touches += 1
this.lastTouchTime := pTime
if shouldExpand
if pivotVal > this.top
this.top := pivotVal
if pivotVal < this.bottom
this.bottom := pivotVal
this.center := (this.top + this.bottom) / 2.0
int(na)
Once a pivot is assigned to a cluster, this method updates the cluster state.
The touch count is incremented and the last touch time is refreshed. If zone expansion is enabled, the script also checks whether the new pivot extends above the current top or below the current bottom. If it does, the cluster boundaries are widened and the center is recalculated.
So clusters do not have to remain frozen. They can evolve as new oscillator reactions appear.
7) Creating a New Cluster When No Match Exists
if not wasClustered
float zoneHalf = math.max(proximity * 0.15, 0.8)
float zTop = pVal + zoneHalf
float zBot = pVal - zoneHalf
MFICluster newCl = MFICluster.new(
top = zTop,
bottom = zBot,
center = pVal,
isOB = isOB,
touches = 1,
firstBarTime = pTime,
lastTouchTime= pTime,
firstBarIdx = pBarIdx,
isValid = true,
isDisplayed = false)
If the new pivot does not belong to any existing valid cluster, the script creates a fresh cluster.
The initial zone width is determined from the current proximity threshold. Specifically, the script takes fifteen percent of that threshold and applies it equally above and below the pivot center, while enforcing a minimum half size of 0.8.
So every new cluster begins as a compact seed zone around one confirmed pivot and can later grow through repeated touches.
8) Minimum Touch Display Rule
if this.touches >= minT
This is the first major visual gate inside the drawing logic.
A cluster is not drawn just because it exists internally. It becomes visible only when its touch count reaches the user selected minimum touches threshold.
This helps reduce noise by hiding weak single touch or low confidence zones until repeated oscillator interaction has been proven.
So display is based on structural repetition, not just first occurrence.
9) Zone Strength and Opacity Calculation
f_calcOpacity(int touches, int baseOp) =>
float strength = math.min((touches - 1) / 8.0, 1.0)
int result = int(baseOp + (strength * 15))
math.min(result, 40)
This function converts touch count into visual intensity.
The script measures strength from the number of touches relative to a capped scale. Then it adds that strength bonus to the base opacity setting, while also imposing an upper limit.
This means zones with more touches appear slightly stronger and easier to notice than weaker zones.
So touch count influences not only logic, but also visual emphasis.
10) Zone Color Selection
f_zoneColor(float center) =>
center >= 70 ? obColor : center <= 30 ? osColor : midColor
This is the color classification rule.
If the cluster center is at or above 70, the zone uses the overbought color.
If the cluster center is at or below 30, the zone uses the oversold color.
Anything in between uses the mid color.
This is important because a cluster may come from an upper or lower pivot, but its actual center still determines how extreme its oscillator location really is.
So the visual color reflects where the cluster sits inside the MFI range.
11) Drawing the Zone Box
this.zoneBox := box.new(
left=this.firstBarTime, top=this.top, right=time, bottom=this.bottom,
border_color=borderCol, border_width=bWidth, bgcolor=fillCol,
xloc=xloc.bar_time)
Once the cluster qualifies for display, the script draws a box from the first touch time to the current bar time, with the cluster’s top and bottom as boundaries.
So the zone is not a single point marker. It becomes a persistent horizontal oscillator region that extends over time.
This makes the MFI structure much easier to interpret as a live area rather than isolated pivot dots.
12) Drawing the Optional Center Line
if drawCenter
this.centerLine := line.new(
x1=this.firstBarTime, y1=this.center, x2=time, y2=this.center,
color=color.new(baseCol, zoneOpacity - 5), style=line.style_dotted,
width=1, xloc=xloc.bar_time)
If enabled, the script also draws a center line through the middle of the cluster.
This gives the user a clean reference for the average reaction level inside the zone, which can be useful when the zone expands and becomes wider over time.
So the center line acts like an equilibrium guide inside the cluster.
13) Drawing the Info Label
string typeStr = this.isOB ? "OB" : "OS"
string lblText = typeStr + " · " + str.tostring(math.round(this.center, 1)) + " | ×" + str.tostring(this.touches)
this.infoLabel := label.new(
x=time, y=this.isOB ? this.top : this.bottom,
text=lblText, textcolor=textCol,
style=label.style_none, size=f_labelSize(lSize),
xloc=xloc.bar_time, textalign=text.align_right)
The label contains three pieces of information:
the cluster type,
the approximate center level,
and the touch count.
This means a user can immediately see whether the zone is an upper or lower cluster, where it is centered, and how strong it is based on repeated reactions.
So the label turns the zone into an interpretable structural object instead of only a colored band.
14) Updating Existing Displayed Zones
box.set_right(this.zoneBox, time)
box.set_bgcolor(this.zoneBox, fillCol)
box.set_border_color(this.zoneBox, borderCol)
box.set_border_width(this.zoneBox, bWidth)
box.set_top(this.zoneBox, this.top)
box.set_bottom(this.zoneBox, this.bottom)
Once a zone is already displayed and still valid, the script updates it on every bar.
It extends the right edge to the latest time, refreshes the fill and border styling, and updates the top and bottom in case the zone expanded after new touches.
So visible zones remain live and adaptive rather than remaining frozen in their original shape.
15) Zone Invalidation Logic
method invalidate(MFICluster this, float mfiVal) =>
bool broken = false
if this.isOB
if mfiVal > this.top + 2
broken := true
else
if mfiVal < this.bottom - 2
broken := true
if broken
this.isValid := false
broken
This method decides when a cluster has failed.
For upper type clusters, invalidation occurs if MFI pushes clearly above the zone top by more than two MFI points.
For lower type clusters, invalidation occurs if MFI falls clearly below the zone bottom by more than two MFI points.
This extra buffer is important because it avoids invalidating zones on tiny marginal touches.
So the script requires a decisive break beyond the zone before it stops treating that cluster as active structure.
16) Visual Handling of Invalidated Zones
else
box.set_bgcolor(this.zoneBox, color.new(baseCol, math.max(zoneOpacity + 20, 95)))
box.set_border_color(this.zoneBox, color.new(baseCol, math.max(zoneOpacity + 20, 95)))
if not na(this.centerLine)
line.set_style(this.centerLine, line.style_dashed)
line.set_color(this.centerLine, color.new(baseCol, 80))
When a cluster becomes invalid, the script does not delete it immediately. Instead, it fades the zone heavily and softens the center line.
This allows the user to keep the historical context of where the zone existed while also clearly seeing that it is no longer considered valid.
So invalidated zones remain on the pane as context, but not as active structure.
17) Detecting Whether MFI Is Inside an Active Cluster
if cl.isValid and cl.touches >= minTouches
if cl.isOB and rawMFI >= cl.bottom and rawMFI <= cl.top + 5
inOBZone := true
if not cl.isOB and rawMFI <= cl.top and rawMFI >= cl.bottom - 5
inOSZone := true
This block checks whether the current raw MFI value has entered a valid displayed cluster zone.
For upper clusters, the script allows a small tolerance above the zone.
For lower clusters, it allows a small tolerance below the zone.
This produces the conditions used by the entry alerts. So the alerts are not tied merely to MFI crossing 80 or 20. They can also trigger when MFI enters historically clustered oscillator reaction areas.
18) Dashboard Metrics
int obZoneCount = 0
int osZoneCount = 0
int strongMax = 0
float nearOB = na
float nearOS = na
if cl.touches > strongMax
strongMax := cl.touches
if cl.touches >= minTouches
if cl.center >= 70
obZoneCount += 1
else if cl.center <= 30
osZoneCount += 1
The dashboard summarizes the live structure.
It counts how many active displayed zones are centered in overbought and oversold territory, finds the highest touch count among all clusters, and tracks the nearest upper and lower cluster centers relative to current MFI.
So the dashboard gives a quick structural overview without requiring the user to visually inspect every zone one by one.
19) MFI State Classification for the Dashboard
string mfiState = rawMFI >= 80 ? "OVERBOUGHT" : rawMFI <= 20 ? "OVERSOLD" : rawMFI >= 50 ? "BULLISH" : "BEARISH"
This line classifies the current oscillator state into four broad conditions.
At or above 80 is treated as overbought.
At or below 20 is treated as oversold.
Between 50 and 80 is treated as bullish.
Between 20 and 50 is treated as bearish.
This gives the dashboard an easy to read directional context in addition to the cluster statistics.
20) Alert Conditions
alertcondition(inOBZone, title="MFI Entered OB Cluster Zone", message="Cluster MFI: Price entered an overbought cluster zone — watch for reversal")
alertcondition(inOSZone, title="MFI Entered OS Cluster Zone", message="Cluster MFI: Price entered an oversold cluster zone — watch for reversal")
alertcondition(ta.crossover(rawMFI, 80), title="MFI Crossed Above 80", message="Cluster MFI: MFI crossed above 80 — overbought territory")
alertcondition(ta.crossunder(rawMFI, 20), title="MFI Crossed Below 20", message="Cluster MFI: MFI crossed below 20 — oversold territory")
The script provides four alert types.
Two alerts are structural cluster alerts:
entering an upper cluster,
and entering a lower cluster.
Two alerts are classic threshold alerts:
crossing above 80,
and crossing below 20.
So the user can monitor both traditional MFI extremes and the more advanced cluster based structure.
Indicateur Pine Script®






















