Indicator · Pine Script v6 · On the chart

Delta Break and Retest Stage 5 Break Detection

by rosijay17-cmd

An indicator meant for the 15-minute chart that extracts delta-based points of interest from each session, turns them into ATR-sized zones with contamination and freshness rules, and marks the first close through a zone edge plus an ATR buffer.

View on GitHub

Repaints

GitHub stars1on the repository
Pine versionv6current
Lines of code736
Settings39inputs
LicenceGPL-3.0
Last update7 days ago22 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

736 lines of Pine Script v6 from delta_break_retest/Stage5_BreakDetection.pine, published under GPL-3.0. Keep the author's credit and licence lines when you reuse it.

Stage5_BreakDetection.pine
//@version=6

// ============================================================================
// DELTA BREAK & RETEST -- STAGE 5: BREAK DETECTION
// ----------------------------------------------------------------------------
// Per the build spec (Section 9), Stage 5: 15m setup timeframe -- detect a
// clean break through an active POI zone. Plot break events. Verify by eye
// against real chart structure before Stage 6 (5m retest/trigger) consumes
// this stage's "broken" state.
//
// STILL NO TRADING LOGIC of any kind -- indicator only. This stage tags and
// plots break events; it does not decide whether to retest, trigger, size,
// or enter anything. That is genuinely Stage 6 and beyond.
//
// TIMEFRAME ASSUMPTION: per Section 2's hierarchy, this indicator is meant
// to run on the 15m chart itself (the "setup/POI/break" link) -- close/high/
// low used below are native chart bars, not pulled via request.security.
// The dashboard's "Chart Timeframe" row exists specifically so a mismatch
// (applied on the wrong chart) is visible at a glance rather than silently
// producing garbage break timing.
//
// CARRIED FORWARD UNCHANGED (scope discipline, Section 0):
//   - f_delta()                                   (Stage 1)
//   - Poi type's original fields, session tracking, f_extractSessionPois(),
//     f_cleanupPois(), f_evictOldestForRoom(), f_tryAddPoi(), the session-
//     close extraction block, and the per-bar contamination/tested loop
//     (Stage 2) -- a break needs live, hygiene-checked POI zones to break
//     THROUGH, so the whole registry comes forward as-is.
//   - f_get4hContext() / f_get1hContext() and their request.security() pulls
//     (Stage 4, already fixed for both the empirical dead-pivot bug and the
//     CE10123 tuple-indexing bug -- see the pine-proven-patterns skill) --
//     used here ONLY to TAG a break as bias-aligned or not for the dashboard/
//     log. Not used to gate or suppress break detection itself; per the
//     spec's "one filter at a time, measured" rule, that gating decision is
//     explicitly deferred to a later, separately-tested stage.
//
// DROPPED from Stage 2 (deliberately, scope discipline): the optional live
// in-session delta histogram boxes. Break detection doesn't need them, and
// Stage 2 already validated that visual by eye -- no reason to carry
// unrelated rendering weight into every later stage.
//
// NEW IN THIS STAGE -- break definition:
//   A POI zone (fixed at [zoneLow, zoneHigh] since creation) is "broken" the
//   first bar its close clears the zone edge, PLUS a small ATR buffer, when
//   the PREVIOUS bar's close had not yet cleared that same buffered edge.
//   That is a stateless, per-bar comparison against the zone's own fixed
//   bounds (close vs close[1], same buffered edge both times) -- no separate
//   "which side was price on" field needed on the Poi type. Optionally
//   requires the breaking bar's own delta to agree with the break direction
//   (a wide-range low-conviction bar closing through on pure noise is not
//   what "break" is meant to mean).
// ============================================================================

indicator(title = "Delta Break & Retest - Stage 5 Break Detection", shorttitle = "DBR Stage5", overlay = true, max_boxes_count = 200, max_labels_count = 300)

// ============================================================================
// PURE DELTA FUNCTION (unchanged from Stage 1)
// ============================================================================

f_delta(float o, float h, float l, float c, float v) =>
    float safeRange = math.max(h - l, syminfo.mintick)
    float pressure = math.max(math.min((c - o) / safeRange, 1.0), -1.0)
    v * pressure

float barDelta = f_delta(open, high, low, close, nz(volume, 0.0))

// The live delta profile's lookback can now reach up to 30 days scanned via
// dynamic (variable) historical offsets -- time[i]/open[i]/high[i]/low[i]/
// close[i]/volume[i] inside a while/for loop, not a fixed constant offset
// Pine's compiler can size the buffer for automatically. Force the buffer
// explicitly so a 30-day setting doesn't silently truncate on a chart
// where the auto-detected default would otherwise be too small.
max_bars_back(time, 5000)
max_bars_back(open, 5000)
max_bars_back(high, 5000)
max_bars_back(low, 5000)
max_bars_back(close, 5000)
max_bars_back(volume, 5000)

// ============================================================================
// INPUTS
// ============================================================================

