Skip to content

Free TradingView indicator

Supply and Demand Zones

Draws supply and demand zones from base candles before strong departures and tracks each zone as fresh, tested or broken.

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

What it does

Supply and demand zones mark areas where price paused briefly and then left in a hurry. The pause, a few small "base" candles, is drawn as a zone. A base followed by a strong rally becomes a demand zone and a base followed by a strong drop becomes a supply zone. Each zone is then tracked as fresh (not revisited), tested (price came back to it) or broken (price closed through it).

How it works

  1. A departure candle is a closed candle whose body is at least the set multiple of ATR (14). A bullish departure can create demand and a bearish one can create supply.
  2. The script counts the consecutive small candles right before it: each must have a body of no more than the base multiple of ATR, up to the maximum base length. At least one base candle is needed.
  3. Demand zone: from the lowest low of the base up to the highest body top of the base. Supply zone: from the lowest body bottom of the base up to the highest high. The departure candle must close beyond the zone.
  4. Optionally, a zone that overlaps an existing active zone of the same type is skipped to avoid stacking.
  5. On every closed candle, active zones are updated:
    • Tested: price comes back into the zone after having been away. The test count goes up and the label changes, for example "Demand (tested 1x)".
    • Broken: a candle closes beyond the far side of the zone (below a demand zone or above a supply zone).
    • Retired: once the test limit is reached and price leaves again.

Broken and retired zones stop extending and turn grey, or are deleted. All lists are capped.

How to use it

  • Fresh zones are the ones most supply and demand traders focus on, on the idea that orders left behind have not yet been filled. Each test is often seen as weakening a zone.
  • Higher timeframes give fewer, larger zones. A common workflow is to mark H4 or daily zones and look for entries on a lower timeframe as price reaches them.
  • Zones are areas, not exact prices. Many traders place stops beyond the far edge and use the Position Size Calculator to keep risk fixed.
  • For the general idea of price levels, see the lesson on support and resistance.

Alerts: new demand zone, new supply zone, demand tested, supply tested, demand broken, supply broken.

Settings

InputDefaultWhat it changes
ATR length14ATR used by the size rules
Departure candle: min body (x ATR)1.5Strength of the move away
Base candle: max body (x ATR)0.5What counts as a pause
Max base candles3Base length
Skip zones overlapping an active zoneOnAvoids stacked zones
Retire zone after this many tests3Lifetime of a zone
Keep broken zones (greyed)OnGrey history
Max active zones per side10Cap on live zones

Limitations

Zone rules are a simplification: different traders draw zones differently, and this script's thresholds will miss some zones you would mark by hand and include others you would not. A strong departure is only known after it closes, so zones appear one candle after the move. Zones can break without any reaction, especially around news. Results depend on ATR, so a sudden volatility change shifts what counts as a base or a departure.

Pine Script source

In TradingView open Pine Editor, paste the code, click Save and then Add to chart.

FXP_SupplyDemandZones.pine

188 lines

// © FXPIPS ONLINE — https://fxpips.online/tradingview-indicators/supply-demand-zones/
// License: MIT. Educational tool — not financial advice. Trading involves substantial risk.
//@version=6
// Finds base candles (small bodies) that come right before a strong departure candle
// and draws them as demand (before a rally) or supply (before a drop) zones. Each zone is
// tracked as fresh, tested or broken. Everything is evaluated on closed candles.
indicator("FXP Supply and Demand Zones", shorttitle = "FXP S/D Zones", overlay = true, max_boxes_count = 500)

// ── Inputs ─────────────────────────────────────────────────────────────
grpDet     = "Detection"
atrLen     = input.int(14, "ATR length", minval = 1, group = grpDet)
departMult = input.float(1.5, "Departure candle: min body (x ATR)", minval = 0.5, step = 0.1, group = grpDet)
baseMult   = input.float(0.5, "Base candle: max body (x ATR)", minval = 0.1, step = 0.05, group = grpDet)
maxBase    = input.int(3, "Max base candles", minval = 1, maxval = 6, group = grpDet)
skipOver   = input.bool(true, "Skip zones overlapping an active zone", group = grpDet)

grpSt      = "States"
maxTests   = input.int(3, "Retire zone after this many tests", minval = 1, maxval = 10, group = grpSt)
keepBroken = input.bool(true, "Keep broken zones (greyed)", group = grpSt)

