Indicator · Pine Script v6 · On the chart

S/R Zones MTF [WavesUnchained]

by casoon

This indicator calculates and plots multi-timeframe support and resistance zones using ATR-based reversal structure swings.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code572
Settings33inputs
LicenceMIT
Last updateyesterday27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

572 lines of Pine Script v6 from indicators/market_structure/sr_zones_mtf_v2/sr_zones_mtf_v2.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

sr_zones_mtf_v2.pine
//@version=6
// ============================================================================
// S/R Zones MTF [WavesUnchained]
// Version: 3.2.1
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Multi-timeframe S/R zones built from ATR-reversal structure swings with scoring and merging.
//
// Features:
//   ✓ Up to two configurable higher timeframes plus chart timeframe
//   ✓ ATR-based reversal detection for zone construction
//   ✓ Zone merging — overlapping zones consolidated across timeframes
//   ✓ Zone scoring based on reversal strength and quality
//   ✓ Configurable display limit (up to 50 zones)
//   ✓ Alerts for zone breaks, retests and sweeps
//
// Data Contract:
//   Price:     REQUIRED   chart symbol
//   Volume:    OPTIONAL   real trade volume only — the break volume filter
//                         passes through when unavailable
//   OI:        NO
//   Reference: NO
//   Verdict:   CFD-degraded
//
// ============================================================================
indicator("S/R Zones MTF [WavesUnchained]", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500)

// ============================================================================
// MODULES:
// - inputs.pine
// - core.pine
// - visualization.pine
// ============================================================================

// NOTE: Modules are combined by the build system.

// ============================================================================
// INLINE MODULES (Combined at build time)
// ============================================================================

// ============================================================================
// MODULE: inputs
// ============================================================================

//──────────────────────────────────────────────────────────────────────────────
// Inputs
//──────────────────────────────────────────────────────────────────────────────
grpTF   = "Timeframes"
htf1    = input.timeframe("15", "HTF1 (next higher)", group=grpTF)
useHTF2 = input.bool(false, "Use HTF2", group=grpTF)
htf2    = input.timeframe("60", "HTF2", group=grpTF)
useChartSwings = input.bool(true, "Use Chart-TF Swings", group=grpTF)

grpZone = "Zones"
maxZones     = input.int(10, "Max Zones", minval=1, maxval=50, group=grpZone)
atrLen       = input.int(14, "ATR Length", minval=1, group=grpZone)

zoneAtrMultHTF = input.float(1.0, "Zone Width HTF = ATR(HTF)*", step=0.1, minval=0.1, group=grpZone)
zoneAtrMultCH  = input.float(0.8, "Zone Width Chart = ATR(Chart)*", step=0.1, minval=0.1, group=grpZone)

breakAtrMult = input.float(0.25, "Break Buffer = ATR(Chart)*", step=0.05, minval=0.0, group=grpZone)

grpMerge = "Zone Merging / Dedupe"
mergeOn      = input.bool(true, "Enable Zone Merging", group=grpMerge)
mergeAtrMult = input.float(0.6, "Merge Distance = ATR(Source)*", step=0.1, minval=0.1, group=grpMerge)
tooCloseAtr  = input.float(0.125, "Too-Close Filter = ATR(Source)*", step=0.025, minval=0.0, group=grpMerge)

grpStruct = "Structure (Swing Engine)"
revAtrMult = input.float(1.0, "Reversal Threshold (ATR*X)", step=0.1, minval=0.1, group=grpStruct)

grpBreak = "Break Rules"
invalidation = input.string("Close", "Invalidation", options=["Wick","Close"], group=grpBreak)
avoidFalseBreaks = input.bool(false, "Avoid False Breaks (Volume)", group=grpBreak, tooltip="Requires real trade volume. On feeds that report tick volume or none, the filter passes every break through rather than filtering on a meaningless value — the chart shows a note when that happens.")
breakVolMult     = input.float(1.2, "Break Volume Threshold = SMA(vol)*", step=0.1, minval=0.1, maxval=5.0, group=grpBreak)
breakFailBars    = input.int(5, "Break Failure Window (bars)", minval=1, group=grpBreak, tooltip="If price closes back on the old side of the broken zone edge within this many bars after a break, the break's score bonus is taken back. Provisional default — not yet validated on data.")

