Skip to content

Free TradingView indicator

Liquidity Sweeps

Finds equal highs and lows within an ATR tolerance and flags a sweep when a candle wicks through the level but closes back inside.

Smart moneyv1.0TradingView (Pine Script v6) Non-repainting signals Alerts

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

  1. Swing highs and lows are confirmed pivots with the chosen swing length on each side.
  2. 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.
  3. 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.
  4. 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

InputDefaultWhat it changes
Swing length5Sensitivity of swing detection
Equal-level tolerance (x ATR)0.10How close two swings must be
ATR length14ATR used for the tolerance
Swings remembered per side10How far back matches are searched
Max open levels per side10Cap on live levels
Keep swept / broken levels on chartOnGrey 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}}")

Key inputs

Key inputs of Liquidity Sweeps
InputDefaultDescription
Swing length5Bars on each side that confirm a swing high or low.
Equal-level tolerance0.10 × ATRMaximum price difference for two swings to count as equal.
Swings remembered per side10How many recent swings are compared with each new one.
Max open levels per side10Cap on tracked equal-high and equal-low levels.
Keep swept / broken levelsOnLeaves finished levels on the chart in grey.

Educational tools, not advice. Indicators and Expert Advisors describe price; they do not predict it, and past behaviour does not guarantee future results. Test everything on a demo account first and never risk money you cannot afford to lose. See our disclaimer.

Keep exploring

Related tradingview indicators