Indicator · Pine Script v6 · Own pane

Structure Break Risk

by casoon

This indicator quantifies the risk of a prevailing market structure break by combining trend context, structure, and momentum into a composite 0–100 risk score.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code489
Settings24inputs
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

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

structure_break_risk.pine
//@version=6
// ============================================================================
// Structure Break Risk [WavesUnchained]
// Version: 2.3.1
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// Indicator that answers: how close is the PREVAILING trend's structure to
// breaking — and against WHICH level? Symmetric by design — in an uptrend it
// reads top-break risk, in a downtrend bottom-break risk; the asymmetry comes
// from the situation (which trend is active), never from a hardcoded long/short.
//
// Two coordinated panes make the read intuitive:
//   ✓ RSI pane (this pane) — the RSI with momentum-divergence lines drawn directly
//     on it: when price makes a higher high while RSI makes a LOWER high (uptrend),
//     or price a lower low while RSI a higher low (downtrend), a line marks the
//     failing momentum. Read against the price above — sharing the same time axis —
//     the divergence is unmistakable: the earliest structural crack. A divergence
//     is price-vs-momentum and is only readable where BOTH are visible
//   ✓ Price chart (drawn via force_overlay) — the decisive break-level line the
//     trend must hold, a risk-shaded cushion zone between price and that level, a
//     thin EMA trend-context line, word event labels (Watch / Break Pressure /
//     Structure Broken / Pressure Faded) and a compact info label carrying score,
//     state, direction, break level, distance and a mini-bar per sensor
//
// Risk magnitude is the same 0-100 composite as before — colour encodes the RISK
// band (neutral → orange → red → dark red), never the trade direction, so it
// never reads as buy/sell. Direction is stated in words.
//
// Four pre-break evidence sensors across the roles (weighted, never an
// AND-chain) plus BOS as a separate confirmed structure event:
//   ✓ Near Break Level (Location→Trigger) — distance-to-BOS in ATR
//   ✓ Failed Breakout / SFP (Trigger+Location) — swing sweep that closes back
//   ✓ Structure Erosion (Structure) — lower high (up) / higher low (down)
//   ✓ Pivot Divergence (Momentum) — price pivot extends, RSI at the pivot does not
//   ✓ Confirmed BOS (Event) — close beyond the opposite swing level
//
// Pivots provide swing LEVELS only (Location reference); the break event is the
// Trigger — pivots never act as the signal themselves.
//
// ============================================================================

indicator("Structure Break Risk [WavesUnchained]", "SBR", overlay=false, max_labels_count=500, max_lines_count=100)

// ============================================================================
// Inputs
// ============================================================================

biasLen   = input.int(50, "Trend Bias Length", minval=10, group="Trend Context", tooltip="EMA whose level + slope defines the prevailing trend; direction latches and holds through transitions so risk can build before the trend flips")
pivLen    = input.int(5, "Swing Pivot Length", minval=2, group="Structure", tooltip="Left/right bars for swing high/low detection — these are LEVEL references, not signals")
nearAtr   = input.float(1.5, "Near Break Level ATR Distance", minval=0.2, group="Structure", tooltip="Distance to the break level (in ATR) at which Near-Level risk reaches full")
rsiLen    = input.int(14, "RSI Length", minval=5, group="Momentum")
holdBars  = input.int(10, "Evidence Memory Bars", minval=1, group="Core", tooltip="Transient evidence decays over this many bars so old structure pressure fades instead of sticking as a hard hold")
smoothLen = input.int(3, "Risk Smoothing", minval=1, group="Core")

wNear   = input.float(25.0, "Weight: Near Break Level", minval=0, group="Sensor Weights")
wSfp    = input.float(20.0, "Weight: Failed Breakout", minval=0, group="Sensor Weights")
wStruct = input.float(15.0, "Weight: Structure Erosion", minval=0, group="Sensor Weights")
wDiv    = input.float(10.0, "Weight: Pivot Divergence", minval=0, group="Sensor Weights")