grpFilter = "Filter / Score"
useFilter   = input.bool(true, "Enable Filter", group=grpFilter)
minScore    = input.float(0.0, "Min Score (when filter on)", step=0.5, group=grpFilter)

wTouch      = input.float(1.0, "Weight: Touch", step=0.25, group=grpFilter)
wWick       = input.float(0.8, "Weight: Rejection Wick", step=0.25, group=grpFilter)
wCloseLoc   = input.float(0.7, "Weight: Close Location", step=0.25, group=grpFilter)
wImpulse    = input.float(0.6, "Weight: Impulse", step=0.25, group=grpFilter)
wAge        = input.float(0.001, "Weight: Age Penalty", step=0.0005, group=grpFilter)

grpShow = "Signals"
showTests   = input.bool(true, "Show Tests", group=grpShow)
showBreaks  = input.bool(true, "Show Breaks", group=grpShow)
showRetests = input.bool(true, "Show Retests", group=grpShow)
showSweeps  = input.bool(true, "Show Sweeps", group=grpShow)
retestMinBars = input.int(3, "Retest: min bars after break", minval=1, group=grpShow)
retestLabelCooldownBars = input.int(3, "Retest Label Cooldown (bars)", minval=0, group=grpShow)

grpInvalid = "Invalidated"
keepInvalidatedSnapshot = input.bool(false, "Keep invalidated snapshot (faded)", group=grpInvalid)
invalidFade = input.int(92, "Invalid snapshot transparency", minval=0, maxval=100, group=grpInvalid)

//──────────────────────────────────────────────────────────────────────────────
// Colors
//──────────────────────────────────────────────────────────────────────────────
cSup = color.new(color.lime, 82)
cRes = color.new(color.red,  82)
cMid = color.new(color.gray, 70)


// ============================================================================
// MODULE: core
// ============================================================================

//──────────────────────────────────────────────────────────────────────────────
// Helpers
//──────────────────────────────────────────────────────────────────────────────
f_lowestScoreIndex(_scores) =>
    int idx = 0
    float v = array.get(_scores, 0)
    if array.size(_scores) > 1
        for i = 1 to array.size(_scores) - 1
            float si = array.get(_scores, i)
            if si < v
                v := si
                idx := i
    idx

f_clamp01(x) =>
    math.max(0.0, math.min(1.0, x))

// find merge candidate: returns index or -1
f_findMergeIdx(_mid, _isRes, _dist, _tops, _bots, _resFlags) =>
    int best = -1
    float bestD = 10e10
    if array.size(_tops) > 0
        for i = 0 to array.size(_tops) - 1
            bool r = array.get(_resFlags, i)
            if r == _isRes
                float top = array.get(_tops, i)
                float bot = array.get(_bots, i)
                float mid = (top + bot) / 2.0
                float d = math.abs(_mid - mid)
                if d <= _dist and d < bestD
                    bestD := d
                    best := i
    best

f_isTooClose(_mid, _isRes, _dist, _tops, _bots, _resFlags) =>
    bool tooClose = false
    if array.size(_tops) > 0
        for i = 0 to array.size(_tops) - 1
            if array.get(_resFlags, i) == _isRes
                float mid2 = (array.get(_tops, i) + array.get(_bots, i)) / 2.0
                if math.abs(_mid - mid2) <= _dist
                    tooClose := true
                    break
    tooClose