int profileRowsInput = input.int(40, "Profile Rows (POI Extraction Granularity)", minval = 10, maxval = 120, group = "POI Extraction")
bool showLiveProfileInput = input.bool(true, "Show Live Delta Profile (Price-Binned Histogram)", group = "Delta Profile", tooltip = "The price-by-price buy/sell strength histogram from Stage 1/2 -- teal/orange boxes at each price row. Restored per request; the dashboard's Buyers/Sellers bar is a compact SUMMARY of this, not a replacement for it.")
int histogramLookbackDaysInput = input.int(1, "Histogram Lookback (Days)", minval = 1, maxval = 30, group = "Delta Profile", tooltip = "How far back the live delta profile pools bars from -- a rolling window measured in calendar days from the most recent bar, not tied to session boundaries. 1 = roughly the last trading day; raise up to 30 to build the histogram from many days of bars instead of just the current session. Internally capped at 5000 bars scanned regardless of this setting, to stay inside Pine's own historical-offset limit on a low chart timeframe.")
int histogramOffsetInput = input.int(6, "Histogram Offset (Bars)", minval = 1, maxval = 60, group = "Delta Profile")
int histogramWidthInput = input.int(30, "Histogram Max Width (Bars)", minval = 5, maxval = 100, group = "Delta Profile")
color positiveDeltaColorInput = input.color(color.teal, "Positive Delta Color", group = "Delta Profile")
color negativeDeltaColorInput = input.color(color.orange, "Negative Delta Color", group = "Delta Profile")
string poiAtrTimeframeInput = input.timeframe("15", "POI Zone ATR Timeframe", group = "POI Extraction", tooltip = "Per spec: zone half-width is 0.25 x ATR(14) on 15m by default.")
float poiAtrMultInput = input.float(0.25, "POI Zone Half-Width (x ATR)", minval = 0.05, maxval = 2.0, step = 0.05, group = "POI Extraction")
int contaminationThresholdInput = input.int(3, "Contamination Threshold (Crossings)", minval = 1, maxval = 10, group = "POI Extraction")
int poiFreshnessSessionsInput = input.int(2, "POI Freshness (Sessions Untested)", minval = 1, maxval = 10, group = "POI Extraction")
float minSeparationAtrMultInput = input.float(0.5, "Minimum POI Separation (x ATR)", minval = 0.1, maxval = 3.0, step = 0.1, group = "POI Extraction")
int maxActivePoisInput = input.int(6, "Max Active POIs", minval = 1, maxval = 10, group = "POI Extraction")
bool showAbsorptionInput = input.bool(true, "Extract Absorption Node", group = "POI Extraction")
int minEndedSessionBarsInput = input.int(10, "Minimum Bars To Finalize A Session", minval = 3, maxval = 100, group = "POI Extraction")

color pocColorInput = input.color(color.aqua, "Delta POC Color", group = "POI Colors")
color extremeColorInput = input.color(color.fuchsia, "Delta Extreme Color", group = "POI Colors")
color flipColorInput = input.color(color.yellow, "Delta Flip Color", group = "POI Colors")
color absorptionColorInput = input.color(color.silver, "Absorption Node Color", group = "POI Colors")

float minBreakAtrMultInput = input.float(0.10, "Minimum Break-Through Buffer (x ATR)", minval = 0.0, maxval = 1.0, step = 0.05, group = "Break Detection", tooltip = "Close must clear the zone edge by this many ATR before it counts as a break, not just a wick-through or marginal close.")
bool requireDeltaConfirmationInput = input.bool(true, "Require Delta Confirmation On Break Bar", group = "Break Detection", tooltip = "If on, the breaking bar's own delta must agree with the break direction (positive delta for an up-break, negative for a down-break).")
color breakUpColorInput = input.color(color.lime, "Break Up Color", group = "Break Detection")
color breakDownColorInput = input.color(color.red, "Break Down Color", group = "Break Detection")

string biasTimeframeInput = input.timeframe("240", "4H Bias Timeframe", group = "MTF Context (Tag Only)")
int pivotLeftInput = input.int(3, "Swing Pivot Left Bars", minval = 1, maxval = 20, group = "MTF Context (Tag Only)")
int pivotRightInput = input.int(3, "Swing Pivot Right Bars", minval = 1, maxval = 20, group = "MTF Context (Tag Only)")
int biasEmaLengthInput = input.int(50, "4H EMA Length", minval = 2, group = "MTF Context (Tag Only)")
int biasSlopeLookbackInput = input.int(5, "4H EMA Slope Lookback (Bars)", minval = 1, maxval = 50, group = "MTF Context (Tag Only)")
string trendTimeframeInput = input.timeframe("60", "1H Trend Timeframe", group = "MTF Context (Tag Only)")
int adxLengthInput = input.int(14, "1H ADX / DI Length", minval = 1, group = "MTF Context (Tag Only)")
float adxMinimumInput = input.float(20.0, "1H Minimum ADX", minval = 0, step = 0.5, group = "MTF Context (Tag Only)")
int fastEmaInput = input.int(21, "1H Fast EMA", minval = 1, group = "MTF Context (Tag Only)")
int slowEmaInput = input.int(50, "1H Slow EMA", minval = 2, group = "MTF Context (Tag Only)")
int erLengthInput = input.int(10, "1H Efficiency Ratio Length", minval = 2, group = "MTF Context (Tag Only)")
float erMinimumInput = input.float(0.30, "1H Minimum Efficiency Ratio", minval = 0, maxval = 1, step = 0.05, group = "MTF Context (Tag Only)")

int deltaStrengthWindowInput = input.int(20, "Buyer/Seller Strength Window (Bars)", minval = 5, maxval = 200, group = "Signal Summary", tooltip = "How many recent bars' delta get pooled into the Buyers/Sellers strength bars.")
color buyColorInput = input.color(color.teal, "Buyer Strength Color", group = "Signal Summary")
color sellColorInput = input.color(color.orange, "Seller Strength Color", group = "Signal Summary")
bool showDiagnosticsInput = input.bool(false, "Show Detailed Diagnostics (POI / Break Log / Session Info)", group = "Signal Summary", tooltip = "Off by default -- the dashboard shows only Bias, Regime, Bull/Bear Score, and Buyer/Seller strength. Turn this on to bring back the POI Status list, Break Log, session index, and ATR readout for debugging.")

// ============================================================================
// 15m ATR (zone sizing -- unchanged from Stage 2)
// ============================================================================

float atr15 = request.security(syminfo.tickerid, poiAtrTimeframeInput, ta.atr(14), lookahead = barmerge.lookahead_off)

// ============================================================================
// SESSION TRACKING (unchanged from Stage 2)
// ============================================================================

bool newSession = ta.change(time("D")) != 0

var int sessionStartBar = bar_index
var int sessionIndex = 0

int barsIntoSession = bar_index - sessionStartBar + 1

var float cumulativeDelta = 0.0

if newSession
    cumulativeDelta := 0.0

cumulativeDelta := cumulativeDelta + barDelta

// ============================================================================
// POI TYPE -- Stage 2's fields unchanged, plus NEW break-tracking fields.
// ============================================================================