lvlElev = input.int(35, "State: Watch ≥", group="State Levels")
lvlHigh = input.int(60, "State: Break Pressure ≥", group="State Levels")
lvlCrit = input.int(80, "State: Critical ≥", group="State Levels")

showDiv     = input.bool(true, "Show Divergence Lines", group="RSI Pane", tooltip="Draw momentum-divergence lines on the RSI: price extends but RSI does not (uptrend: price higher high / RSI lower high — downtrend: price lower low / RSI higher low). The earliest structural crack, shown where it is actually readable — against the price above.")
showEma     = input.bool(true, "Show Trend Context Line", group="Chart", tooltip="Thin EMA line on the price chart whose level + slope defines the prevailing trend that is at risk")
showLevel   = input.bool(true, "Show Break-Level Line", group="Chart", tooltip="The decisive swing the prevailing trend must hold, drawn on the price chart; colour = current risk band")
showZone    = input.bool(true, "Show Risk Zone", group="Chart", tooltip="Shades the cushion between price and the break level on the price chart; the fill intensifies as risk rises")
showMarkers = input.bool(true, "Show Event Labels", group="Chart", tooltip="Compact structure-risk events: Watch, Break Pressure, Structure Broken, Pressure Faded. Context events, not trade signals.")
markerCooldown = input.int(10, "Event Label Cooldown", minval=0, group="Chart", tooltip="Minimum bars between visible non-BOS event labels")
requireContextFlip = input.bool(true, "Require Context Flip for Break", group="Chart", tooltip="Only calls Structure Broken when the swing break also reclaims the EMA context. A swing break without the flip stays Break Pressure; if the EMA context flips later while price is still beyond that unconsumed swing level, Structure Broken fires on the flip bar.")

showLabel   = input.bool(true, "Show Info Label", group="Info Label", tooltip="Compact, mobile-friendly text label (not a table) carrying the full read — score, state, direction, break level, distance, reason and a mini-bar per sensor")
labelOffset = input.int(5, "Label Offset (bars right)", minval=0, group="Info Label", tooltip="How far to the right of the last bar the label floats, so it never sits on the candles")
labelSize   = input.string("Normal", "Label Text Size", options=["Small", "Normal", "Large"], group="Info Label")

// ============================================================================
// Helpers
// ============================================================================

f_clamp(x, lo, hi) =>
    math.max(lo, math.min(hi, x))

f_norm(x, lo, hi) =>
    hi == lo ? 0.0 : f_clamp((x - lo) / (hi - lo), 0.0, 1.0)

// Risk-band colours — encode the RISK level, never the trade direction.
color cNeutral = #64748b
color cOrange  = #f59e0b
color cRed     = #dc2626
color cDarkRed = #991b1b
color cCalm    = #16a34a

f_riskCol(r) =>
    r >= lvlCrit ? cDarkRed : r >= lvlHigh ? cRed : r >= lvlElev ? cOrange : cNeutral

// Five-cell mini-bar from a 0..100 value (robust, no str.repeat edge cases).
f_bar(v) =>
    int n = math.round(f_clamp(v, 0.0, 100.0) / 20.0)
    string s = ""
    for i = 0 to 4
        s := s + (i < n ? "█" : "░")
    s

// Right-pad a 0..100 value to 3 chars so the number column lines up in the label.
f_pad(v) =>
    int n = math.round(f_clamp(v, 0.0, 100.0))
    (n < 10 ? "  " : n < 100 ? " " : "") + str.tostring(n, "#")

// One sensor row: bar FIRST (uniform glyph width → bars align in a proportional
// font), then the padded value, then the name.
f_row(name, v) =>
    f_bar(v) + " " + f_pad(v) + "  " + name

// ============================================================================
// Stable trend context — latches the last clear direction through transitions
// ============================================================================

