//@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.
Multi-timeframe support and resistance zones
ATR-based reversal detection and zone construction
Zone merging and consolidation across timeframes
Quality scoring based on touches, wicks, and impulse
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
HTF1 (next higher)
timeframe
15
htf1
Use HTF2
bool
false
useHTF2
HTF2
timeframe
60
htf2
Use Chart-TF Swings
bool
true
useChartSwings
Max Zones
int
10
maxZones
ATR Length
int
14
atrLen
Zone Width HTF = ATR(HTF)*
float
1.0
zoneAtrMultHTF
Zone Width Chart = ATR(Chart)*
float
0.8
zoneAtrMultCH
Break Buffer = ATR(Chart)*
float
0.25
breakAtrMult
Enable Zone Merging
bool
true
mergeOn
Merge Distance = ATR(Source)*
float
0.6
mergeAtrMult
Too-Close Filter = ATR(Source)*
float
0.125
tooCloseAtr
Show all 33 settings
Setting
Type
Default
Variable
Reversal Threshold (ATR*X)
float
1.0
revAtrMult
Invalidation
string
Close
invalidation
Avoid False Breaks (Volume)
bool
false
avoidFalseBreaks
Break Volume Threshold = SMA(vol)*
float
1.2
breakVolMult
Break Failure Window (bars)
int
5
breakFailBars
Enable Filter
bool
true
useFilter
Min Score (when filter on)
float
0.0
minScore
Weight: Touch
float
1.0
wTouch
Weight: Rejection Wick
float
0.8
wWick
Weight: Close Location
float
0.7
wCloseLoc
Weight: Impulse
float
0.6
wImpulse
Weight: Age Penalty
float
0.001
wAge
Show Tests
bool
true
showTests
Show Breaks
bool
true
showBreaks
Show Retests
bool
true
showRetests
Show Sweeps
bool
true
showSweeps
Retest: min bars after break
int
3
retestMinBars
Retest Label Cooldown (bars)
int
3
retestLabelCooldownBars
Keep invalidated snapshot (faded)
bool
false
keepInvalidatedSnapshot
Invalid snapshot transparency
int
92
invalidFade
Alerts only on bar close (confirmed)
bool
true
alertsOnBarClose
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.
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
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
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
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
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
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