type Poi
    float price
    float zoneLow
    float zoneHigh
    string poiType
    int createdBar
    int createdSessionIndex
    int crossCount = 0
    bool contaminated = false
    bool tested = false
    bool touchedLastBar = false
    bool broken = false
    int breakBar = na
    string breakDirection = na
    bool breakAligned = false
    box zoneBox = na
    label zoneLabel = na

var array<Poi> poiRegistry = array.new<Poi>()

// ============================================================================
// SESSION PROFILE EXTRACTION (unchanged from Stage 2)
// ============================================================================

f_extractSessionPois(int barsBack, int rows) =>
    float scanHigh = -1.0e20
    float scanLow = 1.0e20

    for i = 1 to barsBack
        scanHigh := math.max(scanHigh, high[i])
        scanLow := math.min(scanLow, low[i])

    float rowStep = math.max((scanHigh - scanLow) / rows, syminfo.mintick)

    array<float> rowDelta = array.new_float(rows, 0.0)
    array<float> rowRange = array.new_float(rows, 0.0)
    array<float> rowVolume = array.new_float(rows, 0.0)

    for i = 1 to barsBack
        float d = f_delta(open[i], high[i], low[i], close[i], nz(volume[i], 0.0))
        int row = int(math.floor((close[i] - scanLow) / rowStep))
        row := math.max(0, math.min(rows - 1, row))
        array.set(rowDelta, row, array.get(rowDelta, row) + d)
        array.set(rowRange, row, array.get(rowRange, row) + (high[i] - low[i]))
        array.set(rowVolume, row, array.get(rowVolume, row) + nz(volume[i], 0.0))

    float maxAbs = -1.0
    int pocRow = -1

    for row = 0 to rows - 1
        float d = array.get(rowDelta, row)
        if math.abs(d) > maxAbs
            maxAbs := math.abs(d)
            pocRow := row

    float pocPrice = pocRow >= 0 ? scanLow + (pocRow + 0.5) * rowStep : na

    float maxPos = 0.0
    int maxPosRow = -1
    float maxNeg = 0.0
    int maxNegRow = -1

    for row = 0 to rows - 1
        float d = array.get(rowDelta, row)
        if d > maxPos
            maxPos := d
            maxPosRow := row
        if d < maxNeg
            maxNeg := d
            maxNegRow := row

    float extremeHighPrice = maxPosRow >= 0 ? scanLow + (maxPosRow + 0.5) * rowStep : na
    float extremeLowPrice = maxNegRow >= 0 ? scanLow + (maxNegRow + 0.5) * rowStep : na

    float cum = 0.0
    float prevCum = 0.0
    float flipPrice = na

    for row = 0 to rows - 1
        prevCum := cum
        cum := cum + array.get(rowDelta, row)
        if row > 0 and ((prevCum <= 0 and cum > 0) or (prevCum >= 0 and cum < 0))
            flipPrice := scanLow + row * rowStep

    float totalVol = 0.0
    for row = 0 to rows - 1
        totalVol += array.get(rowVolume, row)
    float avgVol = totalVol / rows

    float bestRatio = -1.0
    int absorptionRow = -1

    for row = 0 to rows - 1
        if row != pocRow and array.get(rowVolume, row) > avgVol * 0.5
            float rng = array.get(rowRange, row)
            float ratio = rng > 0 ? math.abs(array.get(rowDelta, row)) / rng : 0.0
            if ratio > bestRatio
                bestRatio := ratio
                absorptionRow := row

    float absorptionPrice = absorptionRow >= 0 ? scanLow + (absorptionRow + 0.5) * rowStep : na

    [pocPrice, extremeHighPrice, extremeLowPrice, flipPrice, absorptionPrice]

// ============================================================================
// POI REGISTRY: cleanup and add-with-hygiene (unchanged from Stage 2)
// ============================================================================

f_cleanupPois(array<Poi> registry, int currentSessionIndex, int freshnessSessions) =>
    int i = array.size(registry) - 1
    while i >= 0
        Poi p = array.get(registry, i)
        bool expired = not p.tested and (currentSessionIndex - p.createdSessionIndex) >= freshnessSessions
        if p.contaminated or expired
            if not na(p.zoneBox)
                box.delete(p.zoneBox)
            if not na(p.zoneLabel)
                label.delete(p.zoneLabel)
            array.remove(registry, i)
        i -= 1

f_evictOldestForRoom(array<Poi> registry) =>
    int evictIdx = -1
    int oldestTestedSession = 999999
    int oldestAnySession = 999999
    int oldestAnyIdx = -1

    if array.size(registry) > 0
        for i = 0 to array.size(registry) - 1
            Poi existing = array.get(registry, i)
            if existing.createdSessionIndex < oldestAnySession
                oldestAnySession := existing.createdSessionIndex
                oldestAnyIdx := i
            if existing.tested and existing.createdSessionIndex < oldestTestedSession
                oldestTestedSession := existing.createdSessionIndex
                evictIdx := i

    if evictIdx < 0
        evictIdx := oldestAnyIdx

    if evictIdx >= 0
        Poi evicted = array.get(registry, evictIdx)
        if not na(evicted.zoneBox)
            box.delete(evicted.zoneBox)
        if not na(evicted.zoneLabel)
            label.delete(evicted.zoneLabel)
        array.remove(registry, evictIdx)

f_tryAddPoi(array<Poi> registry, float price, string poiType, color poiColor, float atrVal, float zoneMult, float minSepMult, int maxActive, int sessIdx) =>
    bool added = false
    if not na(price) and not na(atrVal) and atrVal > 0
        float halfWidth = atrVal * zoneMult
        float zLow = price - halfWidth
        float zHigh = price + halfWidth

        bool tooClose = false
        if array.size(registry) > 0
            for i = 0 to array.size(registry) - 1
                Poi existing = array.get(registry, i)
                if not existing.contaminated and math.abs(existing.price - price) < atrVal * minSepMult
                    tooClose := true

        if not tooClose
            if array.size(registry) >= maxActive
                f_evictOldestForRoom(registry)

        if not tooClose and array.size(registry) < maxActive
            Poi newPoi = Poi.new(price = price, zoneLow = zLow, zoneHigh = zHigh, poiType = poiType, createdBar = bar_index, createdSessionIndex = sessIdx)
            newPoi.zoneBox := box.new(left = bar_index, top = zHigh, right = bar_index, bottom = zLow, xloc = xloc.bar_index, bgcolor = color.new(poiColor, 85), border_color = poiColor)
            newPoi.zoneLabel := label.new(bar_index, zHigh, poiType, style = label.style_label_down, color = color.new(poiColor, 20), textcolor = color.white, size = size.tiny)
            array.push(registry, newPoi)
            added := true
    added