ema         = ta.ema(close, biasLen)
emaSlope    = ema - ema[5]
rawTrendDir = close > ema and emaSlope > 0 ? 1 : close < ema and emaSlope < 0 ? -1 : 0
var int trendDir = 0
if rawTrendDir != 0
    trendDir := rawTrendDir

// ============================================================================
// Swing levels (Location reference only) + ATR scale
// ============================================================================

atr = math.max(ta.atr(14), syminfo.mintick)

ph = ta.pivothigh(high, pivLen, pivLen)
pl = ta.pivotlow(low, pivLen, pivLen)

var float lastPH = na
var float prevPH = na
var float lastPL = na
var float prevPL = na
if not na(ph)
    prevPH := lastPH
    lastPH := ph
if not na(pl)
    prevPL := lastPL
    lastPL := pl

// ============================================================================
// Pivot-based RSI divergence — RSI sampled at the price-pivot bar
// ============================================================================

rsi = ta.rsi(close, rsiLen)

var float lastPricePH = na
var float prevPricePH = na
var float lastRsiPH   = na
var float prevRsiPH   = na
var float lastPricePL = na
var float prevPricePL = na
var float lastRsiPL   = na
var float prevRsiPL   = na
// Bar index of each pivot (the swing occurred pivLen bars before confirmation) —
// the endpoints the divergence line connects.
var int lastPHbar = na
var int prevPHbar = na
var int lastPLbar = na
var int prevPLbar = na
if not na(ph)
    prevPricePH := lastPricePH
    lastPricePH := ph
    prevRsiPH   := lastRsiPH
    lastRsiPH   := rsi[pivLen]
    prevPHbar   := lastPHbar
    lastPHbar   := bar_index - pivLen
if not na(pl)
    prevPricePL := lastPricePL
    lastPricePL := pl
    prevRsiPL   := lastRsiPL
    lastRsiPL   := rsi[pivLen]
    prevPLbar   := lastPLbar
    lastPLbar   := bar_index - pivLen

// ============================================================================
// Evidence sensors — each 0..1, selected symmetrically by the prevailing trend
// ============================================================================

float sNear   = 0.0   // distance to the decisive break level (rises on approach)
float sBos    = 0.0   // confirmed break of structure
float sSfp    = 0.0   // failed breakout / stop-run that closes back
float sStruct = 0.0   // structure erosion (lower high / higher low)
float sDiv    = 0.0   // pivot RSI divergence
float distToBreak = na
float breakLevel  = na
bool approachingBreak = false

if trendDir == 1
    // --- top-break risk (uptrend losing its structure) ---
    breakLevel := lastPL
    if not na(lastPL)
        distToBreak := (close - lastPL) / atr
        approachingBreak := not na(distToBreak[1]) and distToBreak < distToBreak[1]
        sNear := distToBreak >= 0.0 and approachingBreak ? 1.0 - f_norm(distToBreak, 0.0, nearAtr) : 0.0
        if close < lastPL
            sBos := math.max(0.65, f_norm((lastPL - close) / atr, 0.0, 1.5))
    if not na(lastPH) and high > lastPH and close < lastPH
        sSfp := 1.0
    // Structure / divergence fire on the pivot-confirmation bar only, then decay.
    if not na(ph) and not na(lastPH) and not na(prevPH) and lastPH < prevPH
        sStruct := f_norm((prevPH - lastPH) / atr, 0.0, 2.0)
    if not na(ph) and not na(lastPricePH) and not na(prevPricePH) and not na(lastRsiPH) and not na(prevRsiPH)
        if lastPricePH > prevPricePH and lastRsiPH < prevRsiPH
            sDiv := 1.0
