CrossLevels_v0.1.0.pine
Copy
// This source code is subject to the terms of the Mozilla Public License 2.0
// at https://mozilla.org/MPL/2.0/
// © 2026 TheStratSuite contributors
//@version=6
// Cross Levels translates price levels between ES futures and the cash market
// (SPY or SPX) and draws them on whichever chart is open:
//
// - ES chart: the cash market's killzone session highs/lows, opening prices
// and prior day/week/month levels, converted to ES prices, plus SPY
// overnight gap boxes.
// - SPY/SPX chart: ES levels converted to cash prices. The payoff is the
// overnight killzones (ASIA, LONDON) that the cash chart cannot see.
//
// Conversion runs through the basis (ES - SPX, or ES - SPY*10), measured on
// every chart bar where both symbols printed in step and frozen otherwise.
// Two conversion modes:
// - Projection (default): levels convert with the current basis, held
// through feed noise by the re-anchor threshold. The line marks where
// THIS chart will trade when the other symbol returns to its level -
// the liquidity read.
// - Historical: each level converts with the basis measured when its
// extreme printed (a per-level formation stamp), so lines sit on the
// other symbol's actual prints and never move afterward.
// Levels are stored in the SOURCE symbol's own prices; conversion happens at
// render time, and mitigation converts with the same numbers the render
// uses, so a level is always judged exactly where it is drawn.
//
// Known limitations
// -----------------
// - The basis freezes when the cash market is closed. An ES1! contract roll
// shifts the basis, and the frozen value is stale until the first RTH
// overlap bar after the roll.
// - In futures mode with a cash source, only NY-session killzones produce
// levels: the cash symbol has no overnight bars, so ASIA/LNDN stay empty.
// That is correct behavior, not a bug.
// - Chart history must include at least one RTH overlap bar before anything
// draws (the basis is na until then).
// - Prior day/week/month and killzone levels stay empty until the loaded
// history of the source symbol covers a full prior period or session. A
// partially observed period is never served: the level simply does not
// draw, it is never wrong.
indicator(
"Cross Levels (ES-SPY/SPX) - v0.1.0",
shorttitle = "Cross Levels",
overlay = true,
max_labels_count = 200,
max_lines_count = 200,
max_boxes_count = 200,
max_bars_back = 200)
import SpinTrades/PineDraw/2 as draw
// ============================================================================
// SECTION 1: INPUTS
// ============================================================================
// Suite conventions: group-name string constants, tooltips, inline rows for
// paired inputs. Inline rows carry one tooltip, on the row's last input, which
// is where TradingView renders the icon.
string G_GLOBAL = "Global"
string G_KZ = "Killzones"
string G_OPEN = "Opening Prices"
string G_DWM = "Day - Week - Month"
string G_GAP = "SPY Gaps"
string G_DISP = "Display"
string mode = input.string("Auto", "Mode",
options = ["Auto", "Futures chart", "Cash chart"],
group = G_GLOBAL,
tooltip = "Auto detects ES/MES roots as futures and SPY/SPX roots as cash. If the chart symbol is not recognized, nothing draws and the info table shows a warning; set the mode manually in that case. Forcing Cash chart on a symbol that is not SPY converts through the SPX basis unless the Cash level source is SPY.")
string futSym = input.symbol("CME_MINI:ES1!", "Futures symbol",
group = G_GLOBAL,
tooltip = "The futures leg of the basis, and the level source when a cash chart is open.")
string cashSource = input.string("SPY", "Cash level source",
options = ["SPY", "SPX"],
group = G_GLOBAL,
tooltip = "Which cash instrument's levels are drawn in futures mode. Gap boxes always come from the SPY symbol regardless of this choice.")
string spySym = input.symbol("AMEX:SPY", "SPY symbol",
group = G_GLOBAL,
tooltip = "Used for the SPY basis leg and always for gap boxes.")
string spxSym = input.symbol("SP:SPX", "SPX symbol",
group = G_GLOBAL,
tooltip = "Used for the SPX basis leg and for levels when the cash source is SPX.")
string convModeIn = input.string("Projection (liquidity)", "Conversion",
options = ["Projection (liquidity)", "Historical (pin to prints)"],
group = G_GLOBAL,
tooltip = "Projection converts every level with the current basis: the line marks where THIS chart will trade when the other symbol returns to its level - the liquidity read, and the default. Historical pins each level to the basis measured when its extreme actually printed, so lines sit on the other symbol's real prints (they align with wicks) but no longer mark where a retest will land. The difference between the two on any level is how far the basis has drifted since it formed.")
float basisReanchor = input.float(3.0, "Basis Re-anchor Backstop",
minval = 0, step = 0.25,
group = G_GLOBAL,
tooltip = "Projection mode only, in ES/index points. The held basis re-anchors on a schedule - once just after the 09:30 cash open and once in the last quarter hour before the close - so lines move at two predictable moments per session. This backstop additionally re-anchors immediately if the live basis drifts further than this from the held value (contract roll, rate shock). Ordinary intraday decay should never trip it; 0 tracks the live basis every bar.")
var bool projMode = convModeIn == "Projection (liquidity)"
string tz = input.string("America/New_York", "Timezone",
options = ['America/New_York', 'GMT-12', 'GMT-11', 'GMT-10', 'GMT-9', 'GMT-8', 'GMT-7', 'GMT-6', 'GMT-5', 'GMT-4', 'GMT-3', 'GMT-2', 'GMT-1', 'GMT+0', 'GMT+1', 'GMT+2', 'GMT+3', 'GMT+4', 'GMT+5', 'GMT+6', 'GMT+7', 'GMT+8', 'GMT+9', 'GMT+10', 'GMT+11', 'GMT+12', 'GMT+13', 'GMT+14'],
group = G_GLOBAL,
tooltip = "All session windows are interpreted in this timezone. Note GMT is not adjusted for Daylight Saving Time.")
string tfLimit = input.timeframe("30", "Timeframe Limit",
group = G_GLOBAL,
tooltip = "Drawings are suppressed on timeframes greater than this, and on all non-intraday timeframes.")
// Killzones: one inline row each - toggle, color, name, session. KZP defaults.
bool useKz1 = input.bool(true, "", inline = "KZ1", group = G_KZ)
color kz1Col = input.color(color.new(color.blue, 60), "", inline = "KZ1", group = G_KZ)
string kz1Name = input.string("ASIA", "", inline = "KZ1", group = G_KZ)
string kz1Sess = input.session("2000-0000", "", inline = "KZ1", group = G_KZ, tooltip = "Killzone 1: toggle, color, label, session window. Windows follow the Timezone input.")
bool useKz2 = input.bool(true, "", inline = "KZ2", group = G_KZ)
color kz2Col = input.color(color.new(color.red, 60), "", inline = "KZ2", group = G_KZ)
string kz2Name = input.string("LNDN", "", inline = "KZ2", group = G_KZ)
string kz2Sess = input.session("0200-0500", "", inline = "KZ2", group = G_KZ, tooltip = "Killzone 2. In futures mode with a cash source, overnight windows have no cash bars and draw nothing.")
bool useKz3 = input.bool(true, "", inline = "KZ3", group = G_KZ)
color kz3Col = input.color(color.new(color.teal, 60), "", inline = "KZ3", group = G_KZ)
string kz3Name = input.string("NYAM", "", inline = "KZ3", group = G_KZ)
string kz3Sess = input.session("0930-1100", "", inline = "KZ3", group = G_KZ, tooltip = "Killzone 3.")
bool useKz4 = input.bool(false, "", inline = "KZ4", group = G_KZ)
color kz4Col = input.color(color.new(color.orange, 60), "", inline = "KZ4", group = G_KZ)
string kz4Name = input.string("NYL", "", inline = "KZ4", group = G_KZ)
string kz4Sess = input.session("1200-1300", "", inline = "KZ4", group = G_KZ, tooltip = "Killzone 4.")
bool useKz5 = input.bool(true, "", inline = "KZ5", group = G_KZ)
color kz5Col = input.color(color.new(color.fuchsia, 60), "", inline = "KZ5", group = G_KZ)
string kz5Name = input.string("NYPM", "", inline = "KZ5", group = G_KZ)
string kz5Sess = input.session("1330-1600", "", inline = "KZ5", group = G_KZ, tooltip = "Killzone 5.")
bool useKz6 = input.bool(false, "", inline = "KZ6", group = G_KZ)
color kz6Col = input.color(color.new(color.black, 60), "", inline = "KZ6", group = G_KZ)
string kz6Name = input.string("RTH", "", inline = "KZ6", group = G_KZ)
string kz6Sess = input.session("0930-1600", "", inline = "KZ6", group = G_KZ, tooltip = "Killzone 6.")
// Opening prices: the +1-minute session trick captures the open of the first
// bar inside the window ("0930-0931" latches the 09:30 open).
bool useOp1 = input.bool(true, "", inline = "OP1", group = G_OPEN)
string op1Sess = input.session("1800-1801", "", inline = "OP1", group = G_OPEN)
color op1Col = input.color(color.blue, "", inline = "OP1", group = G_OPEN)
string op1Txt = input.string("Daily Open", "", inline = "OP1", group = G_OPEN, tooltip = "Open slot 1: toggle, one-minute window, color, label. The line holds the open of the window's first bar. 18:00 New York is the futures daily (Globex) open - the ES open a cash chart cannot see. A 09:30 cross-symbol open is redundant with the chart's own open, so it is not a default; set the window to 0930-0931 to bring it back. A regular-session cash source has no 18:00 bar, so on a futures chart this slot stays empty.")
bool useOp2 = input.bool(true, "", inline = "OP2", group = G_OPEN)
string op2Sess = input.session("1200-1201", "", inline = "OP2", group = G_OPEN)
color op2Col = input.color(color.orange, "", inline = "OP2", group = G_OPEN)
string op2Txt = input.string("Midday", "", inline = "OP2", group = G_OPEN, tooltip = "Open slot 2.")
bool useOp3 = input.bool(true, "", inline = "OP3", group = G_OPEN)
string op3Sess = input.session("1600-1601", "", inline = "OP3", group = G_OPEN)
color op3Col = input.color(color.red, "", inline = "OP3", group = G_OPEN)
string op3Txt = input.string("Close", "", inline = "OP3", group = G_OPEN, tooltip = "Open slot 3. On a regular-session cash source there is no 16:00 bar, so this slot only fills from a futures source.")
bool useDay = input.bool(true, "Day", inline = "DW1", group = G_DWM)
color dayCol = input.color(color.blue, "", inline = "DW1", group = G_DWM, tooltip = "Prior day high, low and midpoint from the source symbol. On SPY the prior-day range is RTH-only, which is exactly why it differs from the ES chart's own prior day.")
bool useWeek = input.bool(false, "Week", inline = "DW2", group = G_DWM)
color weekCol = input.color(#089981, "", inline = "DW2", group = G_DWM, tooltip = "Prior week high, low and midpoint. Stays empty until loaded history covers a full prior week (see header limitations).")
bool useMonth = input.bool(false, "Month", inline = "DW3", group = G_DWM)
color monthCol = input.color(color.red, "", inline = "DW3", group = G_DWM, tooltip = "Prior month high, low and midpoint. Stays empty until loaded history covers a full prior month (see header limitations).")
bool dwmMids = input.bool(false, "Midpoints", group = G_DWM, tooltip = "Midpoint line of each enabled prior period (PDM/PWM/PMM).")
bool showGaps = input.bool(true, "Show Gap Boxes",
group = G_GAP,
tooltip = "Futures mode only. Boxes span the prior SPY RTH close to the SPY RTH open, converted to ES prices, and extend until the gap fills.")
float minGapSize = input.float(0.1, "Min Gap Size (SPY pts)",
minval = 0, step = 0.05,
group = G_GAP,
tooltip = "Gaps smaller than this are ignored. SPY opens a few cents away from the prior close almost every session, so 0 draws a box every single day; 0.10 SPY (one ES point) keeps those micro-gaps out.")
int maxGaps = input.int(5, "Keep Last N Gaps",
minval = 1, maxval = 20,
group = G_GAP,
tooltip = "Only this many gap boxes are kept; the oldest is dropped first.")
color gapUpCol = input.color(color.new(#089981, 85), "Gap Up", inline = "GC", group = G_GAP)
color gapDnCol = input.color(color.new(#f23645, 85), "Gap Down", inline = "GC", group = G_GAP, tooltip = "Box colors.")
bool hideFilledGaps = input.bool(true, "Hide Filled Gaps",
group = G_GAP,
tooltip = "A gap that price has traded back through is spent - drop its box. Turn off to keep filled boxes dimmed and frozen at their fill bar instead.")
string labelSizeIn = input.string("Small", "Label Size",
options = ["Tiny", "Small", "Normal", "Large"],
group = G_DISP,
tooltip = "Size of all level labels.")
int lineWidthIn = input.int(1, "Line Width", minval = 1, maxval = 5, inline = "LN", group = G_DISP)
string lineStyleIn = input.string("Solid", "", options = ["Solid", "Dashed", "Dotted"], inline = "LN", group = G_DISP, tooltip = "Width for all lines, and style for completed killzone session lines.")
string openStyleIn = input.string("Dotted", "Opens", options = ["Solid", "Dashed", "Dotted"], inline = "LN2", group = G_DISP)
string dwmStyleIn = input.string("Dotted", "D/W/M", options = ["Solid", "Dashed", "Dotted"], inline = "LN2", group = G_DISP, tooltip = "Styles for opening-price lines and for prior day/week/month high/low lines. Both dotted by default, matching TheStrat Suite's open-line convention (midpoints are always dotted).")
int labelOffsetBars = input.int(10, "Label Offset (bars)",
minval = 1, maxval = 100,
group = G_DISP,
tooltip = "How many chart bars past the last bar the lines extend and the labels sit.")
bool debugOn = input.bool(false, "Debug Table",
group = G_DISP,
tooltip = "Diagnostic internals at middle left: the three served closes, freshness, raw vs held vs stamped basis, and one killzone's served-to-drawn chain. For chasing data problems (especially in bar replay); leave off otherwise.")
bool hideMitigated = input.bool(true, "Hide Mitigated Levels",
group = G_DISP,
tooltip = "A killzone or open level that price has already traded through since its session ended is spent - hide it entirely. Turn off to keep mitigated lines frozen at their mitigation bar, KZP-style. Prior day/week/month levels are reference levels and always stay for their full period.")
bool showTable = input.bool(true, "Show Info Table", inline = "TB", group = G_DISP)
string tablePosIn = input.string("Bottom Right", "",
options = ["Bottom Center", "Bottom Left", "Bottom Right", "Middle Center", "Middle Left", "Middle Right", "Top Center", "Top Left", "Top Right"],
inline = "TB", group = G_DISP,
tooltip = "Info table: mode and direction, current basis, and whether the basis is live or frozen.")
// ============================================================================
// SECTION 2: CHART MODE DETECTION
// ============================================================================
// Auto keys off syminfo.root: ES/MES roots are futures, a small honest list of
// cash roots is cash. Anything else draws nothing and warns.
bool rootIsFut = syminfo.root == "ES" or syminfo.root == "MES"
bool rootIsCash = syminfo.root == "SPY" or syminfo.root == "SPX" or syminfo.root == "SPX500USD" or syminfo.root == "US500"
bool futMode = mode == "Futures chart" or (mode == "Auto" and rootIsFut)
bool cashMode = mode == "Cash chart" or (mode == "Auto" and not rootIsFut and rootIsCash)
bool recognized = futMode or cashMode
bool cashIsSpy = cashSource == "SPY"
// Which scale the cash chart's prices are on. When Cash chart is forced on an
// unrecognized symbol, fall back to the Cash level source input as the hint.
bool chartIsSpy = rootIsCash ? syminfo.root == "SPY" : cashIsSpy
// The symbol whose levels we track. Futures mode reads the chosen cash
// instrument; cash mode reads the futures symbol. Unrecognized charts still
// request (requests must be unconditional) but never draw.
string srcSym = futMode ? (cashIsSpy ? spySym : spxSym) : futSym
// ============================================================================
// SECTION 3: SECURITY-CONTEXT TRACKERS
// ============================================================================
// These functions are evaluated INSIDE request.security, iterating the
// requested symbol's own bars at the chart timeframe. They are deliberately
// self-contained: parameters and function-local var state only, no reads of
// global mutable state. That is what makes them legal security expressions.
//
// Each caller below uses a distinct lexical call site, so every killzone /
// open slot / period gets its own var state and its own history buffers.
// Session membership is tracked with an explicit wasIn latch instead of
// time(...)[1], so no built-in history is shared across slots.
// One killzone: live (forming) session extremes while inside the window, and
// the latched extremes of the most recent completed session. Two guards on
// top of the wasIn latch: a window spanning the source's whole trading day
// never sees an out-of-session bar, so the served day roll also closes the
// session; and a run whose start predates loaded history is tainted and
// served as na instead of partial extremes with a wrong anchor.
kzTrack(string sess, string tzArg) =>
var float liveH = na
var float liveL = na
var int liveT = na
var float doneH = na
var float doneL = na
var int doneT = na
var bool wasIn = false
var bool firstBar = true
var bool tainted = false
bool inSess = not na(time(timeframe.period, sess, tzArg))
bool newDay = timeframe.change("D")
if inSess
if not wasIn or newDay
if wasIn
// Session ran continuously into a new served day (the window
// covers the source's full day): latch it, start a new run.
if not tainted
doneH := liveH
doneL := liveL
doneT := liveT
tainted := false
else
// History opened mid-session: this run's start was never
// observed, so its extremes must not be served.
tainted := firstBar
liveH := high
liveL := low
liveT := time
else
liveH := math.max(liveH, high)
liveL := math.min(liveL, low)
else if wasIn
// Session just ended: latch the completed values, clear the live ones.
if not tainted
doneH := liveH
doneL := liveL
doneT := liveT
tainted := false
liveH := na
liveL := na
liveT := na
firstBar := false
wasIn := inSess
[tainted ? na : liveH, tainted ? na : liveL, tainted ? na : liveT, doneH, doneL, doneT]
// All six killzones in one flat tuple (6 x 6 = 36 values, under the ~40 cap).
kzAll(string s1, string s2, string s3, string s4, string s5, string s6, string tzArg) =>
[lv1H, lv1L, lv1T, dn1H, dn1L, dn1T] = kzTrack(s1, tzArg)
[lv2H, lv2L, lv2T, dn2H, dn2L, dn2T] = kzTrack(s2, tzArg)
[lv3H, lv3L, lv3T, dn3H, dn3L, dn3T] = kzTrack(s3, tzArg)
[lv4H, lv4L, lv4T, dn4H, dn4L, dn4T] = kzTrack(s4, tzArg)
[lv5H, lv5L, lv5T, dn5H, dn5L, dn5T] = kzTrack(s5, tzArg)
[lv6H, lv6L, lv6T, dn6H, dn6L, dn6T] = kzTrack(s6, tzArg)
[lv1H, lv1L, lv1T, dn1H, dn1L, dn1T, lv2H, lv2L, lv2T, dn2H, dn2L, dn2T, lv3H, lv3L, lv3T, dn3H, dn3L, dn3T, lv4H, lv4L, lv4T, dn4H, dn4L, dn4T, lv5H, lv5L, lv5T, dn5H, dn5L, dn5T, lv6H, lv6L, lv6T, dn6H, dn6L, dn6T]
// One opening price: the open of the first bar inside a one-minute window,
// most recent occurrence. The day-roll guard mirrors kzTrack for consistency;
// a one-minute window cannot span a served day in practice.
openTrack(string sess, string tzArg) =>
var float oP = na
var int oT = na
var bool wasIn = false
bool inSess = not na(time(timeframe.period, sess, tzArg))
if inSess and (not wasIn or timeframe.change("D"))
oP := open
oT := time
wasIn := inSess
[oP, oT]
// Prior period high/low/start for one resolution, rolled on period change.
// Served only after the SECOND boundary: the first change latches a period
// whose start was never observed, so partial extremes stay na instead of
// drawing as a wrong prior-period level.
periodTrack(simple string res) =>
var float curH = na
var float curL = na
var int curT = na
var float prvH = na
var float prvL = na
var int prvT = na
var int changes = 0
if timeframe.change(res)
changes += 1
prvH := curH
prvL := curL
prvT := curT
curH := high
curL := low
curT := time
else
curH := na(curH) ? high : math.max(curH, high)
curL := na(curL) ? low : math.min(curL, low)
curT := na(curT) ? time : curT
bool ok = changes >= 2
// The forming period's extremes and start are served too: the chart side
// stamps the basis as each extreme prints (SECTION 7), and the forming
// period's identity keys the cash-close promotion - a cash source prints
// no bars overnight, so its served "prior day" stays one session stale
// from the 16:00 close until the next 09:30 bar without it.
[ok ? prvH : na, ok ? prvL : na, ok ? prvT : na, curH, curL, curT]
// Opens and prior day/week/month in one flat tuple (3x2 + 3x6 = 24 values).
openDwmAll(string s1, string s2, string s3, string tzArg) =>
[o1P, o1T] = openTrack(s1, tzArg)
[o2P, o2T] = openTrack(s2, tzArg)
[o3P, o3T] = openTrack(s3, tzArg)
[dH, dL, dT, dCH, dCL, dCT] = periodTrack("D")
[wH, wL, wT, wCH, wCL, wCT] = periodTrack("W")
[mH, mL, mT, mCH, mCL, mCT] = periodTrack("M")
[o1P, o1T, o2P, o2T, o3P, o3T, dH, dL, dT, dCH, dCL, dCT, wH, wL, wT, wCH, wCL, wCT, mH, mL, mT, mCH, mCL, mCT]
// SPY overnight gap data: the close of the last bar of the prior RTH session
// and the open of the first bar of the current one. A regular-session SPY
// feed has no out-of-session bars, so the wasIn edge alone never re-arms;
// the served day roll detects each new session too. prevClose latches only
// at session start, from bars that are already final, so it is reload-stable
// (na until a full prior session has been observed - never a fabricated gap).
gapTrack(string tzArg) =>
var float prevClose = na
var int prevCloseT = na
var float rthOpen = na
var int rthOpenT = na
var float lastClose = na
var int lastCloseT = na
var bool wasIn = false
bool inRth = not na(time(timeframe.period, "0930-1600", tzArg))
if inRth and (not wasIn or timeframe.change("D"))
prevClose := lastClose
prevCloseT := lastCloseT
rthOpen := open
rthOpenT := time
if inRth
lastClose := close
lastCloseT := time
wasIn := inRth
[prevClose, prevCloseT, rthOpen, rthOpenT]
// ============================================================================
// SECTION 4: DATA REQUESTS
// ============================================================================
// All six calls run unconditionally at global scope on every bar. These are
// chart-timeframe requests of ANOTHER symbol, not HTF requests, so
// lookahead_off is correct here; the [1]+lookahead_on rule applies to HTF
// requests only. No calc_bars_count cap: the expressions carry var state,
// and a capped window rebuilds that state on reload from a different bar
// range than the one accumulated live, breaking reload stability.
// Call 1: killzones from the level source (36-value tuple).
[kz1LvH, kz1LvL, kz1LvT, kz1DnH, kz1DnL, kz1DnT, kz2LvH, kz2LvL, kz2LvT, kz2DnH, kz2DnL, kz2DnT, kz3LvH, kz3LvL, kz3LvT, kz3DnH, kz3DnL, kz3DnT, kz4LvH, kz4LvL, kz4LvT, kz4DnH, kz4DnL, kz4DnT, kz5LvH, kz5LvL, kz5LvT, kz5DnH, kz5DnL, kz5DnT, kz6LvH, kz6LvL, kz6LvT, kz6DnH, kz6DnL, kz6DnT] = request.security(srcSym, timeframe.period, kzAll(kz1Sess, kz2Sess, kz3Sess, kz4Sess, kz5Sess, kz6Sess, tz), lookahead = barmerge.lookahead_off)
// Call 2: opening prices and prior day/week/month from the level source
// (24-value tuple; split from call 1 to stay under the tuple cap).
[op1P, op1T, op2P, op2T, op3P, op3T, pdH, pdL, pdT, pdCH, pdCL, pdCT, pwH, pwL, pwT, pwCH, pwCL, pwCT, pmH, pmL, pmT, pmCH, pmCL, pmCT] = request.security(srcSym, timeframe.period, openDwmAll(op1Sess, op2Sess, op3Sess, tz), lookahead = barmerge.lookahead_off)
// Call 3: gap data, always from the SPY symbol (4-value tuple).
[gPrvC, gPrvCT, gRthO, gRthOT] = request.security(spySym, timeframe.period, gapTrack(tz), lookahead = barmerge.lookahead_off)
// Calls 4-6: basis freshness legs. gaps_on means a non-na value only when
// that symbol printed a bar closing in step with this chart bar. Each leg
// carries its bar's close TIME as well (FIX BASIS-PAIR-1, 2026-08-12): a
// basis is only as good as its pairing, and in bar replay the cross-symbol
// feed can lag the chart bar, silently pairing this bar's close with the
// other symbol's PREVIOUS close - the basis then swings by the bar's full
// range. Timestamps let SECTION 5 verify both legs closed the same bar
// before pairing them; a mispaired bar simply holds the last verified basis.
[esCloseG, esTcG] = request.security(futSym, timeframe.period, [close, time_close], gaps = barmerge.gaps_on, lookahead = barmerge.lookahead_off)
[spyCloseG, spyTcG] = request.security(spySym, timeframe.period, [close, time_close], gaps = barmerge.gaps_on, lookahead = barmerge.lookahead_off)
[spxCloseG, spxTcG] = request.security(spxSym, timeframe.period, [close, time_close], gaps = barmerge.gaps_on, lookahead = barmerge.lookahead_off)
// ============================================================================
// SECTION 5: BASIS ENGINE
// ============================================================================
// Two mappings, both frozen at the last overlap bar when the other market is
// closed. The SPY basis is a x10 scaled offset on purpose: it preserves range
// distances (1 SPY point = 10 ES points) and matches how traders quote the
// spread. Until the first fresh overlap bar in loaded history, both are na and
// nothing that depends on them draws.
var float basisSpy10 = na
var float basisSpx = na
var int spyFreshCloseT = na // scheduled close of the last SPY-fresh bar
var int spxFreshCloseT = na
// A pair is fresh only when both legs closed the SAME bar (FIX BASIS-PAIR-1);
// price equality across mismatched timestamps is how a basis goes wrong.
// FIX BASIS-CONF-1 (2026-08-13): every basis mutation below - raw updates,
// scheduled anchors, the drift backstop - runs on confirmed bars only. On
// the realtime bar the legs serve DEVELOPING values tick by tick, and the
// first prints after the cash open swing the raw basis enough to trip
// anchors intrabar, re-pricing every line on ticks. Confirmed-only means
// intrabar can re-render but never re-price, and live matches reload.
bool spyPairFresh = barstate.isconfirmed and not na(esCloseG) and not na(spyCloseG) and esTcG == spyTcG
bool spxPairFresh = barstate.isconfirmed and not na(esCloseG) and not na(spxCloseG) and esTcG == spxTcG
if spyPairFresh
basisSpy10 := esCloseG - spyCloseG * 10
spyFreshCloseT := spyTcG
if spxPairFresh
basisSpx := esCloseG - spxCloseG
spxFreshCloseT := spxTcG
// Two conversion modes (FIX BASIS-FORM-1, 2026-08-12):
//
// PROJECTION (default) marks where THIS chart will trade when the other
// symbol returns to its level - the liquidity read. It converts through a
// held copy of the current basis that re-anchors only when the live value
// drifts past the threshold (FIX BASIS-JITTER-1: SPY and ES closes are not
// synchronized prints, so the raw basis wobbles a fraction of a point bar to
// bar and unheld lines re-price on every bar).
//
// HISTORICAL pins each level to the basis measured when its extreme printed
// (per-level stamps taken in SECTION 7), so lines land on the other symbol's
// actual prints and never move afterward - at the cost of no longer marking
// where a retest will land.
// FIX BASIS-SCHED-1 (2026-08-12): drift-only re-anchoring made the stack
// step every few bars whenever the basis decays directionally (worst near a
// roll). The held basis now re-anchors on a SCHEDULE - the first overlap bar
// after the 09:30 cash open and the first bar in the last quarter hour
// before the 16:00 close - so projection lines move at two predictable
// moments per session. The drift threshold remains only as a backstop for
// regime breaks (contract roll, rate shock) that cannot wait for the next
// scheduled anchor.
var float basisSpy10R = na
var float basisSpxR = na
bool inAnchorAM = not na(time(timeframe.period, "0930-0945", tz))
bool inAnchorPM = not na(time(timeframe.period, "1545-1600", tz))
int dayKey = year(time, tz) * 10000 + month(time, tz) * 100 + dayofmonth(time, tz)
var int amDone = 0
var int pmDone = 0
bool schedAnchor = (inAnchorAM and amDone != dayKey) or (inAnchorPM and pmDone != dayKey)
if schedAnchor and (spyPairFresh or spxPairFresh)
if inAnchorAM
amDone := dayKey
if inAnchorPM
pmDone := dayKey
if spyPairFresh
basisSpy10R := basisSpy10
if spxPairFresh
basisSpxR := basisSpx
if not na(basisSpy10) and (na(basisSpy10R) or math.abs(basisSpy10 - basisSpy10R) > basisReanchor)
basisSpy10R := basisSpy10
if not na(basisSpx) and (na(basisSpxR) or math.abs(basisSpx - basisSpxR) > basisReanchor)
basisSpxR := basisSpx
// Mode-resolved raw basis on this bar: what formation stamps record, and the
// fallback for levels whose extremes printed before the first overlap bar in
// loaded history. projBasis is its held counterpart for Projection mode.
float curBasis = futMode ? (cashIsSpy ? basisSpy10 : basisSpx) : cashMode ? (chartIsSpy ? basisSpy10 : basisSpx) : float(na)
float projBasis = futMode ? (cashIsSpy ? basisSpy10R : basisSpxR) : cashMode ? (chartIsSpy ? basisSpy10R : basisSpxR) : float(na)
// ============================================================================
// SECTION 6: CONVERSION
// ============================================================================
// Source-symbol price to chart price. Converted values round through the chart
// mintick before drawing and before any same-price comparison; raw floats are
// never compared. Returns na while the required basis is na or the chart is
// unrecognized, which is what keeps creation conditional.
// p is a source-symbol price; b is its formation-time stamp (used only in
// Historical mode, with the current basis as fallback for pre-history
// stamps). Projection mode ignores the stamp and uses the held basis.
convToChart(float p, float b) =>
float bb = projMode ? projBasis : na(b) ? curBasis : b
float r = na
if futMode
r := cashIsSpy ? p * 10 + bb : p + bb
else if cashMode
r := chartIsSpy ? (p - bb) / 10 : p - bb
math.round_to_mintick(r)
// Gap boxes always convert SPY prices to ES, regardless of the cash source.
convSpyToEs(float p, float b) =>
float bb = projMode ? basisSpy10R : na(b) ? basisSpy10 : b
math.round_to_mintick(p * 10 + bb)
// ============================================================================
// SECTION 7: PER-BAR STATE - MITIGATION LATCHES AND GAP CAPTURE
// ============================================================================
// Runs on every bar so a reload rebuilds every latch from bar history. The
// latches are monotone (once mitigated / filled, stays so for that level's
// lifetime) and may therefore read live [0] chart data. Formation-time basis
// stamps are the one snapshot-like state here, so they latch on confirmed
// bars only (identical live and on reload). Mitigation converts with the
// same stamps and mode the render uses, so a level is judged exactly where
// it is drawn.
// Served values as fixed-size arrays for loop processing.
// FIX PERF-ALLOC-1 (2026-08-13): these were rebuilt with array.from on EVERY
// bar - fourteen allocations per bar across the file, over half a million on
// a fully loaded chart, which is what pushed total runtime toward the
// platform's 20-second cap. Allocated once now, refilled in place.
set6f(array<float> a, float v1, float v2, float v3, float v4, float v5, float v6) =>
array.set(a, 0, v1)
array.set(a, 1, v2)
array.set(a, 2, v3)
array.set(a, 3, v4)
array.set(a, 4, v5)
array.set(a, 5, v6)
set6i(array<int> a, int v1, int v2, int v3, int v4, int v5, int v6) =>
array.set(a, 0, v1)
array.set(a, 1, v2)
array.set(a, 2, v3)
array.set(a, 3, v4)
array.set(a, 4, v5)
array.set(a, 5, v6)
set3f(array<float> a, float v1, float v2, float v3) =>
array.set(a, 0, v1)
array.set(a, 1, v2)
array.set(a, 2, v3)
set3i(array<int> a, int v1, int v2, int v3) =>
array.set(a, 0, v1)
array.set(a, 1, v2)
array.set(a, 2, v3)
var array<float> kzLvHArr = array.new<float>(6)
var array<float> kzLvLArr = array.new<float>(6)
var array<int> kzLvTArr = array.new<int>(6)
var array<float> kzDnHArr = array.new<float>(6)
var array<float> kzDnLArr = array.new<float>(6)
var array<int> kzDnTArr = array.new<int>(6)
var array<float> opPArr = array.new<float>(3)
var array<int> opTArr = array.new<int>(3)
var array<float> dwmHArr = array.new<float>(3)
var array<float> dwmLArr = array.new<float>(3)
var array<int> dwmTArr = array.new<int>(3)
set6f(kzLvHArr, kz1LvH, kz2LvH, kz3LvH, kz4LvH, kz5LvH, kz6LvH)
set6f(kzLvLArr, kz1LvL, kz2LvL, kz3LvL, kz4LvL, kz5LvL, kz6LvL)
set6i(kzLvTArr, kz1LvT, kz2LvT, kz3LvT, kz4LvT, kz5LvT, kz6LvT)
set6f(kzDnHArr, kz1DnH, kz2DnH, kz3DnH, kz4DnH, kz5DnH, kz6DnH)
set6f(kzDnLArr, kz1DnL, kz2DnL, kz3DnL, kz4DnL, kz5DnL, kz6DnL)
set6i(kzDnTArr, kz1DnT, kz2DnT, kz3DnT, kz4DnT, kz5DnT, kz6DnT)
set3f(opPArr, op1P, op2P, op3P)
set3i(opTArr, op1T, op2T, op3T)
set3f(dwmHArr, pdH, pwH, pmH)
set3f(dwmLArr, pdL, pwL, pmL)
set3i(dwmTArr, pdT, pwT, pmT)
// Input-derived per-slot config (inputs are constant, so var + build once).
var array<bool> kzOnArr = array.from(useKz1, useKz2, useKz3, useKz4, useKz5, useKz6)
var array<color> kzColArr = array.from(kz1Col, kz2Col, kz3Col, kz4Col, kz5Col, kz6Col)
var array<string> kzNameArr = array.from(kz1Name, kz2Name, kz3Name, kz4Name, kz5Name, kz6Name)
var array<string> kzSessArr = array.from(kz1Sess, kz2Sess, kz3Sess, kz4Sess, kz5Sess, kz6Sess)
var array<bool> opOnArr = array.from(useOp1, useOp2, useOp3)
var array<color> opColArr = array.from(op1Col, op2Col, op3Col)
var array<string> opTxtArr = array.from(op1Txt, op2Txt, op3Txt)
var array<bool> dwmOnArr = array.from(useDay, useWeek, useMonth)
var array<color> dwmColArr = array.from(dayCol, weekCol, monthCol)
var array<string> dwmNmArr = array.from("PD", "PW", "PM")
// Chart extremes, mintick-rounded once per bar for every comparison below.
float chHi = math.round_to_mintick(high)
float chLo = math.round_to_mintick(low)
// --- Killzone mitigation: strict break, KZP semantics (high above a high
// level, low below a low level). Latches reset when a new session completes,
// keyed on the completed session's start time. 0 = no session seen yet.
// A window ending where the source prints no next bar (cash source, window
// ending at the 16:00 cash close) latches source-side only at the next
// morning's bar, so the chart clock closes it here: when the CHART exits the
// window while the served tuple still reports live values, those values are
// promoted to the effective completed session and mitigation arms
// immediately. The served done tuple arrives later carrying that same start
// time as its key, so it does not wipe anything latched in between. The
// kzEff* copies are the single completed-session source of truth for both
// mitigation and render, and they rebuild from served series plus chart bars
// on reload.
var array<int> kzKeyT = array.new<int>(6, 0)
var array<bool> kzHiMit = array.new<bool>(6, false)
var array<int> kzHiMitT = array.new<int>(6, 0)
var array<bool> kzLoMit = array.new<bool>(6, false)
var array<int> kzLoMitT = array.new<int>(6, 0)
var array<bool> kzChIn = array.new<bool>(6, false)
var array<float> kzEffDnH = array.new<float>(6)
var array<float> kzEffDnL = array.new<float>(6)
var array<int> kzEffDnT = array.new<int>(6)
// Formation-time basis stamps (Historical mode). While a session is live,
// each extension of its high or low re-stamps that side with the basis on
// the bar the extreme printed; the stamps ride along when the session
// completes (served rollover or chart-side promotion below). Extremes that
// print while this chart has no bars stamp at the next chart bar - the
// closest measurement the chart can observe. All state rebuilds bar by bar
// on reload.
var array<float> kzLvBH = array.new<float>(6)
var array<float> kzLvBL = array.new<float>(6)
var array<float> kzLvPrvH = array.new<float>(6)
var array<float> kzLvPrvL = array.new<float>(6)
var array<int> kzLvKey = array.new<int>(6, 0)
var array<float> kzEffBH = array.new<float>(6)
var array<float> kzEffBL = array.new<float>(6)
// Stamps update on confirmed bars only, so a reload rebuilds byte-identical
// stamps from the same bar closes the realtime pass latched.
if barstate.isconfirmed
for i = 0 to 5
float lvH = array.get(kzLvHArr, i)
float lvL = array.get(kzLvLArr, i)
int lvT = array.get(kzLvTArr, i)
if not na(lvT)
if lvT != array.get(kzLvKey, i)
array.set(kzLvKey, i, lvT)
array.set(kzLvBH, i, curBasis)
array.set(kzLvBL, i, curBasis)
else
if not na(lvH) and lvH != array.get(kzLvPrvH, i)
array.set(kzLvBH, i, curBasis)
if not na(lvL) and lvL != array.get(kzLvPrvL, i)
array.set(kzLvBL, i, curBasis)
array.set(kzLvPrvH, i, lvH)
array.set(kzLvPrvL, i, lvL)
for i = 0 to 5
int dT = array.get(kzDnTArr, i)
if not na(dT) and dT != array.get(kzKeyT, i)
array.set(kzKeyT, i, dT)
array.set(kzEffDnH, i, array.get(kzDnHArr, i))
array.set(kzEffDnL, i, array.get(kzDnLArr, i))
array.set(kzEffDnT, i, dT)
array.set(kzEffBH, i, array.get(kzLvBH, i))
array.set(kzEffBL, i, array.get(kzLvBL, i))
array.set(kzHiMit, i, false)
array.set(kzHiMitT, i, 0)
array.set(kzLoMit, i, false)
array.set(kzLoMitT, i, 0)
// Session-window checks only matter for the cash-close promotion, which
// only a cash SOURCE needs (futures mode) - skip six time() parses per
// bar everywhere else (FIX PERF-ALLOC-1).
bool chIn = false
if futMode
chIn := not na(time(timeframe.period, array.get(kzSessArr, i), tz))
if not chIn and array.get(kzChIn, i)
// Chart clock just exited the window. Promote a still-live served
// session (non-na live tuple whose start is not already the key);
// sessions the source has no bars for (ASIA/LNDN on a cash source)
// serve na live values, so promotion is a no-op there.
float lvH = array.get(kzLvHArr, i)
float lvL = array.get(kzLvLArr, i)
int lvT = array.get(kzLvTArr, i)
if not na(lvH) and not na(lvL) and not na(lvT) and lvT != array.get(kzKeyT, i)
array.set(kzKeyT, i, lvT)
array.set(kzEffDnH, i, lvH)
array.set(kzEffDnL, i, lvL)
array.set(kzEffDnT, i, lvT)
array.set(kzEffBH, i, array.get(kzLvBH, i))
array.set(kzEffBL, i, array.get(kzLvBL, i))
array.set(kzHiMit, i, false)
array.set(kzHiMitT, i, 0)
array.set(kzLoMit, i, false)
array.set(kzLoMitT, i, 0)
array.set(kzChIn, i, chIn)
float lvlH = convToChart(array.get(kzEffDnH, i), array.get(kzEffBH, i))
float lvlL = convToChart(array.get(kzEffDnL, i), array.get(kzEffBL, i))
if not na(lvlH) and not array.get(kzHiMit, i) and chHi > lvlH
array.set(kzHiMit, i, true)
array.set(kzHiMitT, i, time)
if not na(lvlL) and not array.get(kzLoMit, i) and chLo < lvlL
array.set(kzLoMit, i, true)
array.set(kzLoMitT, i, time)
// --- Open-price mitigation: price sits AT the level on its capture bar, so
// the capture bar is excluded and any later bar that trades through the level
// (level inside the bar's range) mitigates it.
var array<int> opKeyT = array.new<int>(3, 0)
var array<bool> opMit = array.new<bool>(3, false)
var array<int> opMitT = array.new<int>(3, 0)
var array<float> opB = array.new<float>(3)
for i = 0 to 2
int oT = array.get(opTArr, i)
if barstate.isconfirmed and not na(oT) and oT != array.get(opKeyT, i)
array.set(opKeyT, i, oT)
array.set(opMit, i, false)
array.set(opMitT, i, 0)
// An open prints on its capture bar, so the capture-bar basis IS its
// formation-time stamp.
array.set(opB, i, curBasis)
float lvl = convToChart(array.get(opPArr, i), array.get(opB, i))
if not na(lvl) and not na(oT) and not array.get(opMit, i) and time > oT and chLo <= lvl and chHi >= lvl
array.set(opMit, i, true)
array.set(opMitT, i, time)
// --- Prior day/week/month: effective copies, basis stamps, and the cash-
// close promotion. The served "prior period" only rolls when the source
// prints the first bar of a new period, so a cash source's prior day stays
// one session stale for the whole overnight (its day rolls at 09:30, while
// the futures chart keeps trading). FIX DWM-PROMOTE-1 (2026-08-13): when the
// chart clock exits the cash RTH window and the source's forming day is
// complete-but-unrolled, that day is promoted to the effective prior day,
// keyed on its start time so the real 09:30 roll wipes nothing. Day slot
// only: promoting a week or month would need to know the source's last
// session of the period, and both are off by default. Stamps: the forming
// period's extremes are watched from the served tuple and stamped as each
// prints; on roll or promotion the forming stamps become the prior stamps.
// The midpoint converts with the average of the two.
var array<float> dwmCHArr = array.new<float>(3)
var array<float> dwmCLArr = array.new<float>(3)
var array<int> dwmCTArr = array.new<int>(3)
set3f(dwmCHArr, pdCH, pwCH, pmCH)
set3f(dwmCLArr, pdCL, pwCL, pmCL)
set3i(dwmCTArr, pdCT, pwCT, pmCT)
var array<int> dwmKeyT = array.new<int>(3, 0)
var array<int> dwmCurKey = array.new<int>(3, 0)
var array<float> dwmEffH = array.new<float>(3)
var array<float> dwmEffL = array.new<float>(3)
var array<int> dwmEffT = array.new<int>(3)
var array<float> dwmBH = array.new<float>(3)
var array<float> dwmBL = array.new<float>(3)
var array<float> dwmCurBH = array.new<float>(3)
var array<float> dwmCurBL = array.new<float>(3)
var array<float> dwmPrvCH = array.new<float>(3)
var array<float> dwmPrvCL = array.new<float>(3)
var bool dwmRthPrev = false
if barstate.isconfirmed
for i = 0 to 2
int pT = array.get(dwmTArr, i)
if not na(pT) and pT != array.get(dwmKeyT, i)
// Served roll: first source bar of a new period.
array.set(dwmKeyT, i, pT)
array.set(dwmEffH, i, array.get(dwmHArr, i))
array.set(dwmEffL, i, array.get(dwmLArr, i))
array.set(dwmEffT, i, pT)
array.set(dwmBH, i, array.get(dwmCurBH, i))
array.set(dwmBL, i, array.get(dwmCurBL, i))
int cT = array.get(dwmCTArr, i)
if not na(cT) and cT != array.get(dwmCurKey, i)
// New forming period: reset its stamps.
array.set(dwmCurKey, i, cT)
array.set(dwmCurBH, i, curBasis)
array.set(dwmCurBL, i, curBasis)
float cH = array.get(dwmCHArr, i)
float cL = array.get(dwmCLArr, i)
if not na(cH) and cH != array.get(dwmPrvCH, i)
array.set(dwmCurBH, i, curBasis)
if not na(cL) and cL != array.get(dwmPrvCL, i)
array.set(dwmCurBL, i, curBasis)
array.set(dwmPrvCH, i, cH)
array.set(dwmPrvCL, i, cL)
// Cash-close promotion (futures mode, day slot). On the confirmed bar
// after the chart exits the cash RTH window, the source's forming day is
// final even though it has not rolled; promote it.
bool dwmRthIn = false
if futMode
dwmRthIn := not na(time(timeframe.period, "0930-1600", tz))
if futMode and not dwmRthIn and dwmRthPrev
float cH0 = array.get(dwmCHArr, 0)
float cL0 = array.get(dwmCLArr, 0)
int cT0 = array.get(dwmCTArr, 0)
if not na(cH0) and not na(cL0) and not na(cT0) and cT0 != array.get(dwmKeyT, 0)
array.set(dwmKeyT, 0, cT0)
array.set(dwmEffH, 0, cH0)
array.set(dwmEffL, 0, cL0)
array.set(dwmEffT, 0, cT0)
array.set(dwmBH, 0, array.get(dwmCurBH, 0))
array.set(dwmBL, 0, array.get(dwmCurBL, 0))
dwmRthPrev := dwmRthIn
// --- Gap capture: latch on rthOpenTime change into a small ring buffer of
// source prices, times, per-edge basis stamps and a filled flag. State only -
// the boxes themselves are a separate fixed pool touched only under
// barstate.islast.
var array<float> gapPrvCArr = array.new<float>()
var array<float> gapOpenArr = array.new<float>()
var array<int> gapStartArr = array.new<int>()
var array<bool> gapFillArr = array.new<bool>()
var array<int> gapFillTArr = array.new<int>()
var array<float> gapBPrvArr = array.new<float>()
var array<float> gapBOpnArr = array.new<float>()
var int lastGapKey = 0
if barstate.isconfirmed and not na(gRthOT) and gRthOT != lastGapKey
lastGapKey := gRthOT
if not na(gPrvC) and not na(gPrvCT) and math.abs(gRthO - gPrvC) >= minGapSize
array.push(gapPrvCArr, gPrvC)
array.push(gapOpenArr, gRthO)
array.push(gapStartArr, gRthOT)
array.push(gapFillArr, false)
array.push(gapFillTArr, 0)
// Per-edge formation stamps: the open edge printed on this bar; the
// prior-close edge printed at the last cash close, whose basis is the
// value held through the overnight - this bar's PREVIOUS basis, before
// the 09:30 refresh.
array.push(gapBPrvArr, basisSpy10[1])
array.push(gapBOpnArr, basisSpy10)
if array.size(gapStartArr) > maxGaps
array.shift(gapPrvCArr)
array.shift(gapOpenArr)
array.shift(gapStartArr)
array.shift(gapFillArr)
array.shift(gapFillTArr)
array.shift(gapBPrvArr)
array.shift(gapBOpnArr)
// Gap fill: price touching the far edge (the converted prior RTH close) fills
// the gap. Monotone latch, checked every bar with the current basis.
if array.size(gapStartArr) > 0
for j = 0 to array.size(gapStartArr) - 1
if not array.get(gapFillArr, j)
float farEdge = convSpyToEs(array.get(gapPrvCArr, j), array.get(gapBPrvArr, j))
if not na(farEdge)
bool gapUp = array.get(gapOpenArr, j) > array.get(gapPrvCArr, j)
if (gapUp and chLo <= farEdge) or (not gapUp and chHi >= farEdge)
array.set(gapFillArr, j, true)
array.set(gapFillTArr, j, time)
// ============================================================================
// SECTION 8: RENDER HELPERS
// ============================================================================
// Label consolidation is PineDraw's ColoredLabelSet (exact-match merge on
// prices already rounded through convToChart; the first entry at a price wins
// the color, and a duplicate text on a row is skipped - a live and a
// completed line of the same killzone print once). The flush below stays
// local for one reason: the merged row's price is appended a single time
// AFTER merging, so "A + B 769.33" prints one price, not one per source.
// ============================================================================
// SECTION 9: RENDER (barstate.islast only)
// ============================================================================
// Strict compute -> filter -> render: sections 5-7 ran on every bar; every
// object create/update/delete lives here, under barstate.islast, through the
// PineDraw update-in-place helpers. Creation is conditional (an object exists
// only while its show condition holds), never create-then-hide, and every
// delete path assigns na back to the owning field via clearLine/clearBox.
// Owned object references. Worst case: 12 killzone lines (6 x completed H/L)
// + 3 open lines + 9 prior-period lines = 24 lines; up to maxGaps (<= 20)
// boxes; roughly 29 labels before consolidation. Well inside the 200 caps.
var array<line> kzDnHiLn = array.new<line>(6)
var array<line> kzDnLoLn = array.new<line>(6)
var array<line> opLnArr = array.new<line>(3)
var array<line> dwmHiLn = array.new<line>(3)
var array<line> dwmLoLn = array.new<line>(3)
var array<line> dwmMidLn = array.new<line>(3)
var array<box> gapBxArr = array.new<box>(maxGaps)
var draw.LabelPool lblPool = draw.newPool()
var string styleMain = draw.lineStyle(lineStyleIn)
var string styleOpen = draw.lineStyle(openStyleIn)
var string styleDwm = draw.lineStyle(dwmStyleIn)
var string lblSz = draw.textSize(labelSizeIn)
var string tablePosC = draw.tablePos(tablePosIn)
// Info table: created once, cells rewritten on the last bar. Tables are their
// own object class and do not consume the line/box/label budget.
var table infoTable = table.new(tablePosC, 1, 3, bgcolor = color.new(color.gray, 85), border_width = 1, border_color = color.new(color.gray, 70))
if barstate.islast
// Pool reset runs first and the trim at the bottom runs last, both on
// every islast pass regardless of any toggle, so switching a feature off
// visibly removes its labels.
lblPool.reset()
// Anchor geometry from served bar times only - never timenow.
// FIX LINEEND-GAP-1 (2026-08-13): bar spacing comes from the chart
// timeframe, never from time - time[1] - on the first bar after an
// overnight or weekend gap that difference is hours, and the right edge
// leapt a week out.
int lineEnd = time + timeframe.in_seconds(timeframe.period) * 1000 * labelOffsetBars
bool tfOk = timeframe.isintraday and timeframe.in_seconds() <= timeframe.in_seconds(tfLimit)
bool drawOk = recognized and tfOk
// Label collection for this pass: map from rounded price to entry index.
draw.ColoredLabelSet cset = draw.newColoredSet()
string srcName = futMode ? cashSource : "ES"
// ---- Killzones: most recent completed session H/L only (from the
// effective copies, so a chart-side promoted session renders as
// completed), extended until mitigated (x2 freezes at the mitigation
// bar). Forming sessions draw nothing: during overlap hours they
// duplicate this chart's own developing range.
for i = 0 to 5
bool kzEn = drawOk and array.get(kzOnArr, i)
color kzCol = array.get(kzColArr, i)
string kzNm = srcName + " " + array.get(kzNameArr, i)
float dH = convToChart(array.get(kzEffDnH, i), array.get(kzEffBH, i))
float dL = convToChart(array.get(kzEffDnL, i), array.get(kzEffBL, i))
int dT = array.get(kzEffDnT, i)
if kzEn and not na(dH) and not na(dT) and not (hideMitigated and array.get(kzHiMit, i))
int x2h = array.get(kzHiMit, i) ? array.get(kzHiMitT, i) : lineEnd
array.set(kzDnHiLn, i, draw.updateLine(array.get(kzDnHiLn, i), dT, dH, x2h, dH, kzCol, lineWidthIn, styleMain))
cset.add(dH, kzNm + " H", kzCol, int(na), " + ", 0)
else
array.set(kzDnHiLn, i, draw.clearLine(array.get(kzDnHiLn, i)))
if kzEn and not na(dL) and not na(dT) and not (hideMitigated and array.get(kzLoMit, i))
int x2l = array.get(kzLoMit, i) ? array.get(kzLoMitT, i) : lineEnd
array.set(kzDnLoLn, i, draw.updateLine(array.get(kzDnLoLn, i), dT, dL, x2l, dL, kzCol, lineWidthIn, styleMain))
cset.add(dL, kzNm + " L", kzCol, int(na), " + ", 0)
else
array.set(kzDnLoLn, i, draw.clearLine(array.get(kzDnLoLn, i)))
// ---- Opening prices: most recent occurrence, until mitigated.
for i = 0 to 2
float oP = convToChart(array.get(opPArr, i), array.get(opB, i))
int oT = array.get(opTArr, i)
if drawOk and array.get(opOnArr, i) and not na(oP) and not na(oT) and not (hideMitigated and array.get(opMit, i))
color opCol = array.get(opColArr, i)
int x2 = array.get(opMit, i) ? array.get(opMitT, i) : lineEnd
array.set(opLnArr, i, draw.updateLine(array.get(opLnArr, i), oT, oP, x2, oP, opCol, lineWidthIn, styleOpen))
cset.add(oP, srcName + " " + array.get(opTxtArr, i), opCol, int(na), " + ", 0)
else
array.set(opLnArr, i, draw.clearLine(array.get(opLnArr, i)))
// ---- Prior day / week / month: H, L and midpoint, extended to lineEnd
// for the full period (not mitigation-gated, matching KZP), replaced when
// a new period completes.
for i = 0 to 2
float srcH = array.get(dwmEffH, i)
float srcL = array.get(dwmEffL, i)
float pH = convToChart(srcH, array.get(dwmBH, i))
float pL = convToChart(srcL, array.get(dwmBL, i))
float pM = convToChart(math.avg(srcH, srcL), math.avg(array.get(dwmBH, i), array.get(dwmBL, i)))
int pT = array.get(dwmEffT, i)
color dwCol = array.get(dwmColArr, i)
string dwNm = array.get(dwmNmArr, i)
if drawOk and array.get(dwmOnArr, i) and not na(pH) and not na(pL) and not na(pT)
array.set(dwmHiLn, i, draw.updateLine(array.get(dwmHiLn, i), pT, pH, lineEnd, pH, dwCol, lineWidthIn, styleDwm))
array.set(dwmLoLn, i, draw.updateLine(array.get(dwmLoLn, i), pT, pL, lineEnd, pL, dwCol, lineWidthIn, styleDwm))
cset.add(pH, srcName + " " + dwNm + "H", dwCol, int(na), " + ", 0)
cset.add(pL, srcName + " " + dwNm + "L", dwCol, int(na), " + ", 0)
if dwmMids
array.set(dwmMidLn, i, draw.updateLine(array.get(dwmMidLn, i), pT, pM, lineEnd, pM, dwCol, lineWidthIn, line.style_dotted))
cset.add(pM, srcName + " " + dwNm + "M", dwCol, int(na), " + ", 0)
else
array.set(dwmMidLn, i, draw.clearLine(array.get(dwmMidLn, i)))
else
array.set(dwmHiLn, i, draw.clearLine(array.get(dwmHiLn, i)))
array.set(dwmLoLn, i, draw.clearLine(array.get(dwmLoLn, i)))
array.set(dwmMidLn, i, draw.clearLine(array.get(dwmMidLn, i)))
// ---- Gap boxes (futures mode only): a fixed pool of maxGaps box slots
// mapped onto the ring buffer, so ring shifts never touch objects outside
// this pass. A filled gap freezes its right edge and dims - a state change
// on a live object, not create-then-hide.
int nGaps = array.size(gapStartArr)
for j = 0 to maxGaps - 1
bool showBx = showGaps and futMode and drawOk and j < nGaps
if showBx and hideFilledGaps
showBx := not array.get(gapFillArr, j)
if showBx
float eA = convSpyToEs(array.get(gapOpenArr, j), array.get(gapBOpnArr, j))
float eB = convSpyToEs(array.get(gapPrvCArr, j), array.get(gapBPrvArr, j))
showBx := not na(eA) and not na(eB)
if showBx
float gsz = array.get(gapOpenArr, j) - array.get(gapPrvCArr, j)
bool gapUp = gsz > 0
bool fll = array.get(gapFillArr, j)
color baseCol = gapUp ? gapUpCol : gapDnCol
color bxCol = fll ? color.new(baseCol, math.min(100, math.round(color.t(baseCol)) + 10)) : baseCol
int x2 = fll ? array.get(gapFillTArr, j) : lineEnd
array.set(gapBxArr, j, draw.updateBox(array.get(gapBxArr, j), array.get(gapStartArr, j), math.max(eA, eB), x2, math.min(eA, eB), bxCol, color.new(bxCol, 100)))
cset.add(math.round_to_mintick(math.avg(eA, eB)), "SPY Gap " + (gapUp ? "▲" : "▼") + " " + str.tostring(math.abs(gsz), "#.##"), color.new(baseCol, 40), int(na), " + ", 0)
if not showBx
array.set(gapBxArr, j, draw.clearBox(array.get(gapBxArr, j)))
// ---- Flush consolidated labels through the pool. One label per rounded
// price, price printed once at the end of the merged text.
int nLbl = array.size(cset.prices)
if nLbl > 0
for i = 0 to nLbl - 1
float p = array.get(cset.prices, i)
lblPool.acquire(lineEnd, p, array.get(cset.texts, i) + " " + str.tostring(p, format.mintick), false, array.get(cset.colors, i), color.white, lblSz)
// ---- Info table.
table.clear(infoTable, 0, 0, 0, 2)
if not recognized
table.cell(infoTable, 0, 0, "Unrecognized chart symbol - set Mode manually", text_color = color.white, bgcolor = color.new(color.red, 20), text_size = size.small, text_halign = text.align_left)
else if showTable
bool activeIsSpy = futMode ? cashIsSpy : chartIsSpy
float activeBasis = activeIsSpy ? basisSpy10 : basisSpx
int freshT = activeIsSpy ? spyFreshCloseT : spxFreshCloseT
bool freshNow = not na(freshT) and time_close - freshT <= timeframe.in_seconds(timeframe.period) * 2000
string dirTxt = futMode ? "Futures ES ← " + cashSource : "Cash " + (chartIsSpy ? "SPY" : "SPX") + " ← ES"
string basisTxt = (activeIsSpy ? "SPY×10 " : "SPX ") + (na(activeBasis) ? "n/a" : (activeBasis >= 0 ? "+" : "") + str.tostring(activeBasis, "#.##"))
string stateTxt = na(activeBasis) ? "no overlap bar yet" : freshNow ? "live" : "frozen " + str.format_time(freshT, "HH:mm", tz)
table.cell(infoTable, 0, 0, dirTxt, text_color = chart.fg_color, text_size = size.small, text_halign = text.align_left)
table.cell(infoTable, 0, 1, basisTxt, text_color = chart.fg_color, text_size = size.small, text_halign = text.align_left)
table.cell(infoTable, 0, 2, stateTxt, text_color = chart.fg_color, text_size = size.small, text_halign = text.align_left)
// ---- Pool trim: delete every pooled label beyond this pass's high-water
// mark. Unconditional, so toggling features off removes their labels.
while array.size(lblPool.items) > lblPool.used
label.delete(array.pop(lblPool.items))
// ============================================================================
// SECTION 10: DEBUG TABLE
// ============================================================================
// Diagnostic internals, off by default. Built to answer one question class:
// when lines misbehave, is the problem the served data (do the three legs
// rewind in bar replay? do killzone tuples?), the basis (raw vs held vs
// stamped), or the render. Row meanings:
// chart bar / close - replay position and this chart's own last price
// es/spy/spx leg - the three gaps_on served closes; on an ES chart the
// es leg must equal chart close, and in bar replay a
// leg showing a price from the WRONG ERA is the proof
// that replay does not rewind other-symbol feeds
// fresh - which basis pairs updated on this bar
// spy10 / spx - raw basis and its held (re-anchored) copy
// conv basis - curBasis (stamp source) and projBasis (projection)
// NYPM served - killzone 5's served completed H and start time; a
// start time from the wrong era means source-side
// session data is not rewinding either
// NYPM eff/stamp - the effective level, its formation stamp, and the
// converted price actually drawn
// confirmed bars - counter proving stamp latching runs in this mode
var int dbgConfirmed = 0
if barstate.isconfirmed
dbgConfirmed += 1
dbgF(float x) =>
na(x) ? "na" : str.tostring(x, "#.##")
dbgT(int t) =>
na(t) or t == 0 ? "na" : str.format_time(t, "MM-dd HH:mm", tz)
var table dbgTbl = debugOn ? table.new(position.middle_left, 2, 18, bgcolor = color.new(color.black, 20), border_width = 1, border_color = color.new(color.gray, 60)) : table(na)
dbgRow(int r, string k, string v) =>
table.cell(dbgTbl, 0, r, k, text_color = color.silver, text_size = size.tiny, text_halign = text.align_left)
table.cell(dbgTbl, 1, r, v, text_color = color.white, text_size = size.tiny, text_halign = text.align_left)
if debugOn and barstate.islast
dbgRow(0, "chart bar closes", dbgT(time_close))
dbgRow(1, "chart close", dbgF(close))
dbgRow(2, "es leg", dbgF(esCloseG) + " @ " + dbgT(esTcG))
dbgRow(3, "spy leg", dbgF(spyCloseG) + " @ " + dbgT(spyTcG))
dbgRow(4, "spx leg", dbgF(spxCloseG) + " @ " + dbgT(spxTcG))
dbgRow(5, "fresh", (spyPairFresh ? "spy " : "") + (spxPairFresh ? "spx" : "") + (not spyPairFresh and not spxPairFresh ? "none" : ""))
dbgRow(6, "spy10 raw|held", dbgF(basisSpy10) + " | " + dbgF(basisSpy10R))
dbgRow(7, "spx raw|held", dbgF(basisSpx) + " | " + dbgF(basisSpxR))
dbgRow(8, "conv cur|proj", dbgF(curBasis) + " | " + dbgF(projBasis))
dbgRow(9, "mode", (futMode ? "futures" : cashMode ? "cash" : "?") + " src:" + (futMode ? cashSource : "ES") + " " + (projMode ? "proj" : "hist"))
dbgRow(10, "NYPM served", dbgF(kz5DnH) + " @ " + dbgT(kz5DnT))
dbgRow(11, "NYPM eff", dbgF(array.get(kzEffDnH, 4)) + " @ " + dbgT(array.get(kzEffDnT, 4)))
dbgRow(12, "NYPM stamp|drawn", dbgF(array.get(kzEffBH, 4)) + " | " + dbgF(convToChart(array.get(kzEffDnH, 4), array.get(kzEffBH, 4))))
dbgRow(13, "confirmed bars", str.tostring(dbgConfirmed))
dbgRow(14, "PD eff", dbgF(array.get(dwmEffH, 0)) + "/" + dbgF(array.get(dwmEffL, 0)) + " @ " + dbgT(array.get(dwmEffT, 0)))
dbgRow(15, "PD cur", dbgF(array.get(dwmCHArr, 0)) + "/" + dbgF(array.get(dwmCLArr, 0)) + " @ " + dbgT(array.get(dwmCTArr, 0)))
dbgRow(16, "PD key", dbgT(array.get(dwmKeyT, 0)))
dbgRow(17, "rth prev", str.tostring(dwmRthPrev) + " (futMode " + str.tostring(futMode) + ")")
Show all 1,091 lines