grpDisp    = "Display"
maxZones   = input.int(10, "Max active zones per side", minval = 1, maxval = 50, group = grpDisp)
maxBroken  = input.int(10, "Max broken zones kept", minval = 0, maxval = 100, group = grpDisp)
colDemand  = input.color(#26a69a, "Demand", group = grpDisp)
colSupply  = input.color(#ef5350, "Supply", group = grpDisp)
colBroken  = input.color(color.gray, "Broken / retired", group = grpDisp)

// ── Types and storage ──────────────────────────────────────────────────
type Zone
    box   bx
    float top
    float bottom
    bool  isDemand
    int   tests
    bool  away

var array<Zone> demand = array.new<Zone>()
var array<Zone> supply = array.new<Zone>()
var array<Zone> broken = array.new<Zone>()

zoneText(Zone z) =>
    string kind = z.isDemand ? "Demand" : "Supply"
    z.tests == 0 ? kind + " (fresh)" : kind + " (tested " + str.tostring(z.tests) + "x)"

zoneColor(Zone z) =>
    color base = z.isDemand ? colDemand : colSupply
    z.tests == 0 ? color.new(base, 78) : color.new(base, 90)

retireZone(Zone z) =>
    if keepBroken and maxBroken > 0
        z.bx.set_right(bar_index)
        z.bx.set_bgcolor(color.new(colBroken, 90))
        z.bx.set_border_color(color.new(colBroken, 70))
        z.bx.set_text_color(colBroken)
        broken.push(z)
        while broken.size() > maxBroken
            Zone old = broken.shift()
            old.bx.delete()
    else
        z.bx.delete()
    true

bool conf = barstate.isconfirmed
float atr = ta.atr(atrLen)

// ── Update existing zones ──────────────────────────────────────────────
bool demandTested = false
bool supplyTested = false
bool demandBroken = false
bool supplyBroken = false

if demand.size() > 0
    for i = demand.size() - 1 to 0
        Zone z = demand.get(i)
        if conf
            if close < z.bottom
                demandBroken := true
                z.bx.set_text("Demand (broken)")
                demand.remove(i)
                retireZone(z)
                continue
            if low <= z.top
                if z.away
                    z.tests := z.tests + 1
                    z.away := false
                    demandTested := true
                    z.bx.set_text(zoneText(z))
                    z.bx.set_bgcolor(zoneColor(z))
            else
                z.away := true
            if z.tests >= maxTests and z.away
                z.bx.set_text("Demand (retired)")
                demand.remove(i)
                retireZone(z)
                continue
        z.bx.set_right(bar_index)

if supply.size() > 0
    for i = supply.size() - 1 to 0
        Zone z = supply.get(i)
        if conf
            if close > z.top
                supplyBroken := true
                z.bx.set_text("Supply (broken)")
                supply.remove(i)
                retireZone(z)
                continue
            if high >= z.bottom
                if z.away
                    z.tests := z.tests + 1
                    z.away := false
                    supplyTested := true
                    z.bx.set_text(zoneText(z))
                    z.bx.set_bgcolor(zoneColor(z))
            else
                z.away := true
            if z.tests >= maxTests and z.away
                z.bx.set_text("Supply (retired)")
                supply.remove(i)
                retireZone(z)
                continue
        z.bx.set_right(bar_index)

// ── Detect new zones ───────────────────────────────────────────────────
float bodyNow  = math.abs(close - open)
bool  departUp = conf and close > open and bodyNow >= departMult * nz(atr)
bool  departDn = conf and close < open and bodyNow >= departMult * nz(atr)

// count consecutive base candles right before the departure candle
int baseCount = 0
if departUp or departDn
    for i = 1 to maxBase
        if math.abs(close[i] - open[i]) <= baseMult * nz(atr)
            baseCount += 1
        else
            break

overlaps(array<Zone> zones, float t, float b) =>
    bool hit = false
    if zones.size() > 0
        for j = 0 to zones.size() - 1
            Zone z = zones.get(j)
            if t >= z.bottom and b <= z.top
                hit := true
    hit

bool newDemand = false
bool newSupply = false

if baseCount > 0
    float bodyTop = math.max(open[1], close[1])
    float bodyBot = math.min(open[1], close[1])
    float hiWick  = high[1]
    float loWick  = low[1]
    for i = 1 to baseCount
        bodyTop := math.max(bodyTop, math.max(open[i], close[i]))
        bodyBot := math.min(bodyBot, math.min(open[i], close[i]))
        hiWick  := math.max(hiWick, high[i])
        loWick  := math.min(loWick, low[i])
    if departUp
        float t = bodyTop
        float b = loWick
        if t > b and close > t and not (skipOver and overlaps(demand, t, b))
            box bx = box.new(bar_index - baseCount, t, bar_index, b, border_color = color.new(colDemand, 50), bgcolor = color.new(colDemand, 78), text = "Demand (fresh)", text_color = colDemand, text_size = size.small, text_halign = text.align_right)
            demand.push(Zone.new(bx, t, b, true, 0, true))
            newDemand := true
            while demand.size() > maxZones
                Zone old = demand.shift()
                old.bx.delete()
    if departDn
        float t = hiWick
        float b = bodyBot
        if t > b and close < b and not (skipOver and overlaps(supply, t, b))
            box bx = box.new(bar_index - baseCount, t, bar_index, b, border_color = color.new(colSupply, 50), bgcolor = color.new(colSupply, 78), text = "Supply (fresh)", text_color = colSupply, text_size = size.small, text_halign = text.align_right)
            supply.push(Zone.new(bx, t, b, false, 0, true))
            newSupply := true
            while supply.size() > maxZones
                Zone old = supply.shift()
                old.bx.delete()

// ── Alerts ─────────────────────────────────────────────────────────────
alertcondition(newDemand, "New demand zone", "{{ticker}} {{interval}}: new demand zone")
alertcondition(newSupply, "New supply zone", "{{ticker}} {{interval}}: new supply zone")
alertcondition(demandTested, "Demand zone tested", "{{ticker}} {{interval}}: price returned to a demand zone at {{close}}")
alertcondition(supplyTested, "Supply zone tested", "{{ticker}} {{interval}}: price returned to a supply zone at {{close}}")
alertcondition(demandBroken, "Demand zone broken", "{{ticker}} {{interval}}: demand zone broken at {{close}}")
alertcondition(supplyBroken, "Supply zone broken", "{{ticker}} {{interval}}: supply zone broken at {{close}}")

Key inputs

Key inputs of Supply and Demand Zones
InputDefaultDescription
Departure candle: min body1.5 × ATRHow strong the move away from the base must be.
Base candle: max body0.5 × ATRHow small a candle must be to count as part of the base.
Max base candles3Longest base that is included in a zone.
Retire zone after this many tests3Zones are retired after repeated retests.
Max active zones per side10Oldest zones are deleted beyond this number.

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