else if trendDir == -1
    // --- bottom-break risk (downtrend losing its structure) ---
    breakLevel := lastPH
    if not na(lastPH)
        distToBreak := (lastPH - close) / atr
        approachingBreak := not na(distToBreak[1]) and distToBreak < distToBreak[1]
        sNear := distToBreak >= 0.0 and approachingBreak ? 1.0 - f_norm(distToBreak, 0.0, nearAtr) : 0.0
        if close > lastPH
            sBos := math.max(0.65, f_norm((close - lastPH) / atr, 0.0, 1.5))
    if not na(lastPL) and low < lastPL and close > lastPL
        sSfp := 1.0
    // Structure / divergence fire on the pivot-confirmation bar only, then decay.
    if not na(pl) and not na(lastPL) and not na(prevPL) and lastPL > prevPL
        sStruct := f_norm((lastPL - prevPL) / atr, 0.0, 2.0)
    if not na(pl) and not na(lastPricePL) and not na(prevPricePL) and not na(lastRsiPL) and not na(prevRsiPL)
        if lastPricePL < prevPricePL and lastRsiPL > prevRsiPL
            sDiv := 1.0

// Transient evidence decays so old pressure fades instead of sticking as a
// rectangular hold window.
decayFactor = holdBars <= 1 ? 0.0 : 1.0 - 1.0 / holdBars
var float sNearH = 0.0
var float sBosH = 0.0
var float sSfpH = 0.0
var float sStructH = 0.0
var float sDivH = 0.0
sNearH := math.max(sNear, nz(sNearH[1]) * decayFactor)
sBosH := math.max(sBos, nz(sBosH[1]) * decayFactor)
sSfpH := math.max(sSfp, nz(sSfpH[1]) * decayFactor)
sStructH := math.max(sStruct, nz(sStructH[1]) * decayFactor)
sDivH := math.max(sDiv, nz(sDivH[1]) * decayFactor)

// Confirmed BOS this bar (real structural event). Swing breaks are only called
// a confirmed break when the broader EMA context is also reclaimed/flipped;
// otherwise they stay break-pressure events so local counter-swing breaks do not
// read as trend breaks. The break is judged against the trend that was in force
// BEFORE this bar — the latch above already flips on the breaking bar itself.
bosTrendDir = nz(trendDir[1])
rawBosUpBreak = bosTrendDir == 1 and not na(lastPL) and close < lastPL and close[1] >= lastPL
rawBosDnBreak = bosTrendDir == -1 and not na(lastPH) and close > lastPH and close[1] <= lastPH
bosContextOk = not requireContextFlip or (rawBosUpBreak and close < ema and emaSlope < 0) or (rawBosDnBreak and close > ema and emaSlope > 0)
// A swing break that stayed Break Pressure becomes a confirmed break on the bar the
// EMA context flips, as long as price still holds beyond that swing level and the
// level was not already consumed by an earlier confirmed break.
var float consumedPL = na
var float consumedPH = na
flipUpBreak = requireContextFlip and bosTrendDir == 1 and trendDir == -1 and not rawBosUpBreak and not na(lastPL) and close < lastPL and (na(consumedPL) or lastPL != consumedPL)
flipDnBreak = requireContextFlip and bosTrendDir == -1 and trendDir == 1 and not rawBosDnBreak and not na(lastPH) and close > lastPH and (na(consumedPH) or lastPH != consumedPH)
bosUpNow = (rawBosUpBreak and bosContextOk) or flipUpBreak
bosDnNow = (rawBosDnBreak and bosContextOk) or flipDnBreak
bosNow = bosUpNow or bosDnNow
bosPressureNow = (rawBosUpBreak or rawBosDnBreak) and not bosContextOk
if bosUpNow
    consumedPL := lastPL
if bosDnNow
    consumedPH := lastPH

// ============================================================================
// Composite risk
// ============================================================================

weightSum = wNear + wSfp + wStruct + wDiv
// Structure Erosion and Pivot Divergence exclude each other at the same pivot pair
// (erosion needs a lower high, divergence a higher high), so the reachable maximum
// is the weight sum minus the smaller of the two — scaling by it makes 100 reachable.
reachableMax = weightSum - math.min(wStruct, wDiv)