// ============================================================================
// SESSION CLOSE: finalize the ended session's profile, extract POIs
// (unchanged from Stage 2)
// ============================================================================

if newSession
    if bar_index > 0 and barsIntoSession > 1
        int endedBars = bar_index - sessionStartBar
        if endedBars >= minEndedSessionBarsInput and not na(atr15)
            [pocPx, exHighPx, exLowPx, flipPx, absPx] = f_extractSessionPois(endedBars, profileRowsInput)

            f_cleanupPois(poiRegistry, sessionIndex, poiFreshnessSessionsInput)

            f_tryAddPoi(poiRegistry, pocPx, "Delta POC", pocColorInput, atr15, poiAtrMultInput, minSeparationAtrMultInput, maxActivePoisInput, sessionIndex)
            f_tryAddPoi(poiRegistry, exHighPx, "Delta Extreme +", extremeColorInput, atr15, poiAtrMultInput, minSeparationAtrMultInput, maxActivePoisInput, sessionIndex)
            f_tryAddPoi(poiRegistry, exLowPx, "Delta Extreme -", extremeColorInput, atr15, poiAtrMultInput, minSeparationAtrMultInput, maxActivePoisInput, sessionIndex)
            f_tryAddPoi(poiRegistry, flipPx, "Delta Flip", flipColorInput, atr15, poiAtrMultInput, minSeparationAtrMultInput, maxActivePoisInput, sessionIndex)
            if showAbsorptionInput
                f_tryAddPoi(poiRegistry, absPx, "Absorption Node", absorptionColorInput, atr15, poiAtrMultInput, minSeparationAtrMultInput, maxActivePoisInput, sessionIndex)

    sessionIndex += 1
    sessionStartBar := bar_index

// ============================================================================
// 4H / 1H MTF CONTEXT -- carried forward from Stage 4, already fixed for
// both the dead-pivot bug (round 1) and the CE10123 tuple-indexing bug
// (round 2, confirmed working on-device). Used here ONLY to tag a break as
// bias-aligned or not; see header comment.
// ============================================================================

f_get4hContext() =>
    float ph = ta.pivothigh(high, pivotLeftInput, pivotRightInput)
    float pl = ta.pivotlow(low, pivotLeftInput, pivotRightInput)

    float latestHigh = ta.valuewhen(not na(ph), ph, 0)
    float previousHigh = ta.valuewhen(not na(ph), ph, 1)
    float latestLow = ta.valuewhen(not na(pl), pl, 0)
    float previousLow = ta.valuewhen(not na(pl), pl, 1)

    float ema = ta.ema(close, biasEmaLengthInput)
    bool aboveEma = close > ema
    bool slopeUp = ema > ema[biasSlopeLookbackInput]
    bool slopeDown = ema < ema[biasSlopeLookbackInput]

    [latestHigh[1], previousHigh[1], latestLow[1], previousLow[1], aboveEma[1], slopeUp[1], slopeDown[1]]

[lastPivotHigh, prevPivotHigh, lastPivotLow, prevPivotLow, closeAboveEma4h, emaSlopeUp4h, emaSlopeDown4h] = request.security(syminfo.tickerid, biasTimeframeInput, f_get4hContext(), gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_on)

bool haveEnoughPivots = not na(lastPivotHigh) and not na(prevPivotHigh) and not na(lastPivotLow) and not na(prevPivotLow)

bool higherHigh = haveEnoughPivots and lastPivotHigh > prevPivotHigh
bool lowerHigh = haveEnoughPivots and lastPivotHigh < prevPivotHigh
bool higherLow = haveEnoughPivots and lastPivotLow > prevPivotLow
bool lowerLow = haveEnoughPivots and lastPivotLow < prevPivotLow

bool structureUp = higherHigh and higherLow
bool structureDown = lowerHigh and lowerLow

bool slowFilterUp = closeAboveEma4h and emaSlopeUp4h
bool slowFilterDown = (not closeAboveEma4h) and emaSlopeDown4h

string bias = structureUp and slowFilterUp ? "LONG_ONLY" : structureDown and slowFilterDown ? "SHORT_ONLY" : "NO_TRADE"

f_efficiencyRatio(int length) =>
    float netMove = math.abs(close - close[length])
    float sumMoves = 0.0
    for i = 0 to length - 1
        sumMoves += math.abs(close[i] - close[i + 1])
    sumMoves > 0 ? netMove / sumMoves : 0.0

f_get1hContext() =>
    [plusDi, minusDi, adx] = ta.dmi(adxLengthInput, adxLengthInput)
    float emaFast = ta.ema(close, fastEmaInput)
    float emaSlow = ta.ema(close, slowEmaInput)
    float er = f_efficiencyRatio(erLengthInput)
    [adx[1], emaFast[1], emaSlow[1], er[1]]

[adx1h, emaFast1h, emaSlow1h, er1h] = request.security(syminfo.tickerid, trendTimeframeInput, f_get1hContext(), gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_on)

bool adxOk = adx1h >= adxMinimumInput
bool emaStackOk = bias == "LONG_ONLY" ? emaFast1h > emaSlow1h : bias == "SHORT_ONLY" ? emaFast1h < emaSlow1h : false
bool erOk = er1h >= erMinimumInput
bool trendOK = bias != "NO_TRADE" and adxOk and emaStackOk and erOk

