ICT MMXL Model [UAlgo]ICT MMXL Model is a liquidity sweep and exhaustion zone indicator designed to detect reversal candidates after price raids a prior swing and fails to hold beyond it. The script tracks confirmed swing highs and swing lows, waits for price to take one of those liquidity pools, then checks whether the sweep candle closes back inside the old structure. When that rejection is strong enough and the selected filters agree, the script builds an exhaustion zone that can later be monitored for retests or invalidation.
The core logic follows a very practical structure based workflow. A bearish setup forms when price sweeps above a prior swing high, but the same sweep candle closes back below that high. A bullish setup forms when price sweeps below a prior swing low, but the same sweep candle closes back above that low. This creates a failed expansion or exhaustion event, which is then projected forward on the chart as a zone.
What makes the script more selective than a simple sweep detector is its filter stack. It can require above average volume, a meaningful wick relative to the candle body, and visible momentum fade before the sweep occurs. These filters help focus the model on conditions where displacement appears to be tiring before the market raids liquidity and rejects the move.
Once a zone is created, it remains active until price closes beyond the invalidation boundary. While active, the script extends the zone, counts retests, optionally draws a mid line, and keeps the structure visible for future interaction. This makes the indicator useful not only for spotting the original sweep event, but also for tracking how price behaves when it revisits the exhaustion area later.
In practice, ICT MMXL Model can be used for liquidity based reversal analysis, rejection zone mapping, and structured retest monitoring. It is especially useful for traders who want a rules based way to identify failed sweep behavior and keep those zones on the chart until the market either respects or invalidates them.
🔹 Features
🔸 Prior Swing Liquidity Tracking
The script continuously records confirmed swing highs and swing lows using user defined pivot settings. Only the most recent group of swings is kept in memory, which keeps the model focused on nearby liquidity rather than distant historical structure.
🔸 Bearish and Bullish Sweep Detection
A bearish MMXL candidate appears when price trades above a prior swing high but closes back below it. A bullish MMXL candidate appears when price trades below a prior swing low but closes back above it. This captures the idea of a liquidity raid followed by rejection.
🔸 Volume Confirmation Filter
An optional volume filter requires the sweep candle to trade at or above a multiple of average volume. This helps avoid weaker sweeps that occur without meaningful participation.
🔸 Wick Rejection Filter
An optional wick filter requires the rejecting wick to be large relative to the candle body. For bearish setups, the upper wick must be large enough. For bullish setups, the lower wick must be large enough. This helps focus the model on candles that visibly reject the sweep area.
🔸 Momentum Fade Filter
An optional momentum fade filter checks whether body size and ATR have both decreased over a chosen lookback. This helps identify sweeps that occur after displacement has begun losing energy.
🔸 Automatic Exhaustion Zone Construction
When all setup conditions are met, the script creates a bullish or bearish exhaustion zone using the swept prior swing and the extreme of the sweep candle. This gives the user a clearly defined reaction area rather than a single line only.
🔸 Active Zone Extension
Once a zone is formed, it extends forward in time while it remains valid. This allows the trader to monitor later interaction without manually redrawing structure.
🔸 Retest Counting
The script tracks whether price reenters an active zone. Each first fresh entry increments the retest count, which is shown inside the zone label. This gives the user quick feedback on how many times the area has been revisited.
🔸 Mid Line Support
An optional dashed mid line can be drawn through the center of each zone. This can help visualize equilibrium inside the exhaustion area and provide an extra internal reference.
🔸 Sweep Marker Visualization
An optional visual marker is placed on the original sweep candle so the user can immediately see where the liquidity raid happened.
🔸 Violation Logic
A bullish zone is invalidated if price closes below the zone bottom. A bearish zone is invalidated if price closes above the zone top. Violated zones remain visible with muted styling so the user can still see where the structure failed.
🔸 Zone Limit Control
The script can limit the number of simultaneously active zones. If the maximum is reached, the oldest active zone is removed first. This helps control visual clutter.
🔸 Alert Support
Alerts are included for:
new bearish zone formation,
new bullish zone formation,
bullish zone retest,
bearish zone retest,
and zone violation.
This makes the script suitable for both visual chart work and alert driven monitoring.
🔹 Calculations
1) Defining Swing and Zone Objects
type SwingPoint
int barIdx
float price
bool isHigh
type ExhaustionZone
float zoneTop
float zoneBot
bool isBullish
int formBar
int formTime
int retests
bool isActive
bool inZone
bool hasMomentumFade
box zoneBox
line midLine
label zoneLabel
label sweepLabel
This is the structural foundation of the script.
A SwingPoint stores a confirmed swing location. It contains the bar index, the swing price, and whether the swing is a high or a low.
An ExhaustionZone stores the full lifecycle of a bearish or bullish MMXL setup. It keeps:
the zone top,
the zone bottom,
its direction,
when it formed,
how many retests it has seen,
whether it is still active,
whether price is currently inside it,
whether momentum fade was present,
and all visual objects used to draw it.
So before any detection begins, the script already has a clean data structure for both liquidity references and live exhaustion zones.
2) Confirming Swing Highs and Swing Lows
phPrice = ta.pivothigh(high, pivLeft, pivRight)
plPrice = ta.pivotlow (low, pivLeft, pivRight)
pivBarIdx = bar_index - pivRight
pivBarTime = time
if not na(phPrice)
sp = SwingPoint.new(barIdx=pivBarIdx, price=phPrice, isHigh=true)
swingHighs.push(sp)
if swingHighs.size() > maxSwings
swingHighs.shift()
if not na(plPrice)
sp = SwingPoint.new(barIdx=pivBarIdx, price=plPrice, isHigh=false)
swingLows.push(sp)
if swingLows.size() > maxSwings
swingLows.shift()
This is the first live detection step.
The script uses ta.pivothigh and ta.pivotlow to confirm structural highs and lows. Because a pivot is only confirmed after the chosen right side bars have passed, the actual pivot bar is not the current bar. That is why the script calculates:
pivBarIdx = bar_index - pivRight
pivBarTime = time
These values point back to the actual swing location.
When a confirmed pivot high appears, it is pushed into the swingHighs array. When a confirmed pivot low appears, it is pushed into the swingLows array. Each array is capped at the selected maximum size, so only the most recent prior swings are kept.
This means the script always maintains a fresh liquidity map of nearby highs and lows that may later be swept.
3) Referencing the Actual Sweep Candle
sweepBar = bar_index - pivRight
sweepHigh = high
sweepLow = low
sweepClose = close
sweepOpen = open
sweepVol = volume
sweepTime = time
This block is very important because the MMXL setup is evaluated on the pivot confirmed candle, not on the current bar.
Since pivot confirmation happens after pivRight bars, the actual candidate sweep candle lives pivRight bars in the past. The script therefore reads all needed values from that offset:
the high,
the low,
the open,
the close,
the volume,
and the time.
This allows the model to evaluate the true sweep candle rather than mixing its logic with the current bar.
4) Volume Filter Calculation
avgVol = ta.sma(volume, volLookback)
volOk = not useVolFilter or (sweepVol >= avgVol * volMult)
This is the first optional filter.
The script calculates average volume over the chosen lookback period and then reads that average at the sweep candle offset. It then compares the sweep candle volume against a user selected multiple of that average.
If the volume filter is enabled, the sweep candle must satisfy:
sweepVol >= avgVol * volMult
If the filter is disabled, volume automatically passes.
This helps the model avoid sweep candles that occur on weak participation and focus more on events where the raid happened with notable activity.
5) Momentum Fade Filter Calculation
isMomentumFading(int offset, int lookback) =>
bodyNow = math.abs(close - open )
bodyPrev = math.abs(close - open )
atrNow = ta.atr(14)
atrPrev = ta.atr(14)
bodyFade = bodyNow < bodyPrev
atrFade = atrNow < atrPrev
bodyFade and atrFade
This function checks whether displacement was weakening before the sweep occurred.
It compares two things:
the body size of the sweep candle versus the body size from earlier in the lookback window,
and the ATR at the sweep candle versus the ATR from earlier in the same window.
For momentum to be considered fading, both conditions must be true:
current body size must be smaller than the earlier body size,
and current ATR must be smaller than the earlier ATR.
That means the filter is looking for a sweep that occurs after both candle expansion and volatility have started to cool off. This can be useful for detecting conditions where price makes one final reach into liquidity as momentum begins to fade.
6) Bearish MMXL Detection Logic
if swingHighs.size() > 0 and barstate.isconfirmed
priorHigh = swingHighs.last().price
bearSweepCondition = sweepHigh > priorHigh and sweepClose < priorHigh
sweepBody = math.abs(sweepClose - sweepOpen)
upperWick = sweepHigh - math.max(sweepClose, sweepOpen)
bearWickOk = not useWickFilter or (sweepBody == 0.0 or upperWick >= sweepBody * wickRatio)
bearMomFade = momFade
momOkBear = not useMomFilter or bearMomFade
This is the core bearish setup logic.
First, the script checks that at least one prior swing high exists. It then defines the most recent stored swing high as the liquidity reference.
A bearish sweep exists only if both conditions are true:
the sweep candle trades above the prior swing high,
and the same sweep candle closes back below that prior high.
That is the classic failed buy side liquidity raid behavior.
Then the script calculates the candle body and upper wick. If the wick filter is enabled, the upper wick must be large enough relative to the body. If the momentum fade filter is enabled, the earlier momentum fade calculation must also pass.
So a bearish MMXL zone requires:
a sweep above prior highs,
a close back below the liquidity level,
and any enabled filters to agree.
7) Bullish MMXL Detection Logic
if swingLows.size() > 0 and barstate.isconfirmed
priorLow = swingLows.last().price
bullSweepCondition = sweepLow < priorLow and sweepClose > priorLow
sweepBody = math.abs(sweepClose - sweepOpen)
lowerWick = math.min(sweepClose, sweepOpen) - sweepLow
bullWickOk = not useWickFilter or (sweepBody == 0.0 or lowerWick >= sweepBody * wickRatio)
bullMomFade = momFade
momOkBull = not useMomFilter or bullMomFade
This is the mirror image of the bearish model.
The script checks that a prior swing low exists, then defines it as the current sell side liquidity reference.
A bullish sweep exists only if:
the sweep candle trades below the prior swing low,
and the same sweep candle closes back above that prior low.
That represents a failed sell side liquidity raid.
If the wick filter is enabled, the lower wick must be large enough relative to the body. If the momentum fade filter is enabled, the sweep must also occur while body size and ATR are both fading.
So a bullish MMXL zone requires:
a sweep below prior lows,
a close back above the liquidity level,
and all enabled filters to pass.
8) Preventing Duplicate Zone Creation
alreadyExists = false
for z in zones
if z.formTime == sweepTime
alreadyExists := true
break
This small block avoids duplicate zones from the same sweep candle.
Before creating a new zone, the script checks whether a zone with the same formation time already exists. If it does, the new one is ignored.
This matters because bullish and bearish checks both run inside the script, and repeated recalculation on confirmed bars could otherwise produce duplicate objects from the same event.
9) Controlling the Maximum Number of Active Zones
activeCount = 0
for z in zones
if z.isActive
activeCount += 1
if activeCount >= maxZones
for idx = 0 to zones.size() - 1
z = zones.get(idx)
if z.isActive
z.deleteDrawings()
z.isActive := false
break
This is the script’s chart management block.
Before a new zone is added, the script counts how many zones are still active. If that count has already reached the maximum allowed level, it searches from oldest to newest and removes the first active zone it finds.
The removed zone has its drawings deleted and its active state turned off.
This keeps the chart readable and prevents unlimited zone accumulation.
10) Building the Bearish and Bullish Zones
newZone = ExhaustionZone.new(
zoneTop = sweepHigh,
zoneBot = priorHigh,
isBullish = false,
formBar = sweepBar,
formTime = sweepTime,
retests = 0,
isActive = true,
inZone = false,
hasMomentumFade = bearMomFade,
zoneBox = na,
midLine = na,
zoneLabel = na,
sweepLabel = na)
newZone = ExhaustionZone.new(
zoneTop = priorLow,
zoneBot = sweepLow,
isBullish = true,
formBar = sweepBar,
formTime = sweepTime,
retests = 0,
isActive = true,
inZone = false,
hasMomentumFade = bullMomFade,
zoneBox = na,
midLine = na,
zoneLabel = na,
sweepLabel = na)
These two blocks define the actual exhaustion zones.
For bearish MMXL:
the zone top is the sweep candle high,
and the zone bottom is the prior swing high that got raided.
For bullish MMXL:
the zone top is the prior swing low that got raided,
and the zone bottom is the sweep candle low.
This is a very practical choice because the zone spans the rejection area between the liquidity level and the extreme of the sweep candle. In other words, the model does not just mark the swept price. It marks the full exhaustion region created by the failed expansion.
11) Drawing the Zone, Mid Line, and Sweep Marker
this.zoneBox := box.new(
left = this.formTime,
top = this.zoneTop,
right = time,
bottom = this.zoneBot,
border_color = bordCol,
border_width = 1,
bgcolor = fillCol,
xloc = xloc.bar_time)
if showMidLine
midLineCol = color.new(this.isBullish ? #00BCD4 : #FF5252, 55)
this.midLine := line.new(
x1 = this.formTime,
y1 = midPrice,
x2 = time,
y2 = midPrice,
color = midLineCol,
style = line.style_dashed,
width = 1,
xloc = xloc.bar_time)
This method handles the primary visual construction of each zone.
The script draws a filled box from the formation time to the current time, using the top and bottom prices of the exhaustion zone. If the mid line option is enabled, it also draws a dashed center line at the average of the zone top and zone bottom.
So the user gets:
the full zone body,
an optional equilibrium reference,
and live forward projection of the zone.
12) Zone Label and Momentum Fade Tag
fadeTag = this.hasMomentumFade ? " ⚡ Fading" : ""
labelText = (this.isBullish ? "MMXL ▲" : "MMXL ▼") +
" Retests: " + str.tostring(this.retests) + fadeTag
The label text is not static. It contains live information about the zone state.
Every label shows:
whether the zone is bullish or bearish,
how many retests have occurred,
and whether the setup formed with momentum fade.
This is useful because it lets the trader read structural context directly from the chart without opening settings or rechecking filters manually.
13) Extending the Zone and Updating Retests
method updateRight(ExhaustionZone this) =>
if not na(this.zoneBox)
box.set_right(this.zoneBox, time)
if showMidLine and not na(this.midLine)
line.set_x2(this.midLine, time)
if not na(this.zoneLabel)
label.set_x(this.zoneLabel, time)
fadeTagU = this.hasMomentumFade ? " ⚡ Fading" : ""
label.set_text(this.zoneLabel,
(this.isBullish ? "MMXL ▲" : "MMXL ▼") +
" Retests: " + str.tostring(this.retests) + fadeTagU)
if barstate.isconfirmed
for z in zones
if z.isActive
z.updateRight()
nowInside = z.isInsideZone()
if nowInside and not z.inZone
z.retests += 1
z.inZone := nowInside
These blocks manage the live state of every active zone.
On each confirmed bar, the script extends the right side of the box, the mid line, and the label to the latest time.
Then it checks whether price is currently inside the zone using isInsideZone() . If price has just entered the zone and it was not inside on the previous bar, the retest counter is incremented. The inZone flag is then updated.
This is important because retests are counted on fresh reentry, not on every bar that remains inside the zone. That prevents overcounting during long stays inside the same area.
14) How the Script Defines a Zone Retest
method isInsideZone(ExhaustionZone this) =>
high >= this.zoneBot and low <= this.zoneTop
This method defines zone interaction.
Price is considered inside a zone when the bar’s high is above or equal to the zone bottom and the bar’s low is below or equal to the zone top. In other words, any overlap between the current candle range and the zone counts as interaction.
This broad overlap definition is practical because zones are areas, not exact prices. A full candle close inside the zone is not required for retest counting.
15) Zone Violation Logic
method checkViolation(ExhaustionZone this) =>
if this.isBullish
close < this.zoneBot
else
close > this.zoneTop
This method defines when a zone is no longer valid.
For bullish zones:
a close below the zone bottom invalidates the structure.
For bearish zones:
a close above the zone top invalidates the structure.
This is a strict close based rule, which helps avoid invalidating the zone on a temporary wick only.
16) Visual Handling of Violated Zones
method violate(ExhaustionZone this) =>
this.isActive := false
if not na(this.zoneBox)
box.set_bgcolor(this.zoneBox, violatedCol)
box.set_border_color(this.zoneBox, color.new(#78909C, 50))
if showMidLine and not na(this.midLine)
line.set_color(this.midLine, color.new(#78909C, 70))
if not na(this.zoneLabel)
label.set_textcolor(this.zoneLabel, color.new(#78909C, 40))
label.set_text(this.zoneLabel,
(this.isBullish ? "MMXL ▲" : "MMXL ▼") +
" VIOLATED")
When a violation happens, the zone is not fully deleted. Instead, the script changes its state and appearance.
The zone becomes inactive, its colors are muted, and the label text changes to indicate that the structure has been violated.
This preserves chart history while clearly separating valid zones from failed ones.
17) Alert Logic for Formations, Retests, and Violations
alertcondition(newBearZone,
title = "MMXL — Bearish Exhaustion Zone Formed",
message = "MMXL: BSL sweep detected on {{ticker}} {{interval}}. Bearish exhaustion zone created.")
alertcondition(newBullZone,
title = "MMXL — Bullish Exhaustion Zone Formed",
message = "MMXL: SSL sweep detected on {{ticker}} {{interval}}. Bullish exhaustion zone created.")
alertcondition(bullRetest,
title = "MMXL — Bullish Zone Retest",
message = "MMXL: Price retesting bullish exhaustion zone on {{ticker}} {{interval}}.")
alertcondition(bearRetest,
title = "MMXL — Bearish Zone Retest",
message = "MMXL: Price retesting bearish exhaustion zone on {{ticker}} {{interval}}.")
alertcondition(bullViolated or bearViolated,
title = "MMXL — Zone Violated",
message = "MMXL: An exhaustion zone was violated (close outside zone) on {{ticker}} {{interval}}.")
These alerts turn the script into an event driven structure tool.
The first set alerts when a new bullish or bearish MMXL zone forms.
The second set alerts when price retests an active zone.
The third alert triggers when a zone becomes invalidated.
This makes the model useful not only for visual chart review, but also for live monitoring workflows.
Indicateur Pine Script®


