float riskRaw = 0.0
if trendDir != 0 and reachableMax > 0.0
    riskRaw := math.min((sNearH * wNear + sSfpH * wSfp + sStructH * wStruct + sDivH * wDiv) / reachableMax * 100.0, 100.0)

risk = ta.ema(riskRaw, smoothLen)

// ============================================================================
// State / explanation
// ============================================================================

string state = "Quiet"
if trendDir == 0
    state := "No trend"
else if bosNow
    state := "Structure Broken"
else if bosPressureNow
    state := "Break Pressure"
else if risk >= lvlCrit
    state := "Critical"
else if risk >= lvlHigh
    state := "Break Pressure"
else if risk >= lvlElev
    state := "Watch"

string reason = "No clear evidence"
if bosNow
    reason := "Break confirmed"
else if bosPressureNow
    reason := "Local swing break"
else if sNearH > 0.7
    reason := "Approaching break level"
else if sSfpH > 0.5
    reason := "Failed breakout"
else if sStructH > 0.5
    reason := "Structure eroding"
else if sDivH > 0.5
    reason := "Momentum divergence"

dirTxt = trendDir == 1 ? "Uptrend at risk" : trendDir == -1 ? "Downtrend at risk" : "No trend context"
riskCol = f_riskCol(risk)
stateCol = state == "Structure Broken" or state == "Critical" ? cDarkRed : state == "Break Pressure" ? cRed : state == "Watch" ? cOrange : cNeutral

// ============================================================================
// Price chart overlay — drawn via force_overlay (this indicator lives in its own
// RSI pane). Break-level line, risk-shaded cushion zone, thin trend-context EMA.
// ============================================================================

hasCtx = trendDir != 0 and not na(breakLevel)

// Thin EMA trend-context line — neutral colour (direction is read from slope + the
// info text, never from a buy/sell-coloured line).
plot(showEma ? ema : na, "Trend Context", color=color.new(cNeutral, 25), linewidth=1, force_overlay=true)

// Break-level line + cushion zone. Plot the level whenever there is context so the
// zone fill always has an anchor; the LINE's visibility is controlled by its colour
// (na = hidden) independent of the zone.
pLevel = plot(hasCtx ? breakLevel : na, "Break Level", color=showLevel ? riskCol : na, linewidth=2, style=plot.style_linebr, force_overlay=true)
pPrice = plot(hasCtx ? close : na, "", color=color.new(color.gray, 100), display=display.none, editable=false, force_overlay=true)

// Zone fill intensifies as risk rises; only shown once risk is at least Watch.
zoneAlpha = f_clamp(92.0 - risk * 0.3, 70.0, 92.0)
fill(pLevel, pPrice, color=showZone and hasCtx and risk >= lvlElev ? color.new(riskCol, zoneAlpha) : na, title="Risk Zone")

// ============================================================================
// RSI pane (this pane) — the RSI with momentum-divergence lines drawn on it. A
// divergence is price-vs-momentum, so it is only readable where BOTH are visible:
// the RSI here and the price above, sharing the same time axis. When price makes a
// higher high while RSI makes a LOWER high (uptrend) — or price a lower low while
// RSI a higher low (downtrend) — a line marks the failing momentum: the earliest
// structural crack. Drawn in the warning (orange) risk band, never buy/sell.
// ============================================================================