// Raw (bias-independent) 1H stack direction -- needed by the bull/bear point
// score below, which scores each direction on its own merits rather than
// only ever evaluating the direction bias already picked.
bool emaStackBullish1h = emaFast1h > emaSlow1h
bool emaStackBearish1h = emaFast1h < emaSlow1h

// ============================================================================
// EVERY BAR: contamination/tested bookkeeping (unchanged from Stage 2) AND
// NEW break detection, merged into one loop over the registry.
// ============================================================================

var array<string> breakLog = array.new_string()

if array.size(poiRegistry) > 0
    for i = 0 to array.size(poiRegistry) - 1
        Poi p = array.get(poiRegistry, i)

        if not p.contaminated
            bool touchedNow = high >= p.zoneLow and low <= p.zoneHigh

            if touchedNow and not p.touchedLastBar
                if not p.tested
                    p.tested := true
                p.crossCount := p.crossCount + 1
                if p.crossCount >= contaminationThresholdInput
                    p.contaminated := true

            p.touchedLastBar := touchedNow

            // Only keep stretching the box to the current bar while the
            // zone is still "live" (unbroken, uncontaminated) -- once it
            // breaks, freeze the box at the break bar instead of smearing
            // it across every bar between the break and now. A zone that
            // broke 200 bars ago doesn't need a horizontal line spanning
            // those 200 bars; the break-event arrow label already marks
            // exactly where it happened.
            if not na(p.zoneBox) and not p.broken
                box.set_right(p.zoneBox, bar_index)

            // Fade BOTH the box and its original text label once a POI is
            // no longer "live" -- contaminated (chopped through repeatedly,
            // genuinely dead) or broken (played out, superseded by its own
            // dedicated break-arrow label). Only still-live POIs (fresh /
            // naked / tested-but-unbroken) keep their full poiType color, so
            // the chart's visual weight tracks what's actually still
            // relevant instead of every POI ever extracted competing at
            // equal brightness.
            if p.contaminated and not na(p.zoneBox)
                box.set_bgcolor(p.zoneBox, color.new(color.gray, 92))
                box.set_border_color(p.zoneBox, color.new(color.gray, 60))
                if not na(p.zoneLabel)
                    label.set_color(p.zoneLabel, color.new(color.gray, 85))
                    label.set_textcolor(p.zoneLabel, color.new(color.gray, 50))
            else if p.broken and not na(p.zoneLabel)
                label.set_color(p.zoneLabel, color.new(color.gray, 70))
                label.set_textcolor(p.zoneLabel, color.new(color.gray, 35))

            // NEW: break detection -- stateless close-vs-close[1] comparison
            // against this POI's own fixed, buffered zone edges. See header
            // comment for the full definition and rationale.
            if not p.broken and not p.contaminated
                float buffer = not na(atr15) ? atr15 * minBreakAtrMultInput : 0.0
                bool brokeUpNow = close > p.zoneHigh + buffer and close[1] <= p.zoneHigh + buffer
                bool brokeDownNow = close < p.zoneLow - buffer and close[1] >= p.zoneLow - buffer
                bool deltaOkUp = not requireDeltaConfirmationInput or barDelta > 0
                bool deltaOkDown = not requireDeltaConfirmationInput or barDelta < 0

                if brokeUpNow and deltaOkUp
                    p.broken := true
                    p.breakBar := bar_index
                    p.breakDirection := "UP"
                    p.breakAligned := bias == "LONG_ONLY"
                    label.new(bar_index, p.zoneHigh, "▲ " + p.poiType, style = label.style_label_up, color = color.new(breakUpColorInput, p.breakAligned ? 0 : 55), textcolor = color.white, size = size.small)
                    array.unshift(breakLog, "bar" + str.tostring(bar_index) + " " + p.poiType + " UP @ " + str.tostring(close, format.mintick) + (p.breakAligned ? " (aligned)" : ""))
                    if array.size(breakLog) > 8
                        array.pop(breakLog)
                else if brokeDownNow and deltaOkDown
                    p.broken := true
                    p.breakBar := bar_index
                    p.breakDirection := "DOWN"
                    p.breakAligned := bias == "SHORT_ONLY"
                    label.new(bar_index, p.zoneLow, "▼ " + p.poiType, style = label.style_label_down, color = color.new(breakDownColorInput, p.breakAligned ? 0 : 55), textcolor = color.white, size = size.small)
                    array.unshift(breakLog, "bar" + str.tostring(bar_index) + " " + p.poiType + " DOWN @ " + str.tostring(close, format.mintick) + (p.breakAligned ? " (aligned)" : ""))
                    if array.size(breakLog) > 8
                        array.pop(breakLog)

// ============================================================================
// LIVE IN-SESSION DELTA PROFILE (Stage 1/2, restored)
// ----------------------------------------------------------------------------
// Dropped from this file's first draft as an out-of-scope carry-forward;
// put back per explicit request -- the dashboard's Buyers/Sellers bar is a
// single-number SUMMARY of recent buy/sell dominance, not a substitute for
// seeing WHERE along the price axis that pressure actually concentrated.
// Unchanged logic from Stage 1/2: current session only, rebuilt fresh each
// bar on barstate.islast, drawn as teal/orange boxes to the right of price.
// ============================================================================

var array<box> profileBoxes = array.new_box()

