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.
//@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.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Profile Rows (POI Extraction Granularity)
int
40
profileRowsInput
Show Live Delta Profile (Price-Binned Histogram)
bool
true
showLiveProfileInput
Histogram Lookback (Days)
int
1
histogramLookbackDaysInput
Histogram Offset (Bars)
int
6
histogramOffsetInput
Histogram Max Width (Bars)
int
30
histogramWidthInput
Positive Delta Color
color
color.teal
positiveDeltaColorInput
Negative Delta Color
color
color.orange
negativeDeltaColorInput
POI Zone ATR Timeframe
timeframe
15
poiAtrTimeframeInput
POI Zone Half-Width (x ATR)
float
0.25
poiAtrMultInput
Contamination Threshold (Crossings)
int
3
contaminationThresholdInput
POI Freshness (Sessions Untested)
int
2
poiFreshnessSessionsInput
Minimum POI Separation (x ATR)
float
0.5
minSeparationAtrMultInput
Show all 39 settings
Setting
Type
Default
Variable
Max Active POIs
int
6
maxActivePoisInput
Extract Absorption Node
bool
true
showAbsorptionInput
Minimum Bars To Finalize A Session
int
10
minEndedSessionBarsInput
Delta POC Color
color
color.aqua
pocColorInput
Delta Extreme Color
color
color.fuchsia
extremeColorInput
Delta Flip Color
color
color.yellow
flipColorInput
Absorption Node Color
color
color.silver
absorptionColorInput
Minimum Break-Through Buffer (x ATR)
float
0.10
minBreakAtrMultInput
Require Delta Confirmation On Break Bar
bool
true
requireDeltaConfirmationInput
Break Up Color
color
color.lime
breakUpColorInput
Break Down Color
color
color.red
breakDownColorInput
4H Bias Timeframe
timeframe
240
biasTimeframeInput
Swing Pivot Left Bars
int
3
pivotLeftInput
Swing Pivot Right Bars
int
3
pivotRightInput
4H EMA Length
int
50
biasEmaLengthInput
4H EMA Slope Lookback (Bars)
int
5
biasSlopeLookbackInput
1H Trend Timeframe
timeframe
60
trendTimeframeInput
1H ADX / DI Length
int
14
adxLengthInput
1H Minimum ADX
float
20.0
adxMinimumInput
1H Fast EMA
int
21
fastEmaInput
1H Slow EMA
int
50
slowEmaInput
1H Efficiency Ratio Length
int
10
erLengthInput
1H Minimum Efficiency Ratio
float
0.30
erMinimumInput
Buyer/Seller Strength Window (Bars)
int
20
deltaStrengthWindowInput
Buyer Strength Color
color
color.teal
buyColorInput
Seller Strength Color
color
color.orange
sellColorInput
Show Detailed Diagnostics (POI / Break Log / Session Info)
bool
false
showDiagnosticsInput
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.
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
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
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
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
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
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