plot(rsi, "RSI", color=color.new(#3179f5, 0), linewidth=1, format=format.price, precision=2)
hline(70, "RSI Overbought", color=color.new(color.gray, 70), linestyle=hline.style_dotted)
hline(50, "RSI Midline", color=color.new(color.gray, 85), linestyle=hline.style_dotted)
hline(30, "RSI Oversold", color=color.new(color.gray, 70), linestyle=hline.style_dotted)

bearDivEvent = trendDir == 1 and not na(ph) and not na(prevPricePH) and not na(prevRsiPH) and lastPricePH > prevPricePH and lastRsiPH < prevRsiPH
bullDivEvent = trendDir == -1 and not na(pl) and not na(prevPricePL) and not na(prevRsiPL) and lastPricePL < prevPricePL and lastRsiPL > prevRsiPL

if showDiv and barstate.isconfirmed and bearDivEvent and not na(prevPHbar)
    line.new(prevPHbar, prevRsiPH, lastPHbar, lastRsiPH, xloc=xloc.bar_index, color=cOrange, width=2)
    label.new(lastPHbar, lastRsiPH, "Div", xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_down,
         color=color.new(cOrange, 8), textcolor=color.white, size=size.tiny,
         tooltip="Bearish momentum divergence\nPrice: higher high — RSI: lower high (" + str.tostring(prevRsiPH, "#.0") + " → " + str.tostring(lastRsiPH, "#.0") + ")\nThe upthrust is weakening — the uptrend's structure is at risk.")

if showDiv and barstate.isconfirmed and bullDivEvent and not na(prevPLbar)
    line.new(prevPLbar, prevRsiPL, lastPLbar, lastRsiPL, xloc=xloc.bar_index, color=cOrange, width=2)
    label.new(lastPLbar, lastRsiPL, "Div", xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_up,
         color=color.new(cOrange, 8), textcolor=color.white, size=size.tiny,
         tooltip="Bullish momentum divergence\nPrice: lower low — RSI: higher low (" + str.tostring(prevRsiPL, "#.0") + " → " + str.tostring(lastRsiPL, "#.0") + ")\nThe down-thrust is weakening — the downtrend's structure is at risk.")

// ============================================================================
// Event labels — words on the chart, placed on the side that is at risk
// ============================================================================

watchEvent    = ta.crossover(risk, lvlElev)
pressureEvent = ta.crossover(risk, lvlHigh) or ta.crossover(risk, lvlCrit)
resetEvent    = ta.crossunder(risk, lvlElev)

string eventText = ""
string eventTip = ""
color eventColor = cNeutral

if bosNow
    eventText := "Structure Broken"
    eventColor := cDarkRed
    eventTip := bosUpNow
         ? "Structure Broken\nUptrend structure broke: close below last swing low.\nThis confirms structural damage after risk was building."
         : "Structure Broken\nDowntrend structure broke: close above last swing high.\nThis confirms structural damage after risk was building."
else if bosPressureNow
    eventText := "Break Pressure"
    eventColor := cRed
    eventTip := rawBosUpBreak
         ? "Break Pressure\nLocal swing low broke, but trend context did not flip.\nTreat as pressure, not a confirmed trend break."
         : "Break Pressure\nLocal swing high broke, but trend context did not flip.\nTreat as pressure, not a confirmed trend break."
else if pressureEvent
    eventText := "Break Pressure"
    eventColor := risk >= lvlCrit ? cDarkRed : cRed
    eventTip := (risk >= lvlCrit ? "Critical\n" : "Break Pressure\n") + dirTxt + ": break pressure is active.\nReason: " + reason + ". Continuation is vulnerable."
else if watchEvent
    eventText := "Watch"
    eventColor := cOrange
    eventTip := "Watch\n" + dirTxt + ": price is moving toward a structure-break condition.\nReason: " + reason + ". Watch the break level; this is not confirmation."
else if resetEvent
    eventText := "Pressure Faded"
    eventColor := cCalm
    eventTip := "Pressure Faded\n" + dirTxt + ": break pressure faded.\nThe structure-break setup is off unless pressure rebuilds."

hasKeyEvent = eventText != ""
var int lastEventBar = na
// Only genuine confirmed structure events bypass the cooldown; threshold
// crossings (Watch / Break Pressure / Pressure Faded) respect it, so a High and
// a Critical crossover on adjacent bars no longer print two stacked labels.
eventReady = bosNow or bosPressureNow or na(lastEventBar) or bar_index - lastEventBar >= markerCooldown

// Anchor every event to the break-level line it concerns, so the label's pointer
// touches the line instead of floating in empty space. Pressure-building events
// hang just below the line, the relief event just above — opposite sides, both
// connected to the structure they describe.
bosLevel = rawBosUpBreak or flipUpBreak ? lastPL : rawBosDnBreak or flipDnBreak ? lastPH : na
evAnchor = (bosNow or bosPressureNow) and not na(bosLevel) ? bosLevel : not na(breakLevel) ? breakLevel : close
isRelief = eventText == "Pressure Faded"
evStyle = isRelief ? label.style_label_up : label.style_label_down

if showMarkers and barstate.isconfirmed and hasKeyEvent and eventReady
    label.new(bar_index, evAnchor, eventText,
         xloc=xloc.bar_index, yloc=yloc.price, style=evStyle, force_overlay=true,
         color=color.new(eventColor, 8), textcolor=color.white, size=size.tiny,
         tooltip=eventTip + "\nRisk: " + str.tostring(risk, "#.0") +
              "\nNear " + str.tostring(sNearH * 100, "#.0") +
              " · BOS " + str.tostring(sBosH * 100, "#.0") +
              " · SFP " + str.tostring(sSfpH * 100, "#.0") +
              " · Erode " + str.tostring(sStructH * 100, "#.0") +
              " · Div " + str.tostring(sDivH * 100, "#.0"))
    lastEventBar := bar_index

// ============================================================================
// Info label — one compact, mobile-friendly text box (not a table) carrying the
// full read: score, state, direction, break level, distance, reason and a
// mini-bar per evidence sensor. Recreated on the last bar; the previous one is
// deleted so no leak builds up on realtime ticks.
// ============================================================================

f_lblSize(s) =>
    s == "Small" ? size.small : s == "Large" ? size.large : size.normal

var label infoLbl = na
if showLabel and barstate.islast
    label.delete(infoLbl)
    distTxt = na(distToBreak) ? "—" : str.tostring(distToBreak, "0.0") + " ATR"
    lvlTxt  = na(breakLevel) ? "—" : str.tostring(breakLevel, format.mintick)
    txt = "Structure Break Risk  " + str.tostring(risk, "#.0") + "  ·  " + state + "\n" +
         dirTxt + "  ·  " + reason + "\n" +
         "Break " + lvlTxt + "  ·  " + distTxt + "\n" +
         f_row("Near", sNearH * 100) + "\n" +
         f_row("SFP", sSfpH * 100) + "\n" +
         f_row("Erode", sStructH * 100) + "\n" +
         f_row("Div", sDivH * 100) + "\n" +
         f_row("BOS", sBosH * 100) + "  (event)"
    tip = "Structure Break Risk\n" +
         "Score = pre-break structure pressure (0-100); colour = risk band.\n" +
         "BOS is NOT mixed into the score — it is a separate confirmed event.\n" +
         "Bars show each sensor's current contribution; Reason names the dominant one."
    infoLbl := label.new(bar_index + labelOffset, close, txt,
         xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, force_overlay=true,
         color=color.new(color.white, 8), textcolor=stateCol,
         size=f_lblSize(labelSize), textalign=text.align_left, tooltip=tip)
else if not showLabel and barstate.islast
    label.delete(infoLbl)

// ============================================================================
// Debug — log on every state change (why it changed)
// ============================================================================

if barstate.isconfirmed and state != state[1]
    log.info("SBR {0} -> {1} | risk={2} dir={3} reason={4} Near={5} BOS={6} SFP={7} Erode={8} Div={9}",
         state[1], state, str.tostring(risk, "#.0"), dirTxt, reason,
         str.tostring(sNearH, "#.00"), str.tostring(sBosH, "#.00"), str.tostring(sSfpH, "#.00"),
         str.tostring(sStructH, "#.00"), str.tostring(sDivH, "#.00"))

// ============================================================================
// Alerts
// ============================================================================

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

alertcondition(alertOk and (ta.crossover(risk, lvlHigh)), "SBR Break Pressure", "SBR · BREAK PRESSURE HIGH · {{ticker}} {{interval}}")
alertcondition(alertOk and (ta.crossover(risk, lvlCrit)), "SBR Critical", "SBR · RISK CRITICAL · {{ticker}} {{interval}}")
alertcondition(alertOk and (bosUpNow), "SBR Top Break", "SBR · TOP BREAK · {{ticker}} {{interval}}")
alertcondition(alertOk and (bosDnNow), "SBR Bottom Break", "SBR · BOTTOM BREAK · {{ticker}} {{interval}}")

Overview

Structure Break Risk plots an RSI oscillator with momentum divergence lines in a separate pane while projecting key break levels, risk cushion zones, and status labels on the price chart. Four weighted sensors monitor ATR proximity to break levels, swing sweeps, structure erosion, and pivot divergences. Settings allow customization of trend bias lengths, pivot detection periods, evidence weights, and alert thresholds.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

Separate pane below the chart

Category

SMC / ICT / price action, Support & resistance, Oscillators & momentum, Trend

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
Trend Bias Lengthint50biasLen
Swing Pivot Lengthint5pivLen
Near Break Level ATR Distancefloat1.5nearAtr
RSI Lengthint14rsiLen
Evidence Memory Barsint10holdBars
Risk Smoothingint3smoothLen
Weight: Near Break Levelfloat25.0wNear
Weight: Failed Breakoutfloat20.0wSfp
Weight: Structure Erosionfloat15.0wStruct
Weight: Pivot Divergencefloat10.0wDiv
State: Watch ≥int35lvlElev
State: Break Pressure ≥int60lvlHigh
Show all 24 settings
SettingTypeDefaultVariable
State: Critical ≥int80lvlCrit
Show Divergence LinesbooltrueshowDiv
Show Trend Context LinebooltrueshowEma
Show Break-Level LinebooltrueshowLevel
Show Risk ZonebooltrueshowZone
Show Event LabelsbooltrueshowMarkers
Event Label Cooldownint10markerCooldown
Require Context Flip for BreakbooltruerequireContextFlip
Show Info LabelbooltrueshowLabel
Label Offset (bars right)int5labelOffset
Label Text SizestringNormallabelSize
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

114 stars

This indicator combines a scaled Forecast Oscillator with a Point of Force state detector. The Forecast Oscillator calculates the percentage deviation of price from its linear regression curve and normalizes the output between 0 and 1, plotting a line in a sep

Indicator v4 Oscillators & momentumSupport & resistance Repaints
575 stars

This indicator detects equal highs and lows by comparing prices within user-defined left and right bars using a tolerance percentage. It plots horizontal lines on the price chart for equal highs in one color and lows in another. A screener allows input of mult

Indicator v5 Support & resistanceTrend
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Equal Highs and Lows with Screener

This Pine Script indicator plots a ZigZag on the price chart. It identifies reversal points when price moves more than a set percentage from the last pivot. The script calculates forecasts using zigzag data and labels detected geometric patterns including harm

Indicator v4 TrendSupport & resistance
218 stars

This indicator plots market structure on the price chart by identifying swing highs and lows. It connects these points with lines to visualize trend direction and detect Break of Structure (BOS) when price breaks a prior swing in the current trend. Change of C

Indicator v6 TrendSupport & resistance
Updated 3 months ago · None stated GitHub repository of Market Structure - BOS & CHoCH

Open Source Fractal

by cantolab
183 stars

This indicator implements the Fractal Model concept to plot market structure shifts, liquidity sweeps, and Change in State of Delivery (CISD) levels. It overlays custom higher-timeframe candle previews, open prices, and time markers directly on the price chart

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · MIT GitHub repository of Open Source Fractal

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