if showLiveProfileInput and barstate.islast

    if array.size(profileBoxes) > 0
        for i = 0 to array.size(profileBoxes) - 1
            box.delete(array.get(profileBoxes, i))
    array.clear(profileBoxes)

    float scanHigh2 = -1.0e20
    float scanLow2 = 1.0e20

    // Lookback is a configurable ROLLING TIME WINDOW (up to 30 days) rather
    // than being locked to "current session only" -- walk back bar by bar
    // while still inside the window, capped at 5000 bars scanned (Pine's
    // own historical-offset ceiling) so a large day count on a low chart
    // timeframe can't ask for an unbounded number of bars.
    int lookbackMs = histogramLookbackDaysInput * 24 * 60 * 60 * 1000
    int cutoffTime = time - lookbackMs
    int maxScanCap = 5000
    int scanBars2 = 0
    while scanBars2 < maxScanCap and scanBars2 < bar_index and time[scanBars2] >= cutoffTime
        scanBars2 += 1
    scanBars2 := math.max(scanBars2, 1)

    for i = 0 to scanBars2 - 1
        scanHigh2 := math.max(scanHigh2, high[i])
        scanLow2 := math.min(scanLow2, low[i])

    float rowStep2 = math.max((scanHigh2 - scanLow2) / profileRowsInput, syminfo.mintick)

    array<float> rowDelta2 = array.new_float(profileRowsInput, 0.0)

    for i = 0 to scanBars2 - 1
        float d2 = f_delta(open[i], high[i], low[i], close[i], nz(volume[i], 0.0))
        int row2 = int(math.floor((close[i] - scanLow2) / rowStep2))
        row2 := math.max(0, math.min(profileRowsInput - 1, row2))
        array.set(rowDelta2, row2, array.get(rowDelta2, row2) + d2)

    float maxAbs2 = 0.0
    for row2 = 0 to profileRowsInput - 1
        maxAbs2 := math.max(maxAbs2, math.abs(array.get(rowDelta2, row2)))

    int centerX2 = bar_index + histogramOffsetInput

    for row2 = 0 to profileRowsInput - 1
        float d2 = array.get(rowDelta2, row2)
        if d2 != 0
            float rowLow2 = scanLow2 + row2 * rowStep2
            float rowHigh2 = rowLow2 + rowStep2
            int barWidth2 = maxAbs2 > 0 ? int(math.round(histogramWidthInput * math.abs(d2) / maxAbs2)) : 0
            if barWidth2 > 0
                color barColor2 = d2 >= 0 ? positiveDeltaColorInput : negativeDeltaColorInput
                box rowBox2 = box.new(left = centerX2, top = rowHigh2, right = centerX2 + barWidth2, bottom = rowLow2, xloc = xloc.bar_index, bgcolor = color.new(barColor2, 15), border_color = color.new(barColor2, 40))
                array.push(profileBoxes, rowBox2)

// ============================================================================
// DASHBOARD
// ----------------------------------------------------------------------------
// Redesigned per explicit request: the previous version dumped every raw
// diagnostic (session index, ATR, individual POI rows, full break log) at
// equal visual weight -- correct for validating each build stage, but too
// much to read at a glance while actually watching the market. Default view
// is now just five rows: Bias, Regime, Bull Score, Bear Score, and a
// Buyer/Seller strength bar. Everything from the old detailed view still
// exists underneath and is one toggle away (showDiagnosticsInput) for
// debugging -- nothing was deleted, just hidden by default.
// ============================================================================

int coreRows = 7
int diagnosticRows = 13
int tableRows = showDiagnosticsInput ? coreRows + diagnosticRows : coreRows

var table statusTable = table.new(position.bottom_right, 2, tableRows, bgcolor = color.new(color.rgb(15, 20, 27), 10), border_width = 1)

if barstate.islast
    // ---- REGIME: single word, derived from the same ADX/ER chop gate and
    // bias already computed above. CHOPPY when either strength/efficiency
    // gate fails regardless of direction; TRANSITIONAL when the market has
    // enough energy to trade (both gates pass) but 4H structure and the
    // slow EMA filter disagree on direction (bias == NO_TRADE); otherwise
    // TRENDING in whichever direction bias picked.
    string regime = (not adxOk or not erOk) ? "CHOPPY" : bias == "LONG_ONLY" ? "TRENDING UP" : bias == "SHORT_ONLY" ? "TRENDING DOWN" : "TRANSITIONAL"
    color regimeColor = regime == "CHOPPY" ? color.gray : regime == "TRANSITIONAL" ? color.orange : regime == "TRENDING UP" ? color.lime : color.red

    // ---- BULL / BEAR POINT SCORE: six independent yes/no checks per side,
    // each worth one point. Deliberately NOT combined into a single net
    // number -- seeing bull=4/6 next to bear=1/6 (clear edge) reads
    // differently than bull=3/6 next to bear=3/6 (no edge, not "nets to
    // zero" which looks the same as "nothing computed"). Same six criteria
    // mirrored per direction: 4H structure, 4H slow filter (price vs EMA +
    // slope), 1H ADX strength (direction-agnostic), 1H EMA stack direction,
    // 1H efficiency ratio (direction-agnostic), recent buyer/seller delta
    // dominance (computed just below).
    int bullPoints = 0
    int bearPoints = 0
    if structureUp
        bullPoints += 1
    if structureDown
        bearPoints += 1
    if slowFilterUp
        bullPoints += 1
    if slowFilterDown
        bearPoints += 1
    if adxOk
        bullPoints += 1
        bearPoints += 1
    if emaStackBullish1h
        bullPoints += 1
    if emaStackBearish1h
        bearPoints += 1
    if erOk
        bullPoints += 1
        bearPoints += 1

    // ---- BUYER / SELLER STRENGTH: pool the last N bars' delta into a
    // positive (buy) pool and a negative (sell) pool, express each side's
    // share as a 10-block bar. This is what was missing from the dashboard
    // entirely in this stage (dropped from Stage 1/2's on-chart histogram
    // for scope reasons) -- brought back here as a compact readout instead
    // of the full price-binned profile, since the ask was specifically
    // "levels of strength," not the detailed price-by-price breakdown.
    float rollingBuyDelta = 0.0
    float rollingSellDelta = 0.0
    for i = 0 to deltaStrengthWindowInput - 1
        float d = f_delta(open[i], high[i], low[i], close[i], nz(volume[i], 0.0))
        if d > 0
            rollingBuyDelta += d
        else
            rollingSellDelta += -d

    if rollingBuyDelta > rollingSellDelta
        bullPoints += 1
    else if rollingSellDelta > rollingBuyDelta
        bearPoints += 1

    float totalDelta = rollingBuyDelta + rollingSellDelta
    float buyRatio = totalDelta > 0 ? rollingBuyDelta / totalDelta : 0.5
    float sellRatio = 1.0 - buyRatio
    int buyBlocks = int(math.round(buyRatio * 10))
    int sellBlocks = int(math.round(sellRatio * 10))
    string buyBar = str.repeat("█", buyBlocks) + str.repeat("░", 10 - buyBlocks)
    string sellBar = str.repeat("█", sellBlocks) + str.repeat("░", 10 - sellBlocks)

    table.cell(statusTable, 0, 0, "DELTA BREAK & RETEST", text_color = color.white, bgcolor = color.rgb(22, 52, 86), text_size = size.tiny)
    table.cell(statusTable, 1, 0, "", text_color = color.white, bgcolor = color.rgb(22, 52, 86), text_size = size.tiny)

    table.cell(statusTable, 0, 1, "Bias", text_color = color.white, text_size = size.small)
    table.cell(statusTable, 1, 1, bias, text_color = bias == "LONG_ONLY" ? color.lime : bias == "SHORT_ONLY" ? color.red : color.gray, text_size = size.small)

    table.cell(statusTable, 0, 2, "Regime", text_color = color.white, text_size = size.small)
    table.cell(statusTable, 1, 2, regime, text_color = regimeColor, text_size = size.small)

    table.cell(statusTable, 0, 3, "Bull Score", text_color = color.white, text_size = size.small)
    table.cell(statusTable, 1, 3, str.tostring(bullPoints) + " / 6", text_color = bullPoints >= 5 ? color.lime : bullPoints >= 3 ? color.white : color.gray, text_size = size.small)

    table.cell(statusTable, 0, 4, "Bear Score", text_color = color.white, text_size = size.small)
    table.cell(statusTable, 1, 4, str.tostring(bearPoints) + " / 6", text_color = bearPoints >= 5 ? color.red : bearPoints >= 3 ? color.white : color.gray, text_size = size.small)

    table.cell(statusTable, 0, 5, "Buyers", text_color = buyColorInput, text_size = size.small)
    table.cell(statusTable, 1, 5, buyBar + " " + str.tostring(int(math.round(buyRatio * 100))) + "%", text_color = buyColorInput, text_size = size.small)

    table.cell(statusTable, 0, 6, "Sellers", text_color = sellColorInput, text_size = size.small)
    table.cell(statusTable, 1, 6, sellBar + " " + str.tostring(int(math.round(sellRatio * 100))) + "%", text_color = sellColorInput, text_size = size.small)

    // ---- Everything below is the old detailed view, unchanged in content,
    // only shown when showDiagnosticsInput is on.
    if showDiagnosticsInput
        table.cell(statusTable, 0, 7, "Chart TF", text_color = color.white, text_size = size.tiny)
        table.cell(statusTable, 1, 7, timeframe.period, text_color = timeframe.period == "15" ? color.lime : color.orange, text_size = size.tiny)

        table.cell(statusTable, 0, 8, "Session", text_color = color.white, text_size = size.tiny)
        table.cell(statusTable, 1, 8, str.tostring(sessionIndex), text_color = color.white, text_size = size.tiny)

        table.cell(statusTable, 0, 9, "ATR(14) " + poiAtrTimeframeInput, text_color = color.white, text_size = size.tiny)
        table.cell(statusTable, 1, 9, str.tostring(atr15, format.mintick), text_color = color.white, text_size = size.tiny)

        table.cell(statusTable, 0, 10, "Active POIs", text_color = color.white, text_size = size.tiny)
        table.cell(statusTable, 1, 10, str.tostring(array.size(poiRegistry)) + " / " + str.tostring(maxActivePoisInput), text_color = color.white, text_size = size.tiny)

        int brokenCount = 0
        if array.size(poiRegistry) > 0
            for i = 0 to array.size(poiRegistry) - 1
                if array.get(poiRegistry, i).broken
                    brokenCount += 1

        table.cell(statusTable, 0, 11, "Broken", text_color = color.white, text_size = size.tiny)
        table.cell(statusTable, 1, 11, str.tostring(brokenCount), text_color = brokenCount > 0 ? color.lime : color.gray, text_size = size.tiny)

        table.cell(statusTable, 0, 12, "POI STATUS", text_color = color.white, bgcolor = color.rgb(22, 52, 86), text_size = size.tiny)
        table.cell(statusTable, 1, 12, "", text_color = color.white, bgcolor = color.rgb(22, 52, 86), text_size = size.tiny)

        int poiRowStart = 13
        int maxPoiRows = 3
        for r = 0 to maxPoiRows - 1
            int tableRow = poiRowStart + r
            if r < array.size(poiRegistry)
                Poi p = array.get(poiRegistry, r)
                string statusText = p.contaminated ? "DEAD" : (p.broken ? "BROKEN " + p.breakDirection : (not p.tested and (sessionIndex - p.createdSessionIndex) >= 1 ? "NAKED" : (p.tested ? "TESTED" : "FRESH")))
                table.cell(statusTable, 0, tableRow, p.poiType, text_color = color.white, text_size = size.tiny)
                table.cell(statusTable, 1, tableRow, str.tostring(p.price, format.mintick) + " " + statusText, text_color = p.contaminated ? color.gray : (p.broken ? (p.breakDirection == "UP" ? color.lime : color.red) : color.white), text_size = size.tiny)
            else
                table.cell(statusTable, 0, tableRow, "", text_color = color.white, text_size = size.tiny)
                table.cell(statusTable, 1, tableRow, "", text_color = color.white, text_size = size.tiny)

        table.cell(statusTable, 0, 16, "BREAK LOG", text_color = color.white, bgcolor = color.rgb(22, 52, 86), text_size = size.tiny)
        table.cell(statusTable, 1, 16, "", text_color = color.white, bgcolor = color.rgb(22, 52, 86), text_size = size.tiny)

        int logRowStart = 17
        int maxLogRows = 3
        for r = 0 to maxLogRows - 1
            int tableRow = logRowStart + r
            if r < array.size(breakLog)
                table.cell(statusTable, 0, tableRow, array.get(breakLog, r), text_color = color.white, text_halign = text.align_left, text_size = size.tiny)
                table.cell(statusTable, 1, tableRow, "", text_color = color.white, text_size = size.tiny)
            else
                table.cell(statusTable, 0, tableRow, "", text_color = color.white, text_size = size.tiny)
                table.cell(statusTable, 1, tableRow, "", text_color = color.white, text_size = size.tiny)

// ============================================================================
// DATA WINDOW
// ============================================================================

plot(barDelta, title = "Per-Bar Delta", display = display.data_window)
plot(atr15, title = "ATR(14) on POI Timeframe", display = display.data_window)
plot(array.size(poiRegistry), title = "Active POI Count", display = display.data_window)

Overview

A break can optionally require agreement from the bar delta, and it is tagged against 4-hour and 1-hour context for the dashboard only. It plots break events and places no orders. It pulls higher timeframe data.

Details

Published by

rosijay17-cmd on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Volume, Support & resistance, Breakout

Markets

Any symbol

Timeframes

M15-H1

Risk Notes

  • Reads higher-timeframe data with lookahead on, so the history can show signals that were not there in real time

Settings

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

SettingTypeDefaultVariable
Profile Rows (POI Extraction Granularity)int40profileRowsInput
Show Live Delta Profile (Price-Binned Histogram)booltrueshowLiveProfileInput
Histogram Lookback (Days)int1histogramLookbackDaysInput
Histogram Offset (Bars)int6histogramOffsetInput
Histogram Max Width (Bars)int30histogramWidthInput
Positive Delta Colorcolorcolor.tealpositiveDeltaColorInput
Negative Delta Colorcolorcolor.orangenegativeDeltaColorInput
POI Zone ATR Timeframetimeframe15poiAtrTimeframeInput
POI Zone Half-Width (x ATR)float0.25poiAtrMultInput
Contamination Threshold (Crossings)int3contaminationThresholdInput
POI Freshness (Sessions Untested)int2poiFreshnessSessionsInput
Minimum POI Separation (x ATR)float0.5minSeparationAtrMultInput
Show all 39 settings
SettingTypeDefaultVariable
Max Active POIsint6maxActivePoisInput
Extract Absorption NodebooltrueshowAbsorptionInput
Minimum Bars To Finalize A Sessionint10minEndedSessionBarsInput
Delta POC Colorcolorcolor.aquapocColorInput
Delta Extreme Colorcolorcolor.fuchsiaextremeColorInput
Delta Flip Colorcolorcolor.yellowflipColorInput
Absorption Node Colorcolorcolor.silverabsorptionColorInput
Minimum Break-Through Buffer (x ATR)float0.10minBreakAtrMultInput
Require Delta Confirmation On Break BarbooltruerequireDeltaConfirmationInput
Break Up Colorcolorcolor.limebreakUpColorInput
Break Down Colorcolorcolor.redbreakDownColorInput
4H Bias Timeframetimeframe240biasTimeframeInput
Swing Pivot Left Barsint3pivotLeftInput
Swing Pivot Right Barsint3pivotRightInput
4H EMA Lengthint50biasEmaLengthInput
4H EMA Slope Lookback (Bars)int5biasSlopeLookbackInput
1H Trend Timeframetimeframe60trendTimeframeInput
1H ADX / DI Lengthint14adxLengthInput
1H Minimum ADXfloat20.0adxMinimumInput
1H Fast EMAint21fastEmaInput
1H Slow EMAint50slowEmaInput
1H Efficiency Ratio Lengthint10erLengthInput
1H Minimum Efficiency Ratiofloat0.30erMinimumInput
Buyer/Seller Strength Window (Bars)int20deltaStrengthWindowInput
Buyer Strength Colorcolorcolor.tealbuyColorInput
Seller Strength Colorcolorcolor.orangesellColorInput
Show Detailed Diagnostics (POI / Break Log / Session Info)boolfalseshowDiagnosticsInput
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

13 stars

This script identifies and visualizes bullish and bearish order blocks on the price chart. It detects low-volatility consolidation ranges followed by breakout moves accompanied by high trading volume. Users can select between Simple, Relative, or Weighted volu

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 6 months ago · MIT GitHub repository of Advanced Order Blocks with Volume

This indicator identifies and plots price compression zones directly on the price chart using multi-module scoring. It measures ATR compression, ADX chop levels, boundary touches, volume dry-up, and historical pivot levels to identify consolidation areas and s

Indicator v6 BreakoutSupport & resistance
Updated 2 days ago · MIT GitHub repository of CoilForge Zones [WavesUnchained]

Nexural ORB

by JasonTeixeira
9 stars

This indicator plots opening range breakout levels across multiple timeframes, including five, fifteen, thirty, and sixty minutes, directly on the price chart. It also displays the session open and overnight high and low levels. The script calculates an openin

Indicator v6 BreakoutSessions & time
Updated 13 days ago · Custom licence GitHub repository of Nexural ORB

This indicator identifies dynamic support and resistance levels using pivot highs and lows. It plots resistance and support lines directly on the price chart once pivot points are confirmed. Additionally, it calculates a volume oscillator using 9-period and 26

Indicator v5 Support & resistanceSMC / ICT / price action
Updated 1 year ago · MPL-2.0 GitHub repository of Support & Resistance Levels with Break of Structure
2 stars

This overlay indicator approximates a Bookmap-style liquidity view from price and volume. It draws heatmap boxes where volume exceeds a rolling average, volume dots above a relative-volume filter, and a smoothed cumulative-delta proxy. Optional signals use abs

Indicator v6 VolumeSupport & resistance
Updated 6 months ago · None stated GitHub repository of Bookmap Heatmap & Order Flow

Equity Trading Strategy

by AmjadSaidam
2 stars

This indicator draws a daily-anchored volume-weighted average price on the price chart and marks the first long or short reclaim of that level. A long needs a bar below the average whose high reaches it, a later close above it, and confirmation after at least

Indicator v6 VolumeSupport & resistance Repaints
Updated 5 weeks ago · MIT GitHub repository of Equity Trading Strategy

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