f_applyVisuals(_i, _isRes, _bx, _ln) =>
    box b = array.get(_bx, _i)
    line l = array.get(_ln, _i)
    if not na(b)
        box.set_bgcolor(b, _isRes ? cRes : cSup)
    if not na(l)
        line.set_color(l, cMid)

f_updateZoneGeom(_i, _top, _bot, _bx, _ln, _tops, _bots) =>
    array.set(_tops, _i, _top)
    array.set(_bots, _i, _bot)
    box b = array.get(_bx, _i)
    line l = array.get(_ln, _i)
    if not na(b)
        box.set_top(b, _top)
        box.set_bottom(b, _bot)
    if not na(l)
        float mid = (_top + _bot) / 2.0
        line.set_y1(l, mid)
        line.set_y2(l, mid)

f_deleteZone(_i, _bx, _ln, _top, _bot, _isRes, _score, _touches, _lastEvtBar, _lastBreakDir, _lastBreakBar, _lastRetestBar, _breakBonus) =>
    box b = array.get(_bx, _i)
    line l = array.get(_ln, _i)
    if not na(b)
        box.delete(b)
    if not na(l)
        line.delete(l)
    array.remove(_bx, _i)
    array.remove(_ln, _i)
    array.remove(_top, _i)
    array.remove(_bot, _i)
    array.remove(_isRes, _i)
    array.remove(_score, _i)
    array.remove(_touches, _i)
    array.remove(_lastEvtBar, _i)
    array.remove(_lastBreakDir, _i)
    array.remove(_lastBreakBar, _i)
    array.remove(_lastRetestBar, _i)
    array.remove(_breakBonus, _i)

//──────────────────────────────────────────────────────────────────────────────
// Structure swing engine (ATR-reversal, no lookahead)
// Returns: newHighPrice, newLowPrice, atr
//──────────────────────────────────────────────────────────────────────────────
f_structSwings(_revAtrMult, _atrLen) =>
    float a = ta.atr(_atrLen)
    float thr = a * _revAtrMult

    var int dir = 0
    var float pivot = na
    var float extreme = na

    float newHigh = na
    float newLow  = na

    if barstate.isfirst
        pivot := close
        extreme := close
        dir := 0

    if dir == 0
        if close > pivot + thr
            dir := 1
            extreme := high
        else if close < pivot - thr
            dir := -1
            extreme := low

    if dir == 1
        extreme := math.max(extreme, high)
        if close < extreme - thr
            newHigh := extreme
            dir := -1
            extreme := low

    if dir == -1
        extreme := math.min(extreme, low)
        if close > extreme + thr
            newLow := extreme
            dir := 1
            extreme := high

    [newHigh, newLow, a]

//──────────────────────────────────────────────────────────────────────────────
// Swings from TFs
//──────────────────────────────────────────────────────────────────────────────
[swH_ch, swL_ch, atr_ch] = f_structSwings(revAtrMult, atrLen)

[swH_1, swL_1, atr_1] = request.security(syminfo.tickerid, htf1, f_structSwings(revAtrMult, atrLen), barmerge.gaps_off, barmerge.lookahead_off)
// A ternary cannot return a tuple (CE10163): request unconditionally, mask when HTF2 is off.
[swH_2raw, swL_2raw, atr_2raw] = request.security(syminfo.tickerid, htf2, f_structSwings(revAtrMult, atrLen), barmerge.gaps_off, barmerge.lookahead_off)
float swH_2 = useHTF2 ? swH_2raw : na
float swL_2 = useHTF2 ? swL_2raw : na
float atr_2 = useHTF2 ? atr_2raw : na

// HTF ATRs come from f_structSwings (atr_1 / atr_2) — no extra request.security calls needed
zoneW1 = atr_1 * zoneAtrMultHTF
zoneW2 = useHTF2 ? atr_2 * zoneAtrMultHTF : na
zoneWC = atr_ch * zoneAtrMultCH

breakBuf = atr_ch * breakAtrMult

// volume filter (flux-style)
// Only real trade volume can filter a break. Tick-volume feeds count price
// updates, so `volume >= avgVol * mult` would filter on noise; pass through
// instead of silently suppressing breaks. See DATA_VALIDITY.md.
bool volumeIsReal =
     (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and
     not na(volume)
avgVol = ta.sma(volume, atrLen)
volOK  = not avoidFalseBreaks or not volumeIsReal or (volume >= avgVol * breakVolMult)

//──────────────────────────────────────────────────────────────────────────────
// Storage (parallel arrays)
//──────────────────────────────────────────────────────────────────────────────
var box[]  aBx   = array.new_box()
var line[] aLn   = array.new_line()
var float[] aTop = array.new_float()
var float[] aBot = array.new_float()
var bool[]  aRes = array.new_bool()
var float[] aScr = array.new_float()
var int[]   aTch = array.new_int()
var int[]   aEvt = array.new_int()
var int[]   aBrk = array.new_int()   // -1 bear break, 0 none, +1 bull break
var int[]   aBrkBar = array.new_int()
var int[]   aLastRetestBar = array.new_int()
var float[] aBrkBonus = array.new_float()   // break bonus still at risk of a failed break (0 = none)

//──────────────────────────────────────────────────────────────────────────────
// Add/Merge Zone
// _srcAtr is ATR of the source TF (for merge distance normalization)
// _tfBoost: score boost per TF (e.g. Chart 0.5, HTF1 1.0, HTF2 2.0)
//──────────────────────────────────────────────────────────────────────────────
f_addOrMergeZone(_isRes, _price, _width, _srcAtr, _tfBoost) =>
    float top = _isRes ? _price : _price + _width
    float bot = _isRes ? _price - _width : _price
    float mid = (top + bot) / 2.0

    float tooCloseDist = _srcAtr * tooCloseAtr
    bool tooClose = (tooCloseAtr > 0) and f_isTooClose(mid, _isRes, tooCloseDist, aTop, aBot, aRes)

    if not tooClose
        float mergeDist = _srcAtr * mergeAtrMult
        int mergeIdx = (mergeOn and array.size(aTop) > 0) ? f_findMergeIdx(mid, _isRes, mergeDist, aTop, aBot, aRes) : -1

        if mergeIdx != -1
            // Merge: expand existing zone bounds
            float et = array.get(aTop, mergeIdx)
            float eb = array.get(aBot, mergeIdx)
            float nt = math.max(et, top)
            float nb = math.min(eb, bot)
            f_updateZoneGeom(mergeIdx, nt, nb, aBx, aLn, aTop, aBot)

            // reward merge slightly (means: another structure confirmed near same area)
            float sc = array.get(aScr, mergeIdx) + 0.5 + _tfBoost * 0.25
            array.set(aScr, mergeIdx, sc)
        else
            // Create visuals
            color zc  = _isRes ? cRes : cSup
            box b = box.new(left=bar_index, top=top, right=bar_index + 1, bottom=bot, bgcolor=zc, border_color=color.new(color.black, 100))
            line l = line.new(x1=bar_index, y1=mid, x2=bar_index + 1, y2=mid, color=cMid, width=1)

            array.unshift(aBx, b)
            array.unshift(aLn, l)
            array.unshift(aTop, top)
            array.unshift(aBot, bot)
            array.unshift(aRes, _isRes)
            array.unshift(aScr, 0.0 + _tfBoost)
            array.unshift(aTch, 0)
            array.unshift(aEvt, bar_index)
            array.unshift(aBrk, 0)
            array.unshift(aBrkBar, na)
            array.unshift(aLastRetestBar, na)
            array.unshift(aBrkBonus, 0.0)

        // Outside the if/else: both branches must end in the same type (CE10235),
        // and f_deleteZone ends in array.remove (float). A merge never grows the
        // list, so this only acts after a new zone was added.
        if array.size(aTop) > maxZones
            int remIdx = useFilter ? f_lowestScoreIndex(aScr) : (array.size(aTop) - 1)
            f_deleteZone(remIdx, aBx, aLn, aTop, aBot, aRes, aScr, aTch, aEvt, aBrk, aBrkBar, aLastRetestBar, aBrkBonus)

//──────────────────────────────────────────────────────────────────────────────
// Create zones from swing events (Chart + HTF1 + optional HTF2)
//──────────────────────────────────────────────────────────────────────────────
if useChartSwings
    if not na(swH_ch)
        f_addOrMergeZone(true,  swH_ch, zoneWC, atr_ch, 0.5)
    if not na(swL_ch)
        f_addOrMergeZone(false, swL_ch, zoneWC, atr_ch, 0.5)

if not na(swH_1)
    f_addOrMergeZone(true,  swH_1, zoneW1, atr_1, 1.0)
if not na(swL_1)
    f_addOrMergeZone(false, swL_1, zoneW1, atr_1, 1.0)

if useHTF2
    if not na(swH_2)
        f_addOrMergeZone(true,  swH_2, zoneW2, atr_2, 2.0)
    if not na(swL_2)
        f_addOrMergeZone(false, swL_2, zoneW2, atr_2, 2.0)

//──────────────────────────────────────────────────────────────────────────────
// Events + scoring
//──────────────────────────────────────────────────────────────────────────────
int n = array.size(aTop)
float atrNow = atr_ch

// Event flags for alerts (any zone)
bool evtBreakUp = false
bool evtBreakDn = false
bool evtRetest  = false
bool evtSweep   = false

float invHigh = (invalidation == "Close" ? close : high)
float invLow  = (invalidation == "Close" ? close : low)

if n > 0
    for i = 0 to n - 1
        float top = array.get(aTop, i)
        float bot = array.get(aBot, i)
        bool  isR = array.get(aRes, i)

        // extend visuals
        box b = array.get(aBx, i)
        line l = array.get(aLn, i)
        if not na(b)
            box.set_right(b, bar_index + 1)
        if not na(l)
            line.set_x2(l, bar_index + 1)

        float sc = array.get(aScr, i)
        sc -= wAge

        float h = high, lo = low, o = open, c = close
        float rng = math.max(1e-6, (top - bot))

        // zone interaction
        bool inZone = (lo <= top and h >= bot)

        // Sweep (stoprun) = wick beyond boundary, close back inside
        bool sweepR = isR and h > top and c < top and c > bot
        bool sweepS = (not isR) and lo < bot and c > bot and c < top

        // Test = enters zone, close still inside (soft touch) — a sweep bar is scored as sweep only
        bool testR = isR and inZone and c < top and c > bot and not sweepR
        bool testS = (not isR) and inZone and c > bot and c < top and not sweepS

        // buffered breaks, invalidation-aware + volume filter
        bool breakUp = (invHigh > (top + breakBuf)) and volOK
        bool breakDn = (invLow  < (bot - breakBuf)) and volOK

        // Quality metrics (fundierter)
        // close location: for resistance, better if close near bottom; for support, close near top
        float closeloc = f_clamp01((c - bot) / rng)  // 0..1
        float qClose   = isR ? (1.0 - closeloc) : closeloc

        // rejection wick quality (ATR-normalized)
        float upperW = (h - math.max(o, c)) / math.max(1e-6, atrNow)
        float lowerW = (math.min(o, c) - lo) / math.max(1e-6, atrNow)
        float qWick  = isR ? upperW : lowerW

        // impulse after interaction: use bar body in expected direction + range expansion
        // support: bullish body is good, resistance: bearish body is good
        float body = (c - o) / math.max(1e-6, atrNow) // +bull
        float qImp = isR ? (-body) : (body)

        // Touch/Test scoring
        if (testR or testS)
            int tch = array.get(aTch, i) + 1
            array.set(aTch, i, tch)
            sc += wTouch
            sc += wCloseLoc * qClose
            sc += wWick * qWick
            sc += wImpulse * qImp
            array.set(aEvt, i, bar_index)

            if showTests
                label.new(bar_index, isR ? h : lo, "T", style=isR ? label.style_label_down : label.style_label_up,
                  textcolor=color.white, color=color.new(color.black, 0), size=size.tiny)

        // Sweep scoring (showSweeps only gates the label, not the scoring)
        if sweepR or sweepS
            sc += (wTouch * 0.7)
            sc += (wWick * qWick)
            array.set(aEvt, i, bar_index)
            evtSweep := true
            if showSweeps
                label.new(bar_index, isR ? h : lo, "SW", style=isR ? label.style_label_down : label.style_label_up,
                  textcolor=color.white, color=color.new(color.black, 0), size=size.tiny)

        // Failed break: if price closes back on the old side of the broken edge within
        // the failure window, the break bonus is taken back.
        float pendBonus = array.get(aBrkBonus, i)
        int pendBar = array.get(aBrkBar, i)
        if pendBonus != 0.0 and not na(pendBar)
            int sinceBrk = bar_index - pendBar
            int pendDir = array.get(aBrk, i)
            bool brkFailed = sinceBrk > 0 and ((pendDir == 1 and c < top) or (pendDir == -1 and c > bot))
            if brkFailed
                sc -= pendBonus
                array.set(aBrkBonus, i, 0.0)
            else if sinceBrk >= breakFailBars
                array.set(aBrkBonus, i, 0.0)

        // Role-Reversal on Break:
        // If resistance breaks up => becomes support
        // If support breaks down => becomes resistance
        // Break state and role reversal are always tracked; showBreaks only gates the labels
        if isR and breakUp
            if keepInvalidatedSnapshot
                // snapshot as faded resistance, frozen to this bar
                box bs = box.new(left=box.get_left(b), top=top, right=bar_index, bottom=bot,
                  bgcolor=color.new(cRes, invalidFade), border_color=color.new(color.black, 100))
                line ls = line.new(x1=line.get_x1(l), y1=(top + bot) / 2, x2=bar_index, y2=(top + bot) / 2,
                  color=color.new(cMid, invalidFade), width=1)
                bs := bs
                ls := ls

            array.set(aRes, i, false)
            array.set(aBrk, i, +1)
            array.set(aBrkBar, i, bar_index)
            f_applyVisuals(i, false, aBx, aLn)
            float brkBonusUp = (wImpulse * 1.5) + (wCloseLoc * 0.5)
            sc += brkBonusUp
            array.set(aBrkBonus, i, brkBonusUp)
            evtBreakUp := true
            if showBreaks
                label.new(bar_index, h, "B↑", style=label.style_label_up, textcolor=color.white, color=color.new(color.black, 0), size=size.tiny)

        if (not isR) and breakDn
            if keepInvalidatedSnapshot
                box bs = box.new(left=box.get_left(b), top=top, right=bar_index, bottom=bot,
                  bgcolor=color.new(cSup, invalidFade), border_color=color.new(color.black, 100))
                line ls = line.new(x1=line.get_x1(l), y1=(top + bot) / 2, x2=bar_index, y2=(top + bot) / 2,
                  color=color.new(cMid, invalidFade), width=1)
                bs := bs
                ls := ls

            array.set(aRes, i, true)
            array.set(aBrk, i, -1)
            array.set(aBrkBar, i, bar_index)
            f_applyVisuals(i, true, aBx, aLn)
            float brkBonusDn = (wImpulse * 1.5) + (wCloseLoc * 0.5)
            sc += brkBonusDn
            array.set(aBrkBonus, i, brkBonusDn)
            evtBreakDn := true
            if showBreaks
                label.new(bar_index, lo, "B↓", style=label.style_label_down, textcolor=color.white, color=color.new(color.black, 0), size=size.tiny)

        // Retest (sharpened): re-enter zone AFTER break + reject back out, with min bars delay
        int brkDir = array.get(aBrk, i)
        int brkBar = array.get(aBrkBar, i)
        int lastR  = array.get(aLastRetestBar, i)

        bool canRetest = (not na(brkBar)) and (bar_index - brkBar >= retestMinBars)
        bool cooldownOK = na(lastR) or (bar_index - lastR >= retestLabelCooldownBars)

        // After bullish break (R->S): retest should dip into zone and close back ABOVE top (reject upward)
        bool retestBull = canRetest and cooldownOK and (brkDir == +1) and inZone and (lo <= top) and (c > top) and (c > o)

        // After bearish break (S->R): retest should poke into zone and close back BELOW bot (reject downward)
        bool retestBear = canRetest and cooldownOK and (brkDir == -1) and inZone and (h >= bot) and (c < bot) and (c < o)

        if retestBull or retestBear
            sc += (wTouch * 1.2)
            sc += (wWick * qWick)
            sc += (wImpulse * math.abs(qImp))
            array.set(aEvt, i, bar_index)
            array.set(aLastRetestBar, i, bar_index)
            evtRetest := true
            if showRetests
                label.new(bar_index, retestBull ? lo : h, "R", style=retestBull ? label.style_label_up : label.style_label_down,
                  textcolor=color.white, color=color.new(color.black, 0), size=size.tiny)

        array.set(aScr, i, sc)

//──────────────────────────────────────────────────────────────────────────────
// Alerts
//──────────────────────────────────────────────────────────────────────────────
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed

alertcondition(alertOk and (evtBreakUp), "Zone Break Up",   "SRZ · ZONE BREAK UP · {{ticker}} {{interval}}")
alertcondition(alertOk and (evtBreakDn), "Zone Break Down", "SRZ · ZONE BREAK DOWN · {{ticker}} {{interval}}")
alertcondition(alertOk and (evtRetest),  "Zone Retest",     "SRZ · ZONE RETEST · {{ticker}} {{interval}}")
alertcondition(alertOk and (evtSweep),   "Zone Sweep",      "SRZ · ZONE SWEEP · {{ticker}} {{interval}}")


// ============================================================================
// MODULE: visualization
// ============================================================================

//──────────────────────────────────────────────────────────────────────────────
// Filter visibility: fade zones below minScore
//──────────────────────────────────────────────────────────────────────────────
if useFilter and array.size(aTop) > 0
    for i = 0 to array.size(aTop) - 1
        float sc = array.get(aScr, i)
        bool isR = array.get(aRes, i)
        box b = array.get(aBx, i)
        if not na(b)
            color zc = isR ? cRes : cSup
            box.set_bgcolor(b, sc >= minScore ? zc : color.new(zc, 100))

// ============================================================================
// DATA VALIDITY NOTICE
// ============================================================================
// Only when the filter is switched on but cannot work — an enabled filter that
// silently does nothing is worse than no filter.
var table volWarn = table.new(position.bottom_right, 1, 1)
if barstate.islast and avoidFalseBreaks and not volumeIsReal
    table.cell(volWarn, 0, 0, "S/R Zones MTF · volume not available",
         bgcolor = color.new(color.orange, 20), text_color = color.white, text_size = size.small)

Overview

S/R Zones MTF [WavesUnchained] builds zones across configurable higher timeframes and the current chart timeframe, applying scoring based on touch quality, rejection wicks, impulse, and age. It features zone merging and deduplication, along with options to display tests, breaks, retests, and sweeps. It draws directly on the price chart.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Support & resistance

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
HTF1 (next higher)timeframe15htf1
Use HTF2boolfalseuseHTF2
HTF2timeframe60htf2
Use Chart-TF SwingsbooltrueuseChartSwings
Max Zonesint10maxZones
ATR Lengthint14atrLen
Zone Width HTF = ATR(HTF)*float1.0zoneAtrMultHTF
Zone Width Chart = ATR(Chart)*float0.8zoneAtrMultCH
Break Buffer = ATR(Chart)*float0.25breakAtrMult
Enable Zone MergingbooltruemergeOn
Merge Distance = ATR(Source)*float0.6mergeAtrMult
Too-Close Filter = ATR(Source)*float0.125tooCloseAtr
Show all 33 settings
SettingTypeDefaultVariable
Reversal Threshold (ATR*X)float1.0revAtrMult
InvalidationstringCloseinvalidation
Avoid False Breaks (Volume)boolfalseavoidFalseBreaks
Break Volume Threshold = SMA(vol)*float1.2breakVolMult
Break Failure Window (bars)int5breakFailBars
Enable FilterbooltrueuseFilter
Min Score (when filter on)float0.0minScore
Weight: Touchfloat1.0wTouch
Weight: Rejection Wickfloat0.8wWick
Weight: Close Locationfloat0.7wCloseLoc
Weight: Impulsefloat0.6wImpulse
Weight: Age Penaltyfloat0.001wAge
Show TestsbooltrueshowTests
Show BreaksbooltrueshowBreaks
Show RetestsbooltrueshowRetests
Show SweepsbooltrueshowSweeps
Retest: min bars after breakint3retestMinBars
Retest Label Cooldown (bars)int3retestLabelCooldownBars
Keep invalidated snapshot (faded)boolfalsekeepInvalidatedSnapshot
Invalid snapshot transparencyint92invalidFade
Alerts only on bar close (confirmed)booltruealertsOnBarClose
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

948 stars

Moving Average Channel plots separate simple moving averages for upper and lower bands on the price chart. It fills the space between bands with semi-transparent color and colors bars green above the upper band, red below the lower band, or gray in between. Us

Indicator v4 Moving averagesSupport & resistance
Updated 10 days ago · GPL-3.0 GitHub repository of Moving Average Channel

ICT Equal Highs and Lows

by ArunKBhaskar
575 stars

This overlay indicator finds pivot highs and lows on the price chart and compares the last two highs and the last two lows. When the percentage difference between a pair is below the Equal HL threshold, it treats them as equal levels and draws a box. Left and

Indicator v5 SMC / ICT / price actionSupport & resistance
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Equal Highs and Lows
330 stars

This indicator monitors multiple cryptocurrency symbols for trading signals. It calculates and plots entry prices, take profit, and stop loss levels for each symbol including BTCUSDT, ETHUSDT, BNBUSDT and others. Price crossovers trigger buy, sell or close ale

Indicator v5 Support & resistanceScreeners & dashboards
Updated 2 months ago · None stated GitHub repository of Multi Alert Long/Short [Alorse]

This overlay indicator draws a fixed-range volume profile on the price chart, splitting traded activity into up and down volume at each price level. It also plots a sentiment profile, a developing point of control, and value-area high and low around a configur

Indicator v5 VolumeSupport & resistance
Updated 2 months ago · MIT GitHub repository of Volume Profile and Indicator by DGT

BZ-tracker

by Allen1212
303 stars

The BZ-tracker indicator marks ARK Invest buy and sell transactions on stock charts with labels. It plots green labels for purchases and red labels for sales, each annotated with a multiplier such as X1 or X2 to show the number of ARK ETFs trading the stock on

Indicator v4 Support & resistanceScreeners & dashboards
Updated 2 years ago · None stated GitHub repository of BZ-tracker

Function - Linear Regression

by ricardosantos79
261 stars

This indicator fits a linear regression through stored pivot highs and lows and draws the resulting channel on the price chart. A function returns predicted values, maximum and minimum deviations, and the average absolute deviation. Confirmed two-bar pivots up

Indicator v5 TrendSupport & resistance
Updated 4 years ago · None stated GitHub repository of Function - Linear Regression

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers