Indicator · Pine Script v6 · On the chart

Cross Levels (ES-SPY/SPX)

by 345-strategies· code credited to TheStratSuite contributors

Translates key reference levels between CME E-mini S&P 500 futures (ES) and cash equity equivalents (SPY or SPX), displaying them directly on the price chart.

View on GitHub

GitHub stars5on the repository
Pine versionv6current
Lines of code1,091
Settings40inputs
LicenceMPL-2.0
Last update2 days ago26 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

1,091 lines of Pine Script v6 from cross-levels/CrossLevels_v0.1.0.pine, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

CrossLevels_v0.1.0.pine
// 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) + ")")

Overview

When applied to an ES chart, it projects cash session highs, lows, opens, prior period levels, and gap boxes into futures prices. On cash charts, it maps overnight session killzones (Asia and London) into cash prices. Users can select the cash source, session killzones, and switch between projection or historical basis conversion modes.

Details

Published by

345-strategies on GitHub

Code credit

TheStratSuite contributors, as written in the script's header

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Support & resistance, Sessions & time

Markets

Indices

Timeframes

M1-M5, M15-H1

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

indicatorpine-scriptthestrattradingtradingviewprice-actionstratpine-script-librarythestrat-suite

Settings

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

SettingTypeDefaultVariable
ModestringAutomode
Futures symbolsymbolCME_MINI:ES1!futSym
Cash level sourcestringSPYcashSource
SPY symbolsymbolAMEX:SPYspySym
SPX symbolsymbolSP:SPXspxSym
ConversionstringProjection (liquidity)convModeIn
Basis Re-anchor Backstopfloat3.0basisReanchor
TimezonestringAmerica/New_Yorktz
Timeframe Limittimeframe30tfLimit
useKz1booltrueuseKz1
kz1Colcolorcolor.new(color.blue, 60)kz1Col
kz1NamestringASIAkz1Name
Show all 40 settings
SettingTypeDefaultVariable
kz1Sesssession2000-0000kz1Sess
useKz2booltrueuseKz2
kz2Colcolorcolor.new(color.red, 60)kz2Col
kz2NamestringLNDNkz2Name
kz2Sesssession0200-0500kz2Sess
useKz3booltrueuseKz3
kz3Colcolorcolor.new(color.teal, 60)kz3Col
kz3NamestringNYAMkz3Name
kz3Sesssession0930-1100kz3Sess
useKz4boolfalseuseKz4
kz4Colcolorcolor.new(color.orange, 60)kz4Col
kz4NamestringNYLkz4Name
kz4Sesssession1200-1300kz4Sess
useKz5booltrueuseKz5
kz5Colcolorcolor.new(color.fuchsia, 60)kz5Col
kz5NamestringNYPMkz5Name
kz5Sesssession1330-1600kz5Sess
useKz6boolfalseuseKz6
kz6Colcolorcolor.new(color.black, 60)kz6Col
kz6NamestringRTHkz6Name
kz6Sesssession0930-1600kz6Sess
useOp1booltrueuseOp1
op1Sesssession1800-1801op1Sess
op1Colcolorcolor.blueop1Col
op1TxtstringDaily Openop1Txt
useOp2booltrueuseOp2
op2Sesssession1200-1201op2Sess
op2Colcolorcolor.orangeop2Col
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

575 stars

This indicator builds Market Profile Time Price Opportunity (TPO) structures across defined session timeframes directly on the main price chart. It visualizes the Point of Control (POC) and Value Area boundaries using dynamic box drawings. An integrated multi-

Indicator v5 Support & resistanceScreeners & dashboards
Updated 1 year ago · MPL-2.0 GitHub repository of Market Profile with Screener
257 stars

This indicator plots auto trade signals for E-mini S&P 500 futures on the price chart. It computes an EMA200 filter along with day ETH RTH session levels and percentage-based entry zones. Signals trigger CALL or PUT cases when price crosses defined thresholds

Indicator v6 TrendSupport & resistance
Updated 6 weeks ago · None stated GitHub repository of Auto Trade Futures ES
219 stars

This indicator plots reference levels for previous daily high and low prices directly on the price chart. It requests daily timeframe data using the security function and plots the offset values as continuous lines, displaying resistance in red and support in

Indicator v4 Support & resistanceSessions & time
Updated 5 years ago · None stated GitHub repository of 昨日高低點標示

Golden Bullet Strategy

by TradersPost
218 stars

The Golden Bullet Strategy is an overlay signal indicator based on Smart Money Concepts and time-based liquidity models. It identifies liquidity sweeps of a designated reference session range during a specified hunt window. It then confirms Market Structure Sh

Indicator v6 SMC / ICT / price actionSessions & time
Updated 3 months ago · None stated GitHub repository of Golden Bullet Strategy

Moon Dev GEX Arrows

by moondevonyt
92 stars

This overlay indicator plots option strike walls and an optional session VWAP on the price chart for SPY or ES. It spaces major strikes by the selected instrument, draws a set number of strikes on each side, and marks wall-trap and sweep events after a cooldow

Indicator v5 Support & resistanceSessions & time
Updated 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Arrows

Key Levels

by cryptorife
75 stars

This indicator plots reference levels and session markers directly on the price chart. It calculates and draws lines for the daily open, weekly open, monthly open, previous day OHLC, and previous week extremes, along with a selectable day-of-week range and ini

Indicator v4 Support & resistanceSessions & time Repaints
Updated 3 years ago · GPL-3.0 GitHub repository of Key Levels

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers