What it does
Clusters of equal highs or equal lows often attract attention because many traders place stops just beyond them. This indicator finds those levels automatically and watches for a sweep: a candle that trades through the level and then closes back on the original side. It labels equal highs (EQH), equal lows (EQL) and each sweep.
How it works
- Swing highs and lows are confirmed pivots with the chosen swing length on each side.
- Each new swing high is compared with recent swing highs that price has not clearly traded above since. If the two prices differ by no more than the tolerance (a multiple of ATR 14), they form an equal-highs level at the higher of the two prices. A dotted line joins them. Equal lows work the same way at the lower of the two prices.
- On every closed candle, each open level is checked:
- If the high goes above an equal-highs level but the candle closes below it, the level is swept and a "Sweep" label appears.
- If the candle closes above the level, the level is broken (accepted through), not swept, and no signal is given.
- Swept and broken levels stop extending and turn grey, or are removed if you prefer.
All lists are bounded, so old lines and labels are deleted before chart limits are reached.
How to use it
- A sweep of equal highs followed by weakness is read by many traders as a sign that buy-side liquidity was taken and the move may fade. A sweep of equal lows is the mirror case.
- Treat a sweep as a prompt to look closer, not as an entry by itself. Confirmation such as a change of character on a lower timeframe or a rejection candle helps filter weak signals.
- Sweeps around major sessions and news are common. Our forex sessions page shows when liquidity is usually highest.
- A tight tolerance (0.05 ATR) finds only near-perfect equal levels. A wider one (0.2 ATR) finds more, looser clusters.
Alerts: equal highs formed, equal lows formed, highs swept, lows swept. All fire on candle close.
Settings
| Input | Default | What it changes |
|---|---|---|
| Swing length | 5 | Sensitivity of swing detection |
| Equal-level tolerance (x ATR) | 0.10 | How close two swings must be |
| ATR length | 14 | ATR used for the tolerance |
| Swings remembered per side | 10 | How far back matches are searched |
| Max open levels per side | 10 | Cap on live levels |
| Keep swept / broken levels on chart | On | Grey history or a clean chart |
Limitations
Equal levels are only found after the second swing confirms, which is several candles after it formed. A sweep is judged on one candle's close, so a level that is pierced and reclaimed over two candles will be recorded as broken, not swept. In strong trends levels often break cleanly with no reversal. Wicks depend on your data feed and spread, so sweeps on one broker's chart may not appear on another. No pattern here predicts direction with certainty.
Pine Script source
In TradingView open Pine Editor, paste the code, click Save and then Add to chart.
FXP_LiquiditySweeps.pine
164 lines
// © FXPIPS ONLINE — https://fxpips.online/tradingview-indicators/liquidity-sweeps/
// License: MIT. Educational tool — not financial advice. Trading involves substantial risk.
//@version=6
// Finds equal highs and equal lows (two confirmed swings within an ATR-based tolerance)
// and flags a sweep when a closed candle wicks through the level but closes back inside.
// A close beyond the level counts as a break, not a sweep.
indicator("FXP Liquidity Sweeps", shorttitle = "FXP Sweeps", overlay = true, max_lines_count = 500, max_labels_count = 500)
// ── Inputs ─────────────────────────────────────────────────────────────
grpDet = "Detection"
swingLen = input.int(5, "Swing length (bars each side)", minval = 1, maxval = 50, group = grpDet)
tolAtr = input.float(0.10, "Equal-level tolerance (x ATR)", minval = 0.01, step = 0.01, group = grpDet, tooltip = "Two swings count as equal when their prices differ by no more than this many ATRs.")
atrLen = input.int(14, "ATR length", minval = 1, group = grpDet)
maxPivots = input.int(10, "Swings remembered per side", minval = 2, maxval = 50, group = grpDet)
grpDisp = "Display"
maxLevels = input.int(10, "Max open levels per side", minval = 1, maxval = 50, group = grpDisp)
keepOld = input.bool(true, "Keep swept / broken levels on chart", group = grpDisp)
colHigh = input.color(#ef5350, "Equal highs (buy-side liquidity)", group = grpDisp)
colLow = input.color(#26a69a, "Equal lows (sell-side liquidity)", group = grpDisp)
colDone = input.color(color.gray, "Swept / broken level", group = grpDisp)
// ── Types and storage ──────────────────────────────────────────────────
type Level
line ln
label lb
float price
var array<float> hiCandP = array.new<float>()
var array<int> hiCandB = array.new<int>()
var array<float> hiCandT = array.new<float>()
var array<float> loCandP = array.new<float>()
var array<int> loCandB = array.new<int>()
var array<float> loCandT = array.new<float>()
var array<Level> hiLevels = array.new<Level>()
var array<Level> loLevels = array.new<Level>()
finishLevel(Level lv) =>
if keepOld
lv.ln.set_x2(bar_index)
lv.ln.set_color(colDone)
lv.lb.set_textcolor(colDone)
else
lv.ln.delete()
lv.lb.delete()
true
float atr = ta.atr(atrLen)
ph = ta.pivothigh(high, swingLen, swingLen)
pl = ta.pivotlow(low, swingLen, swingLen)
// ── Check open levels for sweeps / breaks (closed bars) ────────────────
bool sweptHigh = false
bool sweptLow = false
if barstate.isconfirmed and hiLevels.size() > 0
for i = hiLevels.size() - 1 to 0
Level lv = hiLevels.get(i)
if high > lv.price
if close < lv.price
sweptHigh := true
label.new(bar_index, high, "Sweep", style = label.style_label_down, color = color.new(color.black, 100), textcolor = colHigh, size = size.small)
hiLevels.remove(i)
finishLevel(lv)
else
lv.ln.set_x2(bar_index)
if barstate.isconfirmed and loLevels.size() > 0
for i = loLevels.size() - 1 to 0
Level lv = loLevels.get(i)
if low < lv.price
if close > lv.price
sweptLow := true
label.new(bar_index, low, "Sweep", style = label.style_label_up, color = color.new(color.black, 100), textcolor = colLow, size = size.small)
loLevels.remove(i)
finishLevel(lv)
else
lv.ln.set_x2(bar_index)
// ── Forget swing candidates that price has clearly traded beyond ───────
if barstate.isconfirmed and hiCandP.size() > 0
for i = hiCandP.size() - 1 to 0
if high > hiCandP.get(i) + hiCandT.get(i)
hiCandP.remove(i)
hiCandB.remove(i)
hiCandT.remove(i)
if barstate.isconfirmed and loCandP.size() > 0
for i = loCandP.size() - 1 to 0
if low < loCandP.get(i) - loCandT.get(i)
loCandP.remove(i)
loCandB.remove(i)
loCandT.remove(i)
// ── New swings: match against remembered swings ────────────────────────
bool newEqh = false
bool newEql = false
if barstate.isconfirmed and not na(ph)
float tol = tolAtr * nz(atr)
bool matched = false
if hiCandP.size() > 0
for i = hiCandP.size() - 1 to 0
float p = hiCandP.get(i)
if math.abs(ph - p) <= tol
float lvl = math.max(ph, p)
line ln = line.new(hiCandB.get(i), lvl, bar_index, lvl, color = colHigh, style = line.style_dotted, width = 1)
label lb = label.new(bar_index - swingLen, lvl, "EQH", style = label.style_label_down, color = color.new(color.black, 100), textcolor = colHigh, size = size.tiny)
hiLevels.push(Level.new(ln, lb, lvl))
while hiLevels.size() > maxLevels
Level old = hiLevels.shift()
old.ln.delete()
old.lb.delete()
hiCandP.remove(i)
hiCandB.remove(i)
hiCandT.remove(i)
matched := true
newEqh := true
break
if not matched
hiCandP.push(ph)
hiCandB.push(bar_index - swingLen)
hiCandT.push(tol)
while hiCandP.size() > maxPivots
hiCandP.shift()
hiCandB.shift()
hiCandT.shift()
if barstate.isconfirmed and not na(pl)
float tol = tolAtr * nz(atr)
bool matched = false
if loCandP.size() > 0
for i = loCandP.size() - 1 to 0
float p = loCandP.get(i)
if math.abs(pl - p) <= tol
float lvl = math.min(pl, p)
line ln = line.new(loCandB.get(i), lvl, bar_index, lvl, color = colLow, style = line.style_dotted, width = 1)
label lb = label.new(bar_index - swingLen, lvl, "EQL", style = label.style_label_up, color = color.new(color.black, 100), textcolor = colLow, size = size.tiny)
loLevels.push(Level.new(ln, lb, lvl))
while loLevels.size() > maxLevels
Level old = loLevels.shift()
old.ln.delete()
old.lb.delete()
loCandP.remove(i)
loCandB.remove(i)
loCandT.remove(i)
matched := true
newEql := true
break
if not matched
loCandP.push(pl)
loCandB.push(bar_index - swingLen)
loCandT.push(tol)
while loCandP.size() > maxPivots
loCandP.shift()
loCandB.shift()
loCandT.shift()
// ── Alerts ─────────────────────────────────────────────────────────────
alertcondition(newEqh, "Equal highs formed", "{{ticker}} {{interval}}: equal highs formed")
alertcondition(newEql, "Equal lows formed", "{{ticker}} {{interval}}: equal lows formed")
alertcondition(sweptHigh, "Highs swept (bearish sweep)", "{{ticker}} {{interval}}: equal highs swept and closed back below at {{close}}")
alertcondition(sweptLow, "Lows swept (bullish sweep)", "{{ticker}} {{interval}}: equal lows swept and closed back above at {{close}}")

