An indicator meant for a 5-minute chart that recomputes a simplified 15-minute break of session delta levels, then watches a retest of the broken level within a set number of hours and flags a trigger candle, optionally needing delta confirmation.
//@version=6
// ============================================================================
// DELTA BREAK & RETEST -- STAGE 6: 5m RETEST & TRIGGER
// ----------------------------------------------------------------------------
// Per the build spec (Section 9), Stage 6: 5m execution timeframe -- watch a
// recently-broken 15m level for a retest, and flag the specific candle that
// confirms (or invalidates) it. Still no orders -- this stage plots/logs a
// TRIGGER event, it does not place a trade. Orders/risk is Stage 7.
//
// RUN THIS ON A 5m CHART. Per Section 2's hierarchy: 15m owns setup/POI/
// break (Stage 5); 5m owns execution/retest/trigger (this stage). The
// dashboard's Chart TF row checks for "5", not "15" -- that's intentional,
// not a leftover from Stage 5.
//
// ARCHITECTURE (read before touching this file): Stage 6 does NOT try to
// read Stage 5's live POI registry -- Pine indicators can't share state
// across separate script instances that way. Instead, per the same
// cross-timeframe idiom already proven in this repo (Stage 4/5's
// f_get4hContext()/f_get1hContext()), Stage 6 recomputes a SIMPLIFIED
// version of the 15m break logic fresh, inline, pulled via
// request.security() from whatever chart this indicator is actually
// applied to.
//
// DELIBERATE SIMPLIFICATION vs. Stage 5's POI registry (scope discipline --
// validate the retest/trigger MECHANICS first, on the simplest case, before
// building back the fuller registry semantics):
// - No contamination counter, no freshness expiry, no eviction, no cap.
// - Only the CURRENT (most recently extracted) price per POI type is
// tracked -- five independent slots (POC, Extreme+, Extreme-, Flip,
// Absorption), not a rotating multi-entry registry. A new session's
// extraction for a given type simply replaces that type's old level,
// whether or not the old one ever broke.
// - Only the MOST RECENT break across all five types is surfaced as the
// active retest candidate (via ta.valuewhen, same idiom as the pivot
// fix). If two types break on the exact same 15m bar, POC > Extreme+ >
// Extreme- > Flip > Absorption wins the tie -- a known, documented
// limitation, not a bug, and fine for validating the state machine.
//
// KNOWN RISK, flagged plainly per this repo's own established practice
// (same spirit as Stage 2's UDT warning): this is the first stage to
// combine per-bar `var` mutation with a request.security() pull returning
// a 5-value tuple. The individual pieces (var state inside a security-
// pulled function; ta.valuewhen + lookahead_on + per-value [1] offset for a
// multi-value tuple) are each independently proven elsewhere in this repo
// (Stage 4/5's MTF context, Stage 3's CooldownModule's var-state discipline),
// but not previously combined exactly this way. If it throws a compile
// error, send the exact message.
//
// DROPPED from this stage (deliberately, scope discipline -- this stage's
// job is retest/trigger, not re-polishing Stage 5's already-validated POI
// visualization): the price-binned box histogram. Stage 5 still has it, on
// its own 15m chart tab, if you want to reference both side by side. The
// Buyers/Sellers strength bar (native, no cross-timeframe pull needed) is
// carried forward unchanged since it's cheap and orthogonal to this.
//
// STILL NO TRADING LOGIC -- indicator only. A TRIGGERED state is a flagged
// candle, not an order. Orders/risk (position sizing, stop placement,
// actual strategy.entry calls) is Stage 7, not here.
// ============================================================================
indicator(title = "Delta Break & Retest - Stage 6 Retest & Trigger", shorttitle = "DBR Stage6", overlay = true, max_boxes_count = 50, max_labels_count = 100)
// ============================================================================
// 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))
// ============================================================================
// INPUTS
// ============================================================================
string breakContextTimeframeInput = input.timeframe("15", "15m Break-Context Timeframe", group = "15m Break Context", tooltip = "The timeframe Stage 5's POI/break logic normally runs on. Recomputed fresh here (simplified, see header) and pulled into this 5m script.")
int profileRowsInput = input.int(40, "Profile Rows (Extraction Granularity)", minval = 10, maxval = 120, group = "15m Break Context")
float poiAtrMultInput = input.float(0.25, "POI Zone Half-Width (x ATR)", minval = 0.05, maxval = 2.0, step = 0.05, group = "15m Break Context")
int minEndedSessionBarsInput = input.int(10, "Minimum Bars To Finalize A Session", minval = 3, maxval = 100, group = "15m Break Context")
bool showAbsorptionInput = input.bool(true, "Track Absorption Node", group = "15m Break Context")
float minBreakAtrMultInput = input.float(0.10, "Minimum Break-Through Buffer (x ATR)", minval = 0.0, maxval = 1.0, step = 0.05, group = "15m Break Context")
bool requireBreakDeltaConfirmationInput = input.bool(true, "Require Delta Confirmation On 15m Break Bar", group = "15m Break Context")
float retestWindowHoursInput = input.float(12.0, "Retest Window (Hours)", minval = 0.5, maxval = 168.0, step = 0.5, group = "Retest / Trigger", tooltip = "How long a broken level stays eligible for a retest before it's marked EXPIRED. Measured from the 15m break bar's own timestamp, not from bar count -- comparable across the 15m break context and this 5m chart.")
bool requireTriggerDeltaConfirmationInput = input.bool(true, "Require Delta Confirmation On 5m Trigger Bar", group = "Retest / Trigger")
color retestZoneColorInput = input.color(color.yellow, "Active Retest Zone Color", group = "Retest / Trigger")
color triggerUpColorInput = input.color(color.lime, "Trigger Up Color", group = "Retest / Trigger")
color triggerDownColorInput = input.color(color.red, "Trigger Down Color", group = "Retest / Trigger")
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")
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", group = "Signal Summary", tooltip = "Off by default -- adds Chart TF, active retest zone bounds/type, and time-since-break to the dashboard.")
// ============================================================================
// SESSION PROFILE EXTRACTION -- same algorithm as Stage 2/5's
// f_extractSessionPois(), unchanged. Called from inside the 15m break-
// context function below.
// ============================================================================
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]
// ============================================================================
// 15m BREAK CONTEXT -- simplified (see header): five independent "current
// level" slots, one break check each per bar, only the MOST RECENT break
// across all five surfaced via ta.valuewhen. Returns
// [zoneLow, zoneHigh, direction, typeCode, eventTimeMs], each offset [1]
// individually before return (multi-value tuple -- see the
// pine-proven-patterns skill's CE10123 note; the outer request.security
// call must NOT also apply [1]).
// direction: 1.0 = broke up, -1.0 = broke down, 0.0 = no event this bar.
// typeCode: 1=POC, 2=Extreme+, 3=Extreme-, 4=Flip, 5=Absorption.
// ============================================================================
f_get15mBreakContextRaw() =>
float atr15Local = ta.atr(14)
bool newSessionLocal = ta.change(time("D")) != 0
var int sessionStartBarLocal = bar_index
var int sessionIndexLocal = 0
int barsIntoSessionLocal = bar_index - sessionStartBarLocal + 1
var float pocLevel = na
var bool pocBroken = false
var float exHighLevel = na
var bool exHighBroken = false
var float exLowLevel = na
var bool exLowBroken = false
var float flipLevel = na
var bool flipBroken = false
var float absLevel = na
var bool absBroken = false
// DIAGNOSTIC (temporary, second probe): sessionIndexLocal proved var
// state advances fine, but Active Zone/Hours Since Break stayed dead --
// so the break somewhere between "extraction runs" and "an event
// reaches the outer script" is failing, not var-state-in-security
// generally. levelSetCount answers "does extraction ever actually
// produce a level" and breakEventCount answers "does the break
// condition itself ever actually fire" -- narrows which of those two
// links in the chain is broken.
var int levelSetCount = 0
var int breakEventCount = 0
// Declared here (not var, so still reset to the "no event" default
// every bar) so they're visible both to the mutation logic below AND
// to the ta.valuewhen calls that read them afterward.
// FIX (round 4): rounds 1-3 all failed, including round 3's raw
// (non-valuewhen) signal -- confirmed on-device running [r3], Break
// Events climbed to a real, growing count (proving events genuinely
// fire) while Active Zone stayed dead regardless. Re-examined every
// WORKING signal in this whole investigation side by side: ph/pl (the
// proven pivot pattern) default to na; close > open's valuewhen source
// effectively rode on a dense boolean, but the na-vs-0.0 asymmetry
// stood out as the one structural difference nothing else has tested
// in isolation. eventDirection/eventTypeCode were the only signals in
// this whole file using a real number (0.0) as their "nothing
// happened" baseline instead of na -- every proven-working signal uses
// na. Switched both to na, matching the pivot pattern exactly.
float eventDirection = na
float eventTypeCode = na
float eventZoneLow = na
float eventZoneHigh = na
// HISTORY: round 2 temporarily removed the "and not pocBroken" guard
// (etc.) from these conditions as a hypothesis test -- it wasn't the
// actual bug (round 6 found the real one, an unsafe na comparison in
// the outer script's event-adoption logic, nothing to do with these
// guards at all). Confirmed on [r6] on-device: with the guard removed,
// now-working retest detection fired on nearly every bar, since a
// level chopping back and forth re-fired a "break" on every fresh
// crossing. Restored the *Broken guards below -- a level only counts
// as broken once per session, matching Stage 5's original design and
// producing meaningful, sparse events instead of chop noise.
if newSessionLocal
if bar_index > 0 and barsIntoSessionLocal > 1
int endedBarsLocal = bar_index - sessionStartBarLocal
if endedBarsLocal >= minEndedSessionBarsInput and not na(atr15Local)
[pocPx, exHighPx, exLowPx, flipPx, absPx] = f_extractSessionPois(endedBarsLocal, profileRowsInput)
if not na(pocPx)
pocLevel := pocPx
pocBroken := false
levelSetCount += 1
if not na(exHighPx)
exHighLevel := exHighPx
exHighBroken := false
levelSetCount += 1
if not na(exLowPx)
exLowLevel := exLowPx
exLowBroken := false
levelSetCount += 1
if not na(flipPx)
flipLevel := flipPx
flipBroken := false
levelSetCount += 1
if showAbsorptionInput and not na(absPx)
absLevel := absPx
absBroken := false
levelSetCount += 1
sessionIndexLocal += 1
sessionStartBarLocal := bar_index
float localBarDelta = f_delta(open, high, low, close, nz(volume, 0.0))
float buffer = not na(atr15Local) ? atr15Local * minBreakAtrMultInput : 0.0
float halfWidth = not na(atr15Local) ? atr15Local * poiAtrMultInput : na
bool deltaOkUp = not requireBreakDeltaConfirmationInput or localBarDelta > 0
bool deltaOkDown = not requireBreakDeltaConfirmationInput or localBarDelta < 0
if not na(pocLevel) and not pocBroken and not na(halfWidth)
float zLow = pocLevel - halfWidth
float zHigh = pocLevel + halfWidth
bool brokeUp = close > zHigh + buffer and close[1] <= zHigh + buffer
bool brokeDown = close < zLow - buffer and close[1] >= zLow - buffer
if brokeUp and deltaOkUp
pocBroken := true
eventDirection := 1.0
eventTypeCode := 1.0
eventZoneLow := zLow
eventZoneHigh := zHigh
else if brokeDown and deltaOkDown
pocBroken := true
eventDirection := -1.0
eventTypeCode := 1.0
eventZoneLow := zLow
eventZoneHigh := zHigh
if not na(exHighLevel) and not exHighBroken and not na(halfWidth)
float zLow = exHighLevel - halfWidth
float zHigh = exHighLevel + halfWidth
bool brokeUp = close > zHigh + buffer and close[1] <= zHigh + buffer
bool brokeDown = close < zLow - buffer and close[1] >= zLow - buffer
if brokeUp and deltaOkUp
exHighBroken := true
eventDirection := 1.0
eventTypeCode := 2.0
eventZoneLow := zLow
eventZoneHigh := zHigh
else if brokeDown and deltaOkDown
exHighBroken := true
eventDirection := -1.0
eventTypeCode := 2.0
eventZoneLow := zLow
eventZoneHigh := zHigh
if not na(exLowLevel) and not exLowBroken and not na(halfWidth)
float zLow = exLowLevel - halfWidth
float zHigh = exLowLevel + halfWidth
bool brokeUp = close > zHigh + buffer and close[1] <= zHigh + buffer
bool brokeDown = close < zLow - buffer and close[1] >= zLow - buffer
if brokeUp and deltaOkUp
exLowBroken := true
eventDirection := 1.0
eventTypeCode := 3.0
eventZoneLow := zLow
eventZoneHigh := zHigh
else if brokeDown and deltaOkDown
exLowBroken := true
eventDirection := -1.0
eventTypeCode := 3.0
eventZoneLow := zLow
eventZoneHigh := zHigh
if not na(flipLevel) and not flipBroken and not na(halfWidth)
float zLow = flipLevel - halfWidth
float zHigh = flipLevel + halfWidth
bool brokeUp = close > zHigh + buffer and close[1] <= zHigh + buffer
bool brokeDown = close < zLow - buffer and close[1] >= zLow - buffer
if brokeUp and deltaOkUp
flipBroken := true
eventDirection := 1.0
eventTypeCode := 4.0
eventZoneLow := zLow
eventZoneHigh := zHigh
else if brokeDown and deltaOkDown
flipBroken := true
eventDirection := -1.0
eventTypeCode := 4.0
eventZoneLow := zLow
eventZoneHigh := zHigh
if showAbsorptionInput and not na(absLevel) and not absBroken and not na(halfWidth)
float zLow = absLevel - halfWidth
float zHigh = absLevel + halfWidth
bool brokeUp = close > zHigh + buffer and close[1] <= zHigh + buffer
bool brokeDown = close < zLow - buffer and close[1] >= zLow - buffer
if brokeUp and deltaOkUp
absBroken := true
eventDirection := 1.0
eventTypeCode := 5.0
eventZoneLow := zLow
eventZoneHigh := zHigh
else if brokeDown and deltaOkDown
absBroken := true
eventDirection := -1.0
eventTypeCode := 5.0
eventZoneLow := zLow
eventZoneHigh := zHigh
if not na(eventDirection)
breakEventCount += 1
// FIX (round 3): rounds 1 (barstate.isconfirmed) and 2 (remove the
// Broken-flag dependency) BOTH failed -- confirmed on-device, Break
// Events climbed to 616 (proving the round-2 change took effect and
// events fire constantly) while Active Zone/Hours Since Break stayed
// dead regardless. Two rounds of hypotheses about WHY ta.valuewhen
// won't surface this signal have now both been falsified. Rather than
// keep guessing at ta.valuewhen's behavior in this exact shape,
// sidestep it: don't aggregate/forward-fill inside this
// security-pulled function at all. Return the RAW, mostly-zero
// per-bar signal instead (0.0 / na on non-event bars, a real value
// only on the specific 15m bar an event happens), and let the outer
// 5m script do the "remember the last real event" job with its own
// var state -- which has worked reliably all session (Stage 3's
// CooldownModule, this file's own retest state machine). gaps_off
// will naturally repeat one 15m bar's raw value across its 3
// overlapping 5m bars; the outer script's existing
// "breakEventTime != activeEventTime" check already de-duplicates
// that correctly without any change needed there.
//
// sanityLatestTime (ta.valuewhen-based) is kept as an ONGOING
// reference point, not because it's needed for anything now, but so
// the dashboard keeps proving ta.valuewhen itself still works in
// general even while this function stops relying on it for the real
// signal.
float sanityLatestTime = ta.valuewhen(close > open, float(time), 0)
[eventZoneLow, eventZoneHigh, eventDirection, eventTypeCode, float(time), float(sessionIndexLocal), float(levelSetCount), float(breakEventCount), sanityLatestTime]
// FIX (round 5): rounds 1-4 all failed despite each targeting a real,
// falsifiable hypothesis. Found something structurally different by
// reading an actual proven MTF zone script already in this repo
// (Supply_and_Demand_Zones_XL.pine's f_structure_series() /
// f_structure_series_confirmed() pair) rather than continuing to guess:
// that script NEVER applies [1] to a local variable still sitting in the
// same scope as the mutation logic that produced it. It computes
// everything in a RAW function with no offset at all, then a SEPARATE
// wrapper function CALLS that raw function, destructures its returned
// tuple into brand-new local bindings, and only THEN applies [1] to
// those fresh bindings, in a completely different function scope. Every
// one of my four previous rounds applied [1] directly to variables still
// in the same scope as 5 nested if-blocks' worth of mutation. Replicating
// that exact two-layer split here.
f_get15mBreakContext() =>
[zoneLow, zoneHigh, direction, typeCode, eventTime, sessCount, lvlCount, evtCount, sanityTime] = f_get15mBreakContextRaw()
[zoneLow[1], zoneHigh[1], direction[1], typeCode[1], eventTime[1], sessCount[1], lvlCount[1], evtCount[1], sanityTime[1]]
[breakZoneLow, breakZoneHigh, breakDirectionRaw, breakTypeCodeRaw, breakEventTimeRaw, breakDiagSessionCountRaw, breakDiagLevelSetCountRaw, breakDiagEventCountRaw, breakDiagSanityTimeRaw] = request.security(syminfo.tickerid, breakContextTimeframeInput, f_get15mBreakContext(), gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_on)
int breakDiagSessionCount = na(breakDiagSessionCountRaw) ? na : int(breakDiagSessionCountRaw)
int breakDiagLevelSetCount = na(breakDiagLevelSetCountRaw) ? na : int(breakDiagLevelSetCountRaw)
int breakDiagEventCount = na(breakDiagEventCountRaw) ? na : int(breakDiagEventCountRaw)
int breakDiagSanityTime = na(breakDiagSanityTimeRaw) ? na : int(breakDiagSanityTimeRaw)
int breakDiagSanityHoursAgo = na(breakDiagSanityTime) ? na : int((time - breakDiagSanityTime) / (60 * 60 * 1000))
// breakDirectionRaw/breakTypeCodeRaw are now the RAW per-bar signal (0.0 on
// most pulls, a real value only on the specific bar an event happened) --
// not a continuously forward-filled "last known event" the way the
// ta.valuewhen version was. haveBrokenAny below means "did THIS pull
// carry a real event," not "has one ever happened by now."
bool haveBrokenAny = not na(breakDirectionRaw)
int breakDirection = haveBrokenAny ? int(breakDirectionRaw) : 0
int breakTypeCode = haveBrokenAny ? int(breakTypeCodeRaw) : 0
int breakEventTime = haveBrokenAny ? int(breakEventTimeRaw) : na
f_typeCodeName(int code) =>
code == 1 ? "Delta POC" : code == 2 ? "Delta Extreme +" : code == 3 ? "Delta Extreme -" : code == 4 ? "Delta Flip" : code == 5 ? "Absorption Node" : "?"
// ============================================================================
// 4H / 1H MTF CONTEXT -- carried forward unchanged from Stage 4/5, confirmed
// working on-device. Used for the Bias/Regime/Score dashboard rows and to
// tag a trigger as bias-aligned or not.
// ============================================================================
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 erOk = er1h >= erMinimumInput
bool emaStackBullish1h = emaFast1h > emaSlow1h
bool emaStackBearish1h = emaFast1h < emaSlow1h
// ============================================================================
// RETEST / TRIGGER STATE MACHINE (5m, native chart bars)
// ----------------------------------------------------------------------------
// A new 15m break event (breakEventTime changes) always adopts as the new
// active candidate, overriding whatever the previous one was doing -- only
// one candidate tracked at a time (see header, same simplification as the
// break context above). Retest window is time-based (hours since the break
// bar's own timestamp), not a bar-count countdown, per the cooldown
// module's proven no-countdown-variable discipline (Stage 3) -- comparable
// across the 15m break context and this 5m chart precisely because it's
// wall-clock time, not bar index.
// ============================================================================
var float activeZoneLow = na
var float activeZoneHigh = na
var int activeDirection = 0
var int activeTypeCode = 0
var int activeEventTime = na
var string retestState = "NONE"
var array<string> retestLog = array.new_string()
// FIX (round 6): rounds 1-5 all targeted the security-pull layer and all
// failed -- confirmed on-device through [r5], with 616+ real break events
// proven firing inside the pulled function the whole time. The actual bug
// was never there: it was HERE. Comparing a real value against a variable
// that starts as na using != is a classic Pine footgun -- na comparisons
// with ==/!= don't reliably behave like normal comparisons; na(x)/not na(x)
// is the only safe check. activeEventTime starts as na (line above), so on
// the very first break event ever, "breakEventTime != activeEventTime"
// most likely evaluated to na (falsy) instead of true -- meaning
// newBreakEvent never fired on that first event, activeEventTime stayed na
// forever, and therefore EVERY subsequent event hit the exact same
// still-na comparison, permanently. This explains why five rounds of
// fixing the security layer changed nothing: that layer was fine.
bool newBreakEvent = breakDirection != 0 and (na(activeEventTime) or breakEventTime != activeEventTime)
if newBreakEvent
activeZoneLow := breakZoneLow
activeZoneHigh := breakZoneHigh
activeDirection := breakDirection
activeTypeCode := breakTypeCode
activeEventTime := breakEventTime
retestState := "WATCHING"
array.unshift(retestLog, "bar" + str.tostring(bar_index) + " NEW " + f_typeCodeName(breakTypeCode) + " " + (breakDirection == 1 ? "UP" : "DOWN"))
if array.size(retestLog) > 8
array.pop(retestLog)
int hoursSinceEvent = na(activeEventTime) ? na : int((time - activeEventTime) / (60 * 60 * 1000))
bool windowExpired = not na(activeEventTime) and hoursSinceEvent > retestWindowHoursInput
if retestState == "WATCHING" or retestState == "RETESTED"
if windowExpired
retestState := "EXPIRED"
array.unshift(retestLog, "bar" + str.tostring(bar_index) + " EXPIRED " + f_typeCodeName(activeTypeCode))
if array.size(retestLog) > 8
array.pop(retestLog)
else
bool touchingZone = high >= activeZoneLow and low <= activeZoneHigh
if touchingZone and retestState == "WATCHING"
retestState := "RETESTED"
if retestState == "RETESTED"
bool triggerOkUp = not requireTriggerDeltaConfirmationInput or barDelta > 0
bool triggerOkDown = not requireTriggerDeltaConfirmationInput or barDelta < 0
bool triggerUp = activeDirection == 1 and close > activeZoneHigh and triggerOkUp
bool triggerDown = activeDirection == -1 and close < activeZoneLow and triggerOkDown
bool invalidateUp = activeDirection == 1 and close < activeZoneLow
bool invalidateDown = activeDirection == -1 and close > activeZoneHigh
if triggerUp or triggerDown
retestState := "TRIGGERED"
bool aligned = (triggerUp and bias == "LONG_ONLY") or (triggerDown and bias == "SHORT_ONLY")
label.new(bar_index, triggerUp ? activeZoneHigh : activeZoneLow, (triggerUp ? "▲▲ " : "▼▼ ") + f_typeCodeName(activeTypeCode) + " RETEST", style = triggerUp ? label.style_label_up : label.style_label_down, color = color.new(triggerUp ? triggerUpColorInput : triggerDownColorInput, aligned ? 0 : 55), textcolor = color.white, size = size.small)
array.unshift(retestLog, "bar" + str.tostring(bar_index) + " TRIGGERED " + f_typeCodeName(activeTypeCode) + " " + (triggerUp ? "UP" : "DOWN") + (aligned ? " (aligned)" : ""))
if array.size(retestLog) > 8
array.pop(retestLog)
else if invalidateUp or invalidateDown
retestState := "INVALIDATED"
array.unshift(retestLog, "bar" + str.tostring(bar_index) + " INVALIDATED " + f_typeCodeName(activeTypeCode))
if array.size(retestLog) > 8
array.pop(retestLog)
// ============================================================================
// ACTIVE ZONE VISUAL -- a single box tracking the current candidate only
// (not a registry), matching the "one candidate at a time" simplification.
// ============================================================================
var box activeZoneBox = na
if barstate.islast
if not na(activeZoneBox)
box.delete(activeZoneBox)
if retestState == "WATCHING" or retestState == "RETESTED"
color zoneDrawColor = retestState == "RETESTED" ? (activeDirection == 1 ? triggerUpColorInput : triggerDownColorInput) : retestZoneColorInput
activeZoneBox := box.new(left = bar_index - 40, top = activeZoneHigh, right = bar_index + 5, bottom = activeZoneLow, xloc = xloc.bar_index, bgcolor = color.new(zoneDrawColor, 85), border_color = zoneDrawColor)
// ============================================================================
// BUYER / SELLER STRENGTH (native 5m delta, unchanged concept from Stage 5)
// ============================================================================
// (computed inline inside the dashboard block below, same as Stage 5)
// ============================================================================
// DASHBOARD -- same compact philosophy as Stage 5: core rows always visible,
// detail behind a toggle. One new row added: Retest.
// ============================================================================
int coreRows = 8
int diagnosticRows = 8
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
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
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
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)
string retestDisplay = retestState == "NONE" ? "NONE" : retestState + " " + f_typeCodeName(activeTypeCode)
color retestColor = retestState == "TRIGGERED" ? (activeDirection == 1 ? color.lime : color.red) : retestState == "RETESTED" ? color.orange : retestState == "WATCHING" ? color.yellow : retestState == "INVALIDATED" ? color.gray : retestState == "EXPIRED" ? color.gray : color.gray
// Version tag on the ALWAYS-VISIBLE header row (not diagnostics-only)
// specifically so a screenshot settles "which round is this running"
// at a glance, without needing the diagnostics toggle on -- this
// exact question has cost several rounds already.
table.cell(statusTable, 0, 0, "DELTA BREAK & RETEST [r7]", 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)
table.cell(statusTable, 0, 7, "Retest", text_color = color.white, text_size = size.small)
table.cell(statusTable, 1, 7, retestDisplay, text_color = retestColor, text_size = size.small)
if showDiagnosticsInput
table.cell(statusTable, 0, 8, "Chart TF", text_color = color.white, text_size = size.tiny)
table.cell(statusTable, 1, 8, timeframe.period, text_color = timeframe.period == "5" ? color.lime : color.orange, text_size = size.tiny)
table.cell(statusTable, 0, 9, "Active Zone", text_color = color.white, text_size = size.tiny)
table.cell(statusTable, 1, 9, na(activeZoneLow) ? "--" : str.tostring(activeZoneLow, format.mintick) + " - " + str.tostring(activeZoneHigh, format.mintick), text_color = color.white, text_size = size.tiny)
table.cell(statusTable, 0, 10, "Hours Since Break", text_color = color.white, text_size = size.tiny)
table.cell(statusTable, 1, 10, na(hoursSinceEvent) ? "--" : str.tostring(hoursSinceEvent), text_color = color.white, text_size = size.tiny)
// DIAGNOSTIC (temporary): proves whether var mutation inside the
// security-pulled 15m function is advancing at all. If this stays
// "0 / na" forever despite plenty of loaded 15m history, that's the
// same dead-value signature as the original Stage 4 pivot bug, just
// for hand-rolled session-count state instead of pivot state -- and
// confirms the whole f_get15mBreakContext() approach needs the same
// kind of fix (rely on a stateful built-in, not hand-rolled var).
table.cell(statusTable, 0, 11, "15m Session # (diag)", text_color = color.white, text_size = size.tiny)
table.cell(statusTable, 1, 11, na(breakDiagSessionCount) ? "-- (dead)" : str.tostring(breakDiagSessionCount), text_color = na(breakDiagSessionCount) or breakDiagSessionCount == 0 ? color.orange : color.lime, text_size = size.tiny)
// DIAGNOSTIC (temporary): second probe, narrows the chain further.
// Levels Set = 0 means extraction never produces a real price for
// any of the 5 POI types (the bug is in f_extractSessionPois or how
// it's called). Levels Set > 0 but Break Events = 0 means levels
// exist but the close-vs-buffered-zone-edge condition never fires
// (the bug is in the break check itself). Both > 0 but Active Zone
// upstream still shows -- means the bug is in the ta.valuewhen /
// tuple-pull step specifically.
table.cell(statusTable, 0, 12, "Levels Set / Break Events (diag)", text_color = color.white, text_size = size.tiny)
table.cell(statusTable, 1, 12, (na(breakDiagLevelSetCount) ? "--" : str.tostring(breakDiagLevelSetCount)) + " / " + (na(breakDiagEventCount) ? "--" : str.tostring(breakDiagEventCount)), text_color = (nz(breakDiagLevelSetCount) > 0 and nz(breakDiagEventCount) > 0) ? color.lime : color.orange, text_size = size.tiny)
// DIAGNOSTIC (temporary, third probe): a trivial ta.valuewhen with
// no relation to break state at all (close > open, true on roughly
// half of all bars) pulled through the exact same [1] +
// lookahead_on chain. If this shows a real, small "hours ago"
// number, ta.valuewhen itself works fine here and the bug is
// specific to how eventDirection/eventZoneLow are built. If this
// ALSO shows dead, ta.valuewhen doesn't work reliably in this exact
// shape regardless of the condition -- contradicts the Stage 4
// pivot precedent and needs a bigger rethink.
table.cell(statusTable, 0, 13, "Sanity ValueWhen Hrs Ago (diag)", text_color = color.white, text_size = size.tiny)
table.cell(statusTable, 1, 13, na(breakDiagSanityHoursAgo) ? "-- (dead)" : str.tostring(breakDiagSanityHoursAgo), text_color = na(breakDiagSanityHoursAgo) ? color.orange : color.lime, text_size = size.tiny)
table.cell(statusTable, 0, 14, "RETEST LOG", text_color = color.white, bgcolor = color.rgb(22, 52, 86), text_size = size.tiny)
table.cell(statusTable, 1, 14, "", text_color = color.white, bgcolor = color.rgb(22, 52, 86), text_size = size.tiny)
if array.size(retestLog) > 0
table.cell(statusTable, 0, 15, array.get(retestLog, 0), text_color = color.white, text_halign = text.align_left, text_size = size.tiny)
table.cell(statusTable, 1, 15, "", text_color = color.white, text_size = size.tiny)
else
table.cell(statusTable, 0, 15, "", text_color = color.white, text_size = size.tiny)
table.cell(statusTable, 1, 15, "", text_color = color.white, text_size = size.tiny)
// ============================================================================
// DATA WINDOW
// ============================================================================
plot(barDelta, title = "Per-Bar Delta", display = display.data_window)
plot(activeDirection, title = "Active Retest Direction", display = display.data_window)
plot(retestState == "TRIGGERED" ? 1 : 0, title = "Triggered This State", display = display.data_window)
Overview
Delta Break and Retest Stage 6 Retest and Trigger also shows buyer and seller strength over a window and a dashboard with 4-hour and 1-hour context. A trigger is only a flagged candle, not an order. 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
15m Break-Context Timeframe
timeframe
15
breakContextTimeframeInput
Profile Rows (Extraction Granularity)
int
40
profileRowsInput
POI Zone Half-Width (x ATR)
float
0.25
poiAtrMultInput
Minimum Bars To Finalize A Session
int
10
minEndedSessionBarsInput
Track Absorption Node
bool
true
showAbsorptionInput
Minimum Break-Through Buffer (x ATR)
float
0.10
minBreakAtrMultInput
Require Delta Confirmation On 15m Break Bar
bool
true
requireBreakDeltaConfirmationInput
Retest Window (Hours)
float
12.0
retestWindowHoursInput
Require Delta Confirmation On 5m Trigger Bar
bool
true
requireTriggerDeltaConfirmationInput
Active Retest Zone Color
color
color.yellow
retestZoneColorInput
Trigger Up Color
color
color.lime
triggerUpColorInput
Trigger Down Color
color
color.red
triggerDownColorInput
Show all 28 settings
Setting
Type
Default
Variable
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
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