ICT Levels Desk · Free & open-source

What happened at that price?

Keep useful price references. See what happens when price returns.

A reference map for TradingView. It does not choose trades.

Pastel clay illustration of session ranges and key price levels: nearby levels stay in focus while their live, swept or filled states are tracked.
Workflow illustration
  1. Map the day

    Sessions, gaps and past highs and lows.

  2. Keep nearby levels

    Focus the map around current price.

  3. See what changed

    Name live, swept or filled levels.

See the real chart

What is ICT Levels Desk?

ICT Levels Desk is a free TradingView indicator for previous-period highs and lows, session opens, opening gaps and other price references. It distinguishes touches, sweeps and closing breaks, with display filters and a context dashboard. This is a reference-mapping tool: it does not supply an entry plan, position size or companion strategy.

How does ICT Levels Desk distinguish a sweep from a break?

For a high or low, a strict wick through followed by a close back inside marks the first test as swept. A strict close beyond marks it broken. Equal closes, opening gaps and FVGs follow distinct state rules. Compare touches, sweeps and breaks

Why can a nearby ICT level disappear from the chart?

The proximity filter can hide references without stopping their state updates or removing them from nearest-level calculations. The display also separates retained finished references from live ones. Check filter settings and reference state before assuming a level was deleted. Understand proximity and retained levels

Does ICT Levels Desk include an entry strategy or trade probabilities?

No. It maps price references and their observed states. Dashboard counts describe loaded history, not independent odds of the next event, and the Co-Pilot provides context. There is no companion strategy, order placement or position-sizing model in this tool. Read the reference-tool boundaries

Yesterday leaves a reference.

A reader wanted gaps, opening prices and old highs on one clear chart. Start with one high.

High so far · 103Closed candle 1
A candle records where price went.The thin wick shows the highest and lowest prices. The body joins open and close. The highest price so far is 103. Green candles close above their open; red candles close below. White dot: latest close. Dashed grey line: high observed so far, not yet a tracked reference.
Thin wick = price extremes · White dot = latest close
Latest close
102
Highest price so far
103
Earlier period · high still developing
1 / 6 · Price moves

A candle records where price went.

The thin wick shows the highest and lowest prices. The body joins open and close. The highest price so far is 103.

Observe first. Keep the reference after the period ends.

Fictional prices. Three candles represent the earlier period; its completed high/low pair changes. Daily ATR, a movement measure, is supplied as 20 points.

How could this help a trade?

Choose where to watch and record the response. Test your own entry, exit and risk rules; a sweep alone does not establish an advantage.

Same line. Different closes.

Reveal five candles. Compare the first visit.

Previous high · 106Closed candle 1
Price is still below the old high.The reference is 106. No wick has passed above it. Reveal candle four, then compare its four versions. Green candles close above their open; red candles close below. White dot: latest close. Gold line: completed previous high at 106.
Thin wick = price extremes · White dot = latest close
Latest close
97
Distance from line
9 points
resting · still live
Candle 1 / 5

Price is still below the old high.

The reference is 106. No wick has passed above it. Reveal candle four, then compare its four versions.

Wick high
98
Close
97
Desk label
resting
Still live. This label alone is not an entry.

Reveal candle four, then switch cases. Candle five shows what the desk remembers.

What is supplied in this example?

The high at 106 already exists before these fictional closed candles. Daily ATR is supplied as 20 points and drawing radius as twice that. Only revealed candles update the state. This miniature lesson does not calculate the trading clock or discover previous periods.

Does every level follow the same steps?

No. These are alternative outcomes, not a ladder. Different reference types also have different rules. An opening gap can fill with a wick; a three-candle gap needs a closing break to invert.

Find those references on a real chart.

Start with one nearby level.

Real TradingView index-futures five-minute chart with ICT Levels Desk: session boxes, previous highs and lows, opening prices, gap states and a nearest-level dashboard.
TradingView snapshot · index futures · five-minute candlesOpen full-size chart ↗
  1. 1 Where did it come from?

    PDH means previous day’s high. Other names identify an opening price, a gap or a completed session.

  2. 2 What happened there?

    Read its state. A swept line remains live; a broken line ends at the closing break.

  3. 3 What is nearby?

    The panel lists unfinished references above and below. Their distance helps you focus.

Explore every reference type in the simulator

Change the closing price, wick, reference type and drawing radius. Try an opening gap and a bullish first FVG with the same close and wick.

CHANGE THE CANDLE. SEE WHY.

Change the candle. Read the level.

The wick and close do different jobs. Move them across the reference to see its state change.

Earlier candles → test candleFinal close 104
Previous high1061221131049586
Drag the blue close ↕
Watch the same candle change
104
5
How the calculation works & more controls
2

Each kind replays its own untouched reference history. A high or low needs a strict wick-through; gaps fill at the far wick edge, while an FVG inverts only on a close through it. Radius changes visibility, not stored prices.

Illustrative inputs · changing a slider recalculates this example · no market feed

Paste it into TradingView.

The complete Pine v6 indicator is free, under MIT.

  1. 1

    Open Pine Editor

    Create a new indicator on an intraday chart.

  2. 2

    Paste the code

    Copy below, replace the sample, save and add to chart.

  3. 3

    Match your session clock

    Set the tool’s timezone and day-close and day-open times for your market.

Draws directly on the price chart; no extra pane. Try five- or fifteen-minute candles.

Indicator code

Copy the code as it is. No coding needed.

View the full code
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
//  ICT LEVELS DESK — NDOG · NWOG · midnight / 08:30 / 09:30 opens · PDH-PDL · PWH-PWL · PMH-PML · first-presented FVG · macro windows  ·  Pine Script v6  ·  v1.2.0
//  Built for a r/pinescript thread reply ("Real Trader Tim's toolkit … but clean, lightweight, only what matters").
//  The wishlist: NDOG / NWOG, first-presentation FVGs, the midnight open, weekly / daily highs and lows, macro
//  windows, and "days back" so the chart does not drown. This desk adds what the wishlist did not ask for:
//     · every level knows its own state — a gap is untouched / entered / FILLED, a high or low is resting / SWEPT /
//       BROKEN, an FVG is fresh / TOUCHED / INVERTED — and fades once it is used, or leaves the chart (switch)
//     · a proximity filter: only levels within N × the daily ATR of price are drawn; the panel says how many are hidden
//     · the panel lists the three nearest levels above and below price with the distance in points and ATR
//     · a countdown to the next macro window, and what today's first-presented FVG did
//     · base rates on the loaded chart: how often the NDOG filled the same day, PDH / PDL got taken, the FPFVG revisited
//  Every level commits when it is born (a closed bar, a closed period); states change on closed bars only.
//  No signals, no background tint. Context, not instruction.  License: MIT — share freely, keep this header.  © 2026 ProEA Lab
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
//@version=6
indicator("ICT Levels Desk — NDOG · NWOG · opens · PDH/PDL · PWH/PWL · FPFVG · macros", "ICT Levels", overlay = true, max_lines_count = 400, max_boxes_count = 200, max_labels_count = 500)

// ─────────────────────────────────────────────────────────────────── INPUTS
gT = "Clock"
inTz        = input.string("America/New_York", "Timezone (IANA)", group = gT, tooltip = "Every time below is read in this timezone. ICT times are New York.", display = display.none)

gG = "Opening gaps (NDOG · NWOG)"
inNdogOn    = input.bool(true, "NDOG — new-day opening gap", group = gG, tooltip = "The gap between the close at the day-close time and the open at the day-open time (16:00 → 18:00 New York for futures and most CFDs; set 16:00 → 09:30 for regular-hours stocks). Drawn as a box with its consequent encroachment (the midline).", display = display.none)
inNwogOn    = input.bool(true, "NWOG — new-week opening gap", group = gG, tooltip = "The same gap across the weekend: the last close before the weekend to the first open after it. ICT keeps the last few weeks of these as reference.", display = display.none)
inCloseTime = input.string("16:00", "  Day-close time (HH:MM)", group = gG, display = display.none)
inOpenTime  = input.string("18:00", "  Day-open time (HH:MM)", group = gG, display = display.none)
inGapMin    = input.float(0.02, "  Skip gaps smaller than N × daily ATR", minval = 0, maxval = 1, step = 0.01, group = gG, display = display.none)
inNdogBack  = input.int(3, "  NDOG — days back", minval = 1, maxval = 20, group = gG, display = display.none)
inNwogKeep  = input.int(5, "  NWOG — keep the last N", minval = 1, maxval = 12, group = gG, display = display.none)
inGapRemove = input.bool(false, "  Remove a gap once it is filled (otherwise it fades)", group = gG, display = display.none)

gO = "Opens"
inMidOn     = input.bool(true, "Midnight open (00:00)", group = gO, tooltip = "ICT's true day open. On regular-hours charts the day's first bar stands in for it.", display = display.none)
in830On     = input.bool(true, "08:30 open", group = gO, display = display.none)
in930On     = input.bool(true, "09:30 open", group = gO, display = display.none)
inOpenBack  = input.int(1, "  Opens — days back", minval = 1, maxval = 10, group = gO, display = display.none)

gL = "Previous highs / lows"
inPdOn      = input.bool(true, "Previous day high / low (PDH · PDL)", group = gL, display = display.none)
inPwOn      = input.bool(true, "Previous week high / low (PWH · PWL)", group = gL, display = display.none)
inPmOn      = input.bool(false, "Previous month high / low (PMH · PML)", group = gL, display = display.none)
inP4On      = input.bool(false, "Previous 4-hour high / low (P4H · P4L)", group = gL, tooltip = "The intraday HTF pair: the last closed 4-hour bar's high and low, replaced when the next one closes.", display = display.none)
inLvlBack   = input.int(3, "  Daily levels — days back", minval = 1, maxval = 30, group = gL, display = display.none)
inLvlRemove = input.bool(false, "  Remove a level once it is broken (otherwise it fades)", group = gL, display = display.none)

gF = "First-presented FVG (FPFVG)"
inFpOn      = input.bool(true, "First FVG after the 09:30 open", group = gF, tooltip = "The first three-bar imbalance printed after the anchor time, on the chart timeframe, drawn with its consequent encroachment. States: fresh → touched (price traded into it) → inverted (a bar closed through its far side).", display = display.none)
inFp830     = input.bool(false, "  Also the first FVG after 08:30", group = gF, display = display.none)
inFpMid     = input.bool(false, "  Also the first FVG after midnight", group = gF, display = display.none)
inFpMin     = input.float(0.0, "  Skip FVGs smaller than N × ATR (chart)", minval = 0, maxval = 3, step = 0.05, group = gF, display = display.none)
inFpBack    = input.int(1, "  FPFVG — days back", minval = 1, maxval = 10, group = gF, display = display.none)

gM = "Macro windows"
inMacLon    = input.bool(true, "London macros (02:33–03:00 · 04:03–04:30)", group = gM, display = display.none)
inMacAm     = input.bool(true, "New York AM macros (08:50–09:10 · 09:50–10:10 · 10:50–11:10)", group = gM, display = display.none)
inMacLunch  = input.bool(true, "Lunch macro (11:50–12:10)", group = gM, display = display.none)
inMacPm     = input.bool(true, "New York PM macros (13:10–13:40 · 15:15–15:45)", group = gM, display = display.none)
inMacStyle  = input.string("Shade + markers", "Draw as", options = ["Shade + markers", "Markers", "Off"], group = gM, tooltip = "Shade = a soft tint across the window's minutes (the classic ICT-macro look); markers = a dotted line at the window's start and end with a tiny tag. The panel always shows the countdown.", display = display.none)
inMacBack   = input.int(1, "  Macro markers — days back", minval = 1, maxval = 10, group = gM, display = display.none)

gK = "Killzones (session boxes)"
inKzOn      = input.bool(true, "Session boxes: Asia · London · New York AM · New York PM", group = gK, tooltip = "Each session's high-low range as a soft box with its name; when the session ends, its high and low become liquidity lines that stay until swept or broken.", display = display.none)
inKzAsia    = input.session("2000-0000", "  Asia", group = gK, display = display.none)
inKzLon     = input.session("0200-0500", "  London", group = gK, display = display.none)
inKzAm      = input.session("0830-1100", "  New York AM", group = gK, display = display.none)
inKzPm      = input.session("1330-1600", "  New York PM", group = gK, display = display.none)
inKzLines   = input.bool(true, "  Session high / low lines until swept", group = gK, display = display.none)
inKzBack    = input.int(2, "  Sessions — days back", minval = 1, maxval = 10, group = gK, display = display.none)

gP = "Dealing range (premium / discount)"
inDrOn      = input.bool(true, "Premium / discount between the previous day high and low", group = gP, tooltip = "The previous day's range split at equilibrium: above it price is in premium (rose), below it in discount (teal). Replaced when a new day's levels are born.", display = display.none)

gV = "Style"
inTheme     = input.string("Aurora Neon", "Theme", options = ["Aurora Neon", "Royal Gold", "Colorblind-safe"], group = gV, display = display.none)
inProxOn    = input.bool(true, "Proximity filter: draw only levels within N × daily ATR of price", group = gV, tooltip = "The anti-clutter rule. Everything is still tracked; only what is near is drawn, and the panel says how many levels are hidden.", display = display.none)
inProxAtr   = input.float(2.0, "  N × daily ATR", minval = 0.25, maxval = 10, step = 0.25, group = gV, display = display.none)
inLabels    = input.string("Short", "Labels", options = ["Full", "Short", "Off"], group = gV, tooltip = "Full = name and price · Short = name only · Off = lines only.", display = display.none)
inLabelGap  = input.int(0, "  Label anchor (bars right of the last bar; the text runs left)", minval = 0, maxval = 20, group = gV, display = display.none)
inFadeAlpha = input.int(80, "  Used levels fade to this transparency", minval = 50, maxval = 95, group = gV, display = display.none)
inMarkers   = input.bool(true, "Event marks on the chart (⚡ sweep · ↻ inversion)", group = gV, display = display.none)
inBoxTags   = input.bool(true, "Name tags on gap / FVG / session boxes", group = gV, display = display.none)

gD = "Dashboard"
inDash      = input.bool(true, "Show the dashboard", group = gD, display = display.none)
inDashPos   = input.string("Middle Left", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right", "Middle Left"], group = gD, display = display.none)
inSize      = input.string("Medium", "Size", options = ["Small", "Medium", "Large"], group = gD, display = display.none)
inShowRec   = input.bool(true, "Show the base rates (record)", group = gD, display = display.none)

gN = "Co-Pilot (narrative)"
inNarr      = input.string("Brief", "Narrative depth", options = ["Off", "Brief", "Standard", "Detailed"], group = gN, tooltip = "Brief = one sentence (NOW). Standard adds WHY and WATCH. Detailed adds the base rates and a glossary.", display = display.none)
inNarrPos   = input.string("Bottom Left", "Position", options = ["Bottom Left", "Bottom Right", "Top Left", "Top Right", "Middle Left", "Middle Right"], group = gN, display = display.none)
inNarrSize  = input.string("Auto", "Size", options = ["Auto", "Small", "Medium", "Large"], group = gN, display = display.none)
inNarrWrap  = input.int(64, "Line width (characters)", minval = 30, maxval = 140, group = gN, display = display.none)

gA = "Alerts"
inAlertJson = input.bool(true, "alert() payload as JSON (webhooks)", group = gA, tooltip = "Choose 'Any alert() function call' when creating the alert.", display = display.none)
inNearAtr   = input.float(0.15, "Alert when price comes within N × daily ATR of a live level (0 = off)", minval = 0, maxval = 2, step = 0.05, group = gA, tooltip = "Fires once when price arrives near a level that is still live (not filled / broken / inverted) and re-arms after price leaves. The payload names the level.", display = display.none)

// ─────────────────────────────────────────────────────────────────── THEME
type Theme
    color bg       = #0B0E14
    color frame    = #B8923A
    color accent   = #D8B14A
    color accentHi = #E8C25A
    color up       = #2BE7C7
    color down     = #FF3B5C
    color flat     = #6B7280
    color txt      = #D7DCE6
    color txtHi    = #ECEFF5
themeOf(string name) =>
    Theme t = Theme.new()
    if name == "Aurora Neon"
        t.bg       := #0E1118
        t.frame    := #2C3A5A
        t.accent   := #7C9CFF
        t.accentHi := #A9B8FF
        t.up       := #2AF0C8
        t.down     := #FF4D6D
        t.flat     := #5B6472
        t.txt      := #E6E9EF
        t.txtHi    := #F2F4F9
    else if name == "Colorblind-safe"
        t.bg       := #0E1014
        t.frame    := #3A4150
        t.accent   := #56B4E9
        t.accentHi := #8ED0F5
        t.up       := #E69F00
        t.down     := #0072B2
        t.flat     := #8C92A0
        t.txt      := #E8EAED
        t.txtHi    := #FFFFFF
    t
Theme th = themeOf(inTheme)
fmt(float x) => str.tostring(x, format.mintick)
color cGapW  = th.accentHi        // NWOG
color cGapD  = th.accent          // NDOG
color cOpen  = th.txtHi           // midnight open
color cOpen2 = th.flat            // 08:30 / 09:30 opens
color cHigh  = th.down            // previous highs (resting sell-side above)
color cLow   = th.up              // previous lows
color cFvgU  = th.up
color cFvgD  = th.down
color cMac   = th.flat
color cAsia  = #F5B84A
color cLon   = #4FA3FF
color cAm    = th.up
color cPm    = #B57BFF
color cPrem  = th.down
color cDisc  = th.up
sessColor(string n) => n == "Asia" ? cAsia : n == "London" ? cLon : n == "NY AM" ? cAm : cPm

// ─────────────────────────────────────────────────────────────────── CLOCK (all in the input timezone)
int    hh     = hour(time, inTz)
int    mi     = minute(time, inTz)
int    nowMin = hh * 60 + mi
bool   newDay = dayofmonth(time, inTz) != dayofmonth(time[1], inTz) or month(time, inTz) != month(time[1], inTz)
int    dow    = dayofweek(time, inTz)
int    dowPrev = dayofweek(time[1], inTz)
bool   afterWeekend = newDay and (dowPrev == dayofweek.friday or dowPrev == dayofweek.saturday)
bool   newMonth = month(time, inTz) != month(time[1], inTz)
var int dayNo = 0
if newDay
    dayNo += 1
var int pdSerial = 0
var int gapSerial = 0
clockMin(string hm) =>
    array<string> parts = str.split(hm, ":")
    array.size(parts) == 2 ? int(nz(str.tonumber(array.get(parts, 0)), -1)) * 60 + int(nz(str.tonumber(array.get(parts, 1)), 0)) : -1
crossT(int t) => t >= 0 and nowMin >= t and (newDay or nowMin[1] < t)      // the first bar at or after time t in the day
string hhmm = str.tostring(hh, "00") + ":" + str.tostring(mi, "00")
int closeT = clockMin(inCloseTime)
int openT  = clockMin(inOpenTime)
if crossT(openT)
    gapSerial += 1
bool insideDay = openT > closeT ? (nowMin >= openT or nowMin < closeT) : (nowMin >= openT and nowMin < closeT)
bool x830   = crossT(510)
bool x930   = crossT(570)
bool xClose = crossT(closeT)
bool xOpen  = crossT(openT)

// ─────────────────────────────────────────────────────────────────── DATA (all ta.* / security at global scope)
float atr   = ta.atr(14)
float atrU  = math.max(atr, syminfo.mintick)
dAtrFn() => ta.atr(14)
float dAtr  = request.security(syminfo.tickerid, "D", dAtrFn()[1], lookahead = barmerge.lookahead_on)
float dAtrU = math.max(nz(dAtr, atrU * 10), syminfo.mintick)
[pdh, pdl] = request.security(syminfo.tickerid, "D", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[pwh, pwl] = request.security(syminfo.tickerid, "W", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[pmh, pml] = request.security(syminfo.tickerid, "M", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[p4h, p4l] = request.security(syminfo.tickerid, "240", [high[1], low[1]], lookahead = barmerge.lookahead_on)
bool p4New = p4h != p4h[1] or p4l != p4l[1]
bool pdNew = pdh != pdh[1] or pdl != pdl[1]
if pdNew
    pdSerial += 1
bool pwNew = pwh != pwh[1] or pwl != pwl[1]
bool pmNew = pmh != pmh[1] or pml != pml[1]
bool fvgUp = low > high[2] and (inFpMin == 0 or low - high[2] >= inFpMin * atrU)
bool fvgDn = high < low[2] and (inFpMin == 0 or low[2] - high >= inFpMin * atrU)

// ─────────────────────────────────────────────────────────────────── LEVEL REGISTRY
type Lvl
    string kind = ""          // NWOG · NDOG · MID · O830 · O930 · PDH · PDL · PWH · PWL · PMH · PML · FPFVG
    float  px = na            // line levels
    float  top = na           // gaps / FVGs
    float  bot = na
    int    dir = 0            // gap: +1 = gapped up (open above the close) · FVG: +1 bullish
    int    bornBar = na
    int    bornDay = 0
    bool   touched = false
    bool   done = false       // filled (gap) · broken (level) · inverted (FVG)
    bool   swept = false      // wick through and close back (levels)
    int    doneBar = na
    string anchor = ""        // FPFVG: which open it followed
    line   ln = na
    line   ce = na
    box    bx = na
    label  lb = na
    label  lb2 = na
    bool   visible = true
    bool   near = false
type Stat
    int nNdog = 0
    int nNdogFilled = 0
    int nNwog = 0
    int nNwogFilled = 0
    int nPdh = 0
    int nPdhTaken = 0
    int nPdl = 0
    int nPdlTaken = 0
    int nFp = 0
    int nFpTouched = 0
    int nFpInverted = 0
type Ev
    bool ndog = false
    bool nwog = false
    bool gapEntered = false
    bool ceTouched = false
    bool pdhSwept = false
    bool pdlSwept = false
    bool pwhSwept = false
    bool pwlSwept = false
    bool fpFormed = false
    bool fpTouched = false
    bool fpInverted = false
    bool macroOpen = false
    bool midCross = false
    bool near = false
    bool kzSwept = false
    string nearWhat = ""
    string what = ""
var array<Lvl> lv = array.new<Lvl>()
var Stat st = Stat.new()
Ev ev = Ev.new()
var array<label> marks = array.new<label>()
if array.size(marks) > 80
    label.delete(array.shift(marks))

isGap(string k) => k == "NDOG" or k == "NWOG"
isFvg(string k) => k == "FPFVG"
isHigh(string k) => k == "PDH" or k == "PWH" or k == "PMH" or k == "P4H" or k == "KZH"
isLow(string k) => k == "PDL" or k == "PWL" or k == "PML" or k == "P4L" or k == "KZL"
kindColor(string k) => k == "NWOG" ? cGapW : k == "NDOG" ? cGapD : k == "MID" ? cOpen : (k == "O830" or k == "O930") ? cOpen2 : isHigh(k) ? cHigh : isLow(k) ? cLow : cFvgU
kindName(string k) => k == "MID" ? "00:00 open" : k == "O830" ? "08:30 open" : k == "O930" ? "09:30 open" : k == "FPFVG" ? "FPFVG" : k
ageOf(Lvl L) => L.kind == "NDOG" ? gapSerial - L.bornDay : (L.kind == "PDH" or L.kind == "PDL") ? pdSerial - L.bornDay : dayNo - L.bornDay
ageTag(Lvl L) => (L.kind == "NDOG" or L.kind == "PDH" or L.kind == "PDL" or L.kind == "MID" or L.kind == "O830" or L.kind == "O930" or isFvg(L.kind)) and ageOf(L) > 0 ? " −" + str.tostring(ageOf(L)) + "d" : ""
lvlRef(Lvl L) => isGap(L.kind) or isFvg(L.kind) ? (L.top + L.bot) / 2 : L.px
lvlName(Lvl L) => L.kind == "KZH" ? L.anchor + " high" : L.kind == "KZL" ? L.anchor + " low" : isFvg(L.kind) ? "FPFVG " + L.anchor : kindName(L.kind)
lvlColor(Lvl L) => isFvg(L.kind) ? (L.dir == 1 ? cFvgU : cFvgD) : (L.kind == "KZH" or L.kind == "KZL") ? sessColor(L.anchor) : kindColor(L.kind)

delLvl(Lvl L) =>
    if not na(L.ln)
        line.delete(L.ln)
    if not na(L.ce)
        line.delete(L.ce)
    if not na(L.bx)
        box.delete(L.bx)
    if not na(L.lb)
        label.delete(L.lb)
    if not na(L.lb2)
        label.delete(L.lb2)

addLine(string kind, float px) =>
    Lvl L = Lvl.new()
    L.kind := kind
    L.px := px
    L.bornBar := bar_index
    L.bornDay := (kind == "PDH" or kind == "PDL") ? pdSerial : dayNo
    color c = kindColor(kind)
    L.ln := line.new(bar_index, px, bar_index + 1, px, extend = extend.right, color = color.new(c, 20), width = kind == "MID" ? 2 : 1, style = kind == "MID" ? line.style_solid : (kind == "O830" or kind == "O930") ? line.style_dashed : line.style_solid)
    L.lb := label.new(bar_index, px, "", style = label.style_label_right, color = color.new(th.bg, 100), textcolor = color.new(c, 10), size = size.tiny)
    array.push(lv, L)
    L

addBox(string kind, float top, float bot, int dir, string anchor) =>
    Lvl L = Lvl.new()
    L.kind := kind
    L.top := top
    L.bot := bot
    L.dir := dir
    L.anchor := anchor
    L.bornBar := bar_index
    L.bornDay := kind == "NDOG" ? gapSerial : dayNo
    color c = kind == "FPFVG" ? (dir == 1 ? cFvgU : cFvgD) : kindColor(kind)
    int x1 = kind == "FPFVG" ? bar_index - 2 : bar_index
    L.bx := box.new(x1, top, bar_index + 1, bot, extend = extend.right, border_color = color.new(c, kind == "NWOG" ? 30 : 50), border_width = 1, bgcolor = color.new(c, kind == "NWOG" ? 80 : kind == "NDOG" ? 86 : 82))
    L.ce := line.new(x1, (top + bot) / 2, bar_index + 1, (top + bot) / 2, extend = extend.right, color = color.new(c, 40), width = 1, style = line.style_dotted)
    L.lb := label.new(bar_index, top, "", style = label.style_label_right, color = color.new(th.bg, 100), textcolor = color.new(c, 10), size = size.tiny)
    if inBoxTags
        L.lb2 := label.new(x1, top, (kind == "FPFVG" ? "FPFVG " + anchor : kind) + " · " + fmt(top - bot), style = label.style_label_lower_right, color = color.new(th.bg, 100), textcolor = color.new(c, 20), size = size.tiny)
    array.push(lv, L)
    L

countKind(string kind) =>
    int n = 0
    if array.size(lv) > 0
        for L in lv
            if L.kind == kind
                n += 1
    n

// ─────────────────────────────────────────────────────────────────── BIRTH (confirmed bars; every level is final when it is born)
var float lastCloseAtT = na
var int   lastCloseDow = 0
var bool  fpArmed930 = false
var bool  fpArmed830 = false
var bool  fpArmedMid = false
var float midOpenPx = na
var bool  ndogOpenToday = false     // an NDOG born today, still to be judged at the next open
var bool  ndogFilledToday = false
var bool  nwogOpen = false
var bool  nwogFilledWk = false
var bool  pdhTakenToday = false
var bool  pdlTakenToday = false
var bool  pdhSeenToday = false
var bool  fpOpen = false
var bool  fpTouchedToday = false
if barstate.isconfirmed
    // ── day roll-over: base-rate bookkeeping for yesterday, prune by days back
    if newDay
        if ndogOpenToday
            st.nNdogFilled += ndogFilledToday ? 1 : 0
        ndogOpenToday := false
        ndogFilledToday := false
        if pdhSeenToday
            st.nPdhTaken += pdhTakenToday ? 1 : 0
            st.nPdlTaken += pdlTakenToday ? 1 : 0
        pdhSeenToday := false
        pdhTakenToday := false
        pdlTakenToday := false
        if fpOpen
            st.nFpTouched += fpTouchedToday ? 1 : 0
        fpOpen := false
        fpTouchedToday := false
        fpArmed930 := false
        fpArmed830 := false
        fpArmedMid := inFpOn and inFpMid
        if afterWeekend and nwogOpen
            st.nNwogFilled += nwogFilledWk ? 1 : 0
            nwogOpen := false
            nwogFilledWk := false
        if array.size(lv) > 0
            for i = array.size(lv) - 1 to 0
                Lvl L = array.get(lv, i)
                int back = L.kind == "NDOG" ? inNdogBack : (L.kind == "MID" or L.kind == "O830" or L.kind == "O930") ? inOpenBack : isFvg(L.kind) ? inFpBack : (L.kind == "NWOG" or L.kind == "PWH" or L.kind == "PWL" or L.kind == "PMH" or L.kind == "PML" or L.kind == "P4H" or L.kind == "P4L") ? 9999 : (L.kind == "KZH" or L.kind == "KZL") ? inKzBack : inLvlBack
                if ageOf(L) >= back
                    delLvl(L)
                    array.remove(lv, i)
        // NWOG: keep the last N
        int nw = countKind("NWOG")
        if nw > inNwogKeep and array.size(lv) > 0
            for i = 0 to array.size(lv) - 1
                Lvl L = array.get(lv, i)
                if L.kind == "NWOG" and nw > inNwogKeep
                    delLvl(L)
                    array.remove(lv, i)
                    nw -= 1
                    break
    if (pdNew or xOpen) and not newDay and array.size(lv) > 0
        for i = array.size(lv) - 1 to 0
            Lvl L = array.get(lv, i)
            if (L.kind == "NDOG" and ageOf(L) >= inNdogBack) or ((L.kind == "PDH" or L.kind == "PDL") and ageOf(L) >= inLvlBack)
                delLvl(L)
                array.remove(lv, i)
    // ── opens
    if newDay and inMidOn
        midOpenPx := open
        addLine("MID", open)
    if x830 and in830On and not newDay
        addLine("O830", open)
    if x930 and in930On and not newDay
        addLine("O930", open)
    if x930 and inFpOn
        fpArmed930 := true
    if x830 and inFpOn and inFp830
        fpArmed830 := true
    // ── previous highs / lows (born when the period rolls)
    if pdNew and inPdOn and not na(pdh)
        addLine("PDH", pdh)
        addLine("PDL", pdl)
        pdhSeenToday := true
        st.nPdh += 1
        st.nPdl += 1
    if pwNew and inPwOn and not na(pwh)
        if array.size(lv) > 0
            for i = array.size(lv) - 1 to 0
                Lvl L = array.get(lv, i)
                if L.kind == "PWH" or L.kind == "PWL"
                    delLvl(L)
                    array.remove(lv, i)
        addLine("PWH", pwh)
        addLine("PWL", pwl)
    if pmNew and inPmOn and not na(pmh)
        if array.size(lv) > 0
            for i = array.size(lv) - 1 to 0
                Lvl L = array.get(lv, i)
                if L.kind == "PMH" or L.kind == "PML"
                    delLvl(L)
                    array.remove(lv, i)
        addLine("PMH", pmh)
        addLine("PML", pml)
    if p4New and inP4On and not na(p4h)
        if array.size(lv) > 0
            for i = array.size(lv) - 1 to 0
                Lvl L = array.get(lv, i)
                if L.kind == "P4H" or L.kind == "P4L"
                    delLvl(L)
                    array.remove(lv, i)
        addLine("P4H", p4h)
        addLine("P4L", p4l)
    // ── opening gaps
    if xOpen and not na(lastCloseAtT)
        float gapSz = math.abs(open - lastCloseAtT)
        if gapSz >= inGapMin * dAtrU
            bool isWeek = lastCloseDow == dayofweek.friday or lastCloseDow == dayofweek.saturday
            if isWeek and inNwogOn
                addBox("NWOG", math.max(open, lastCloseAtT), math.min(open, lastCloseAtT), open > lastCloseAtT ? 1 : -1, "")
                st.nNwog += 1
                nwogOpen := true
                nwogFilledWk := false
                ev.nwog := true
            else if not isWeek and inNdogOn
                addBox("NDOG", math.max(open, lastCloseAtT), math.min(open, lastCloseAtT), open > lastCloseAtT ? 1 : -1, "")
                st.nNdog += 1
                ndogOpenToday := true
                ndogFilledToday := false
                ev.ndog := true
    if insideDay
        lastCloseAtT := close
        lastCloseDow := dow
    // ── first-presented FVG after an anchor
    if (fpArmed930 or fpArmed830 or fpArmedMid) and (fvgUp or fvgDn)
        string anc = fpArmed930 ? "09:30" : fpArmed830 ? "08:30" : "00:00"
        addBox("FPFVG", fvgUp ? low : low[2], fvgUp ? high[2] : high, fvgUp ? 1 : -1, anc)
        st.nFp += 1
        fpOpen := true
        fpTouchedToday := false
        ev.fpFormed := true
        if fpArmed930
            fpArmed930 := false
        else if fpArmed830
            fpArmed830 := false
        else
            fpArmedMid := false
    // ── states (every active level, on the closed bar)
    if array.size(lv) > 0
        for L in lv
            if L.done
                continue
            float refN = lvlRef(L)
            float dN = isGap(L.kind) or isFvg(L.kind) ? (close > L.top ? close - L.top : close < L.bot ? L.bot - close : 0.0) : math.abs(close - L.px)
            bool isNear = inNearAtr > 0 and dN <= inNearAtr * dAtrU
            if isNear and not L.near
                L.near := true
                ev.near := true
                ev.nearWhat := lvlName(L) + " " + fmt(refN)
            else if not isNear
                L.near := false
            if isGap(L.kind)
                bool inside = bar_index > L.bornBar and low <= L.top and high >= L.bot
                if inside and not L.touched
                    L.touched := true
                    ev.gapEntered := true
                    ev.what := L.kind
                float ceP = (L.top + L.bot) / 2
                if bar_index > L.bornBar and low <= ceP and high >= ceP and not L.swept
                    L.swept := true          // for gaps: CE touched
                    ev.ceTouched := true
                bool filled = L.dir == 1 ? low <= L.bot : high >= L.top
                if filled
                    L.done := true
                    L.doneBar := bar_index
                    if L.kind == "NDOG"
                        ndogFilledToday := true
                    else
                        nwogFilledWk := true
            else if isFvg(L.kind)
                bool inside = bar_index > L.bornBar and low <= L.top and high >= L.bot
                if inside and not L.touched
                    L.touched := true
                    fpTouchedToday := true
                    ev.fpTouched := true
                bool inverted = bar_index > L.bornBar and (L.dir == 1 ? close < L.bot : close > L.top)
                if inverted
                    L.done := true
                    L.doneBar := bar_index
                    st.nFpInverted += 1
                    ev.fpInverted := true
                    if inMarkers
                        array.push(marks, label.new(bar_index, L.dir == 1 ? L.bot : L.top, "↻", style = L.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 100), textcolor = color.new(th.flat, 20), size = size.small, tooltip = "FPFVG inverted"))
            else if isHigh(L.kind)
                if high > L.px and not L.touched
                    L.touched := true
                    if close < L.px
                        L.swept := true
                        if L.kind == "PDH"
                            ev.pdhSwept := true
                            pdhTakenToday := true
                        if L.kind == "PWH"
                            ev.pwhSwept := true
                        if L.kind == "KZH"
                            ev.kzSwept := true
                        if inMarkers
                            array.push(marks, label.new(bar_index, high, "⚡", style = label.style_label_down, color = color.new(th.bg, 100), textcolor = color.new(cHigh, 10), size = size.small, tooltip = lvlName(L) + " swept"))
                if close > L.px
                    L.done := true
                    L.doneBar := bar_index
                    if L.kind == "PDH"
                        pdhTakenToday := true
            else if isLow(L.kind)
                if low < L.px and not L.touched
                    L.touched := true
                    if close > L.px
                        L.swept := true
                        if L.kind == "PDL"
                            ev.pdlSwept := true
                            pdlTakenToday := true
                        if L.kind == "PWL"
                            ev.pwlSwept := true
                        if L.kind == "KZL"
                            ev.kzSwept := true
                        if inMarkers
                            array.push(marks, label.new(bar_index, low, "⚡", style = label.style_label_up, color = color.new(th.bg, 100), textcolor = color.new(cLow, 10), size = size.small, tooltip = lvlName(L) + " swept"))
                if close < L.px
                    L.done := true
                    L.doneBar := bar_index
                    if L.kind == "PDL"
                        pdlTakenToday := true
            else
                // opens: crossed = touched (kept drawn; the midnight open is the day's premium / discount line)
                if not L.touched and ((close > L.px and close[1] <= L.px) or (close < L.px and close[1] >= L.px))
                    L.touched := true
                    if L.kind == "MID"
                        ev.midCross := true
        // removal switches
        if inGapRemove or inLvlRemove
            for i = array.size(lv) - 1 to 0
                Lvl L = array.get(lv, i)
                if L.done and ((inGapRemove and isGap(L.kind)) or (inLvlRemove and (isHigh(L.kind) or isLow(L.kind))))
                    delLvl(L)
                    array.remove(lv, i)

// ─────────────────────────────────────────────────────────────────── MACRO WINDOWS
// ─────────────────────────────────────────────────────────────────── KILLZONES (session boxes → liquidity lines)
type Kz
    string name = ""
    bool   live = false
    float  hi = na
    float  lo = na
    int    startBar = na
    box    bx = na
    label  lb = na
var array<Kz> kz = array.from(Kz.new(name = "Asia"), Kz.new(name = "London"), Kz.new(name = "NY AM"), Kz.new(name = "NY PM"))
bool inA  = not na(time(timeframe.period, inKzAsia, inTz))
bool inL  = not na(time(timeframe.period, inKzLon, inTz))
bool inN1 = not na(time(timeframe.period, inKzAm, inTz))
bool inN2 = not na(time(timeframe.period, inKzPm, inTz))
array<bool> kzIn   = array.from(inA, inL, inN1, inN2)
array<bool> kzWas  = array.from(inA[1], inL[1], inN1[1], inN2[1])
var array<box>   kzBoxes = array.new<box>()
var array<label> kzTags  = array.new<label>()
var array<int>   kzDay   = array.new<int>()
if barstate.isconfirmed and inKzOn
    if newDay and array.size(kzBoxes) > 0
        for i = array.size(kzBoxes) - 1 to 0
            if dayNo - array.get(kzDay, i) >= inKzBack
                box.delete(array.get(kzBoxes, i))
                label.delete(array.get(kzTags, i))
                array.remove(kzBoxes, i)
                array.remove(kzTags, i)
                array.remove(kzDay, i)
    for i = 0 to 3
        Kz K = array.get(kz, i)
        bool isIn = array.get(kzIn, i)
        bool wasIn = array.get(kzWas, i)
        color kc = sessColor(K.name)
        if isIn and not wasIn
            K.live := true
            K.hi := high
            K.lo := low
            K.startBar := bar_index
            K.bx := box.new(bar_index, high, bar_index, low, border_color = color.new(kc, 65), border_width = 1, bgcolor = color.new(kc, 88))
            K.lb := label.new(bar_index, high, inBoxTags ? K.name : "", style = label.style_label_lower_right, color = color.new(th.bg, 100), textcolor = color.new(kc, 15), size = size.tiny)
            array.push(kzBoxes, K.bx)
            array.push(kzTags, K.lb)
            array.push(kzDay, dayNo)
        else if isIn and K.live
            K.hi := math.max(K.hi, high)
            K.lo := math.min(K.lo, low)
            box.set_top(K.bx, K.hi)
            box.set_bottom(K.bx, K.lo)
            box.set_right(K.bx, bar_index)
            label.set_y(K.lb, K.hi)
        else if not isIn and wasIn and K.live
            K.live := false
            if inKzLines and not na(K.hi)
                Lvl H = addLine("KZH", K.hi)
                H.anchor := K.name
                Lvl Lo = addLine("KZL", K.lo)
                Lo.anchor := K.name

// ─────────────────────────────────────────────────────────────────── DEALING RANGE (premium / discount between PDH and PDL)
var box   drPrem = na
var box   drDisc = na
var line  drEq = na
var label drL1 = na
var label drL2 = na
var float drEqPx = na
if barstate.isconfirmed and inDrOn and pdNew and not na(pdh) and pdh > pdl
    if not na(drPrem)
        box.delete(drPrem)
        box.delete(drDisc)
        line.delete(drEq)
        label.delete(drL1)
        label.delete(drL2)
    drEqPx := (pdh + pdl) / 2
    drPrem := box.new(bar_index, pdh, bar_index + 1, drEqPx, extend = extend.right, border_color = color.new(cPrem, 100), bgcolor = color.new(cPrem, 93))
    drDisc := box.new(bar_index, drEqPx, bar_index + 1, pdl, extend = extend.right, border_color = color.new(cDisc, 100), bgcolor = color.new(cDisc, 93))
    drEq   := line.new(bar_index, drEqPx, bar_index + 1, drEqPx, extend = extend.right, color = color.new(th.flat, 45), width = 1, style = line.style_dotted)
    drL1   := label.new(bar_index, pdh, inBoxTags ? "premium" : "", style = label.style_label_lower_right, color = color.new(th.bg, 100), textcolor = color.new(cPrem, 40), size = size.tiny)
    drL2   := label.new(bar_index, pdl, inBoxTags ? "discount" : "", style = label.style_label_upper_right, color = color.new(th.bg, 100), textcolor = color.new(cDisc, 40), size = size.tiny)
string drTxt = na(drEqPx) ? "—" : (close > drEqPx ? "premium" : close < drEqPx ? "discount" : "at equilibrium") + " · EQ " + fmt(drEqPx) + " · " + str.tostring(math.abs(close - drEqPx) / math.max(pdh - pdl, syminfo.mintick) * 200, "0") + " % of the half-range away"

var array<int>    macS = array.from(153, 243, 530, 590, 650, 710, 790, 915)
var array<int>    macE = array.from(180, 270, 550, 610, 670, 730, 820, 945)
var array<string> macN = array.from("London 02:33–03:00", "London 04:03–04:30", "NY AM 08:50–09:10", "NY AM 09:50–10:10", "NY AM 10:50–11:10", "Lunch 11:50–12:10", "NY PM 13:10–13:40", "NY PM 15:15–15:45")
macOn(int i) => i < 2 ? inMacLon : i < 5 ? inMacAm : i < 6 ? inMacLunch : inMacPm
bool anyMac = inMacLon or inMacAm or inMacLunch or inMacPm
int    macIdx  = -1          // window in progress
int    macNext = -1          // next window today
bool   macStartNow = false
bool   macEndNow = false
int    prevMin = nz(nowMin[1], -1)
for i = 0 to 7
    if macOn(i)
        int s0 = array.get(macS, i)
        int e0 = array.get(macE, i)
        if nowMin >= s0 and nowMin < e0 and macIdx < 0
            macIdx := i
        if s0 > nowMin and macNext < 0
            macNext := i
        if nowMin >= s0 and (newDay or prevMin < s0) and nowMin < e0
            macStartNow := true
        if nowMin >= e0 and (newDay or prevMin < e0)
            macEndNow := true
if macStartNow
    ev.macroOpen := true
var array<line>  macLines = array.new<line>()
var array<int>   macLineDay = array.new<int>()
var array<label> macTags  = array.new<label>()
var array<int>   macTagDay = array.new<int>()
bgcolor(inMacStyle == "Shade + markers" and macIdx >= 0 ? color.new(cMac, 92) : na, title = "Macro window")
if barstate.isconfirmed and inMacStyle != "Off"
    if newDay and array.size(macLines) > 0
        for i = array.size(macLines) - 1 to 0
            if dayNo - array.get(macLineDay, i) >= inMacBack
                line.delete(array.get(macLines, i))
                array.remove(macLines, i)
                array.remove(macLineDay, i)
    if newDay and array.size(macTags) > 0
        for i = array.size(macTags) - 1 to 0
            if dayNo - array.get(macTagDay, i) >= inMacBack
                label.delete(array.get(macTags, i))
                array.remove(macTags, i)
                array.remove(macTagDay, i)
    if macStartNow
        array.push(macLines, line.new(bar_index, low, bar_index, high, extend = extend.both, color = color.new(cMac, 65), width = 1, style = line.style_dotted))
        array.push(macLineDay, dayNo)
        array.push(macTagDay, dayNo)
        array.push(macTags, label.new(bar_index, high, "macro " + str.substring(array.get(macN, macIdx >= 0 ? macIdx : 0), str.length(array.get(macN, macIdx >= 0 ? macIdx : 0)) - 11, str.length(array.get(macN, macIdx >= 0 ? macIdx : 0))), style = label.style_label_down, color = color.new(th.bg, 100), textcolor = color.new(cMac, 20), size = size.tiny, yloc = yloc.abovebar))
    if macEndNow
        array.push(macLines, line.new(bar_index, low, bar_index, high, extend = extend.both, color = color.new(cMac, 82), width = 1, style = line.style_dotted))
        array.push(macLineDay, dayNo)
string macTxt = macIdx >= 0 ? array.get(macN, macIdx) + " · in progress · " + str.tostring(array.get(macE, macIdx) - nowMin) + " m left" : macNext >= 0 ? array.get(macN, macNext) + " · opens in " + str.tostring(array.get(macS, macNext) - nowMin) + " m" : anyMac ? "no more macros today" : "macros off"

// ─────────────────────────────────────────────────────────────────── DISPLAY (last bar): proximity, labels, fading, nearest levels
float band = inProxOn ? inProxAtr * dAtrU : 1e12
int   hiddenN = 0
int   activeN = 0
// nearest levels above / below (three each)
var array<string> nearUpTxt = array.new<string>()
var array<string> nearDnTxt = array.new<string>()
var array<float>  nearUpD = array.new<float>()
var array<float>  nearDnD = array.new<float>()
stateWord(Lvl L) =>
    isGap(L.kind) ? (L.done ? "filled" : L.swept ? "CE touched" : L.touched ? "entered" : "open") : isFvg(L.kind) ? (L.done ? "inverted" : L.touched ? "touched" : "fresh") : (isHigh(L.kind) or isLow(L.kind)) ? (L.done ? "broken" : L.swept ? "swept" : L.touched ? "tested" : "resting") : (L.touched ? "crossed" : "untested")
if barstate.islast
    array.clear(nearUpTxt)
    array.clear(nearDnTxt)
    array.clear(nearUpD)
    array.clear(nearDnD)
    if array.size(lv) > 0
        // sort indices by distance for label staggering
        int stagger = 0
        float lastLabelPx = na
        for L in lv
            activeN += 1
            float ref = lvlRef(L)
            float dist = isGap(L.kind) or isFvg(L.kind) ? (close > L.top ? close - L.top : close < L.bot ? L.bot - close : 0.0) : math.abs(close - L.px)
            bool vis = dist <= band
            L.visible := vis
            if not vis
                hiddenN += 1
            color c = lvlColor(L)
            int a = vis ? (L.done ? inFadeAlpha : (L.kind == "NWOG" ? 20 : L.kind == "MID" ? 15 : 25)) : 100
            int aFill = vis ? (L.done ? math.min(97, inFadeAlpha + 12) : (L.kind == "NWOG" ? 80 : isFvg(L.kind) ? 82 : 86)) : 100
            if not na(L.lb2)
                label.set_textcolor(L.lb2, color.new(c, vis ? (L.done ? math.min(92, inFadeAlpha + 10) : 20) : 100))
                label.set_y(L.lb2, vis ? L.top : close)
            // park hidden objects on the current price so the auto-scale ignores them; snap back when visible
            float yLine = vis ? L.px : close
            float yTop  = vis ? L.top : close
            float yBot  = vis ? L.bot : close
            int endX = L.done and not na(L.doneBar) ? L.doneBar : bar_index + 1
            if not na(L.ln)
                line.set_color(L.ln, color.new(c, a))
                line.set_style(L.ln, L.done ? line.style_dashed : (L.kind == "O830" or L.kind == "O930") ? line.style_dashed : line.style_solid)
                line.set_y1(L.ln, yLine)
                line.set_y2(L.ln, yLine)
                line.set_extend(L.ln, L.done ? extend.none : extend.right)
                line.set_x2(L.ln, endX)
            if not na(L.bx)
                box.set_bgcolor(L.bx, color.new(c, aFill))
                box.set_border_color(L.bx, color.new(c, vis ? (L.done ? inFadeAlpha : 55) : 100))
                box.set_top(L.bx, yTop)
                box.set_bottom(L.bx, yBot)
                box.set_extend(L.bx, L.done ? extend.none : extend.right)
                box.set_right(L.bx, endX)
            if not na(L.ce)
                line.set_color(L.ce, color.new(c, vis ? (L.done ? math.min(97, inFadeAlpha + 8) : 45) : 100))
                line.set_y1(L.ce, (yTop + yBot) / 2)
                line.set_y2(L.ce, (yTop + yBot) / 2)
                line.set_extend(L.ce, L.done ? extend.none : extend.right)
                line.set_x2(L.ce, endX)
            if not na(L.lb)
                string nm = lvlName(L) + ageTag(L)
                string sw = stateWord(L)
                string txt = inLabels == "Off" or not vis ? "" : inLabels == "Full" ? nm + " · " + fmt(ref) + (sw == "resting" or sw == "open" or sw == "fresh" or sw == "untested" ? "" : " · " + sw) : nm + (L.done or L.swept ? " · " + sw : "")
                label.set_text(L.lb, txt)
                label.set_size(L.lb, L.done ? size.tiny : size.small)
                label.set_textcolor(L.lb, color.new(c, vis ? (L.done ? math.min(90, inFadeAlpha) : 10) : 100))
                label.set_y(L.lb, vis ? (isGap(L.kind) or isFvg(L.kind) ? L.top : L.px) : close)
                label.set_x(L.lb, L.done and not na(L.doneBar) ? L.doneBar : bar_index + inLabelGap)
            // nearest lists (active, not done)
            if not L.done
                string row = lvlName(L) + ageTag(L) + " " + fmt(ref) + " · " + (dist == 0 ? "inside" : str.tostring(dist, format.mintick) + " (" + str.tostring(dist / dAtrU, "0.00") + " ATR)") + (L.touched ? " · " + stateWord(L) : "")
                if ref > close
                    array.push(nearUpTxt, row)
                    array.push(nearUpD, dist)
                else if ref < close
                    array.push(nearDnTxt, row)
                    array.push(nearDnD, dist)
                else
                    array.push(nearUpTxt, row)
                    array.push(nearUpD, 0.0)
    // stagger the labels of live levels that share (almost) the same price
    if array.size(lv) > 1
        array<float> ly = array.new<float>()
        array<int>   li = array.new<int>()
        for i = 0 to array.size(lv) - 1
            Lvl L = array.get(lv, i)
            if L.visible and not L.done and not na(L.lb)
                array.push(ly, isGap(L.kind) or isFvg(L.kind) ? L.top : L.px)
                array.push(li, i)
        if array.size(ly) > 1
            for i = 0 to array.size(ly) - 2
                for j = i + 1 to array.size(ly) - 1
                    if array.get(ly, j) < array.get(ly, i)
                        float ty = array.get(ly, i)
                        array.set(ly, i, array.get(ly, j))
                        array.set(ly, j, ty)
                        int ti = array.get(li, i)
                        array.set(li, i, array.get(li, j))
                        array.set(li, j, ti)
            int k = 0
            for i = 1 to array.size(ly) - 1
                k := array.get(ly, i) - array.get(ly, i - 1) < 0.05 * dAtrU ? k + 1 : 0
                if k > 0
                    Lvl L = array.get(lv, array.get(li, i))
                    label.set_x(L.lb, bar_index + inLabelGap - k * 12)
    // sort the nearest lists by distance (selection sort, tiny arrays)
    if array.size(nearUpD) > 1
        for i = 0 to array.size(nearUpD) - 2
            for j = i + 1 to array.size(nearUpD) - 1
                if array.get(nearUpD, j) < array.get(nearUpD, i)
                    float td = array.get(nearUpD, i)
                    array.set(nearUpD, i, array.get(nearUpD, j))
                    array.set(nearUpD, j, td)
                    string tt = array.get(nearUpTxt, i)
                    array.set(nearUpTxt, i, array.get(nearUpTxt, j))
                    array.set(nearUpTxt, j, tt)
    if array.size(nearDnD) > 1
        for i = 0 to array.size(nearDnD) - 2
            for j = i + 1 to array.size(nearDnD) - 1
                if array.get(nearDnD, j) < array.get(nearDnD, i)
                    float td = array.get(nearDnD, i)
                    array.set(nearDnD, i, array.get(nearDnD, j))
                    array.set(nearDnD, j, td)
                    string tt = array.get(nearDnTxt, i)
                    array.set(nearDnTxt, i, array.get(nearDnTxt, j))
                    array.set(nearDnTxt, j, tt)

// today's objects for the panel
Lvl todayNdog = na
Lvl todayNwog = na
Lvl todayFp = na
Lvl todayMid = na
if array.size(lv) > 0
    for L in lv
        if L.kind == "NDOG"
            todayNdog := L
        if L.kind == "NWOG"
            todayNwog := L
        if L.kind == "FPFVG"
            todayFp := L
        if L.kind == "MID"
            todayMid := L
gapTxt(Lvl L) => na(L) ? "none" : fmt(L.bot) + " – " + fmt(L.top) + " · " + str.tostring((L.top - L.bot) / dAtrU, "0.00") + " ATR · " + (L.dir == 1 ? "gapped up" : "gapped down") + " · " + stateWord(L)
string fpTxt = na(todayFp) ? (inFpOn ? (fpArmed930 or fpArmed830 or fpArmedMid ? "waiting for the first FVG" : "none yet today") : "off") : (todayFp.dir == 1 ? "bullish " : "bearish ") + fmt(todayFp.bot) + " – " + fmt(todayFp.top) + " · after " + todayFp.anchor + " · " + stateWord(todayFp)
string midTxt = na(todayMid) ? "—" : fmt(todayMid.px) + (close > todayMid.px ? " · price above (premium side)" : close < todayMid.px ? " · price below (discount side)" : " · at the open")
string sitTxt = "nearest above: " + (array.size(nearUpTxt) > 0 ? array.get(nearUpTxt, 0) : "nothing tracked") + " · nearest below: " + (array.size(nearDnTxt) > 0 ? array.get(nearDnTxt, 0) : "nothing tracked")

// ─────────────────────────────────────────────────────────────────── DASHBOARD
posOf(string p) =>
    p == "Top Left" ? position.top_left : p == "Bottom Right" ? position.bottom_right : p == "Bottom Left" ? position.bottom_left : p == "Middle Right" ? position.middle_right : p == "Middle Left" ? position.middle_left : position.top_right
szLead() => inSize == "Small" ? size.small : inSize == "Large" ? size.large  : size.normal
szBody() => inSize == "Small" ? size.tiny  : inSize == "Large" ? size.normal : size.small
szSub()  => inSize == "Small" ? size.tiny  : inSize == "Large" ? size.small  : size.tiny
row2(table tb, int r, string k, string v, color vc) =>
    color stripe = r % 2 == 1 ? color.new(th.accent, 94) : th.bg
    table.cell(tb, 0, r, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = stripe)
    table.cell(tb, 1, r, v, text_color = vc, text_size = szBody(), text_halign = text.align_left, text_font_family = font.family_monospace, bgcolor = stripe)
section(table tb, int r, string title) =>
    table.cell(tb, 0, r, title, text_color = th.accent, text_size = szSub(), text_halign = text.align_left, bgcolor = color.new(th.accent, 90))
    table.cell(tb, 1, r, "", bgcolor = color.new(th.accent, 90))
    table.merge_cells(tb, 0, r, 1, r)
pct(int a, int b) => b == 0 ? "—" : str.tostring(math.round(a / b * 100)) + " % (" + str.tostring(a) + " of " + str.tostring(b) + ")"

var table dash = table.new(posOf(inDashPos), 2, 24, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_width = 0)
if barstate.islast and inDash
    table.clear(dash, 0, 0, 1, 23)
    int r = 0
    table.cell(dash, 0, r, "ICT LEVELS DESK", text_color = th.accentHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Opening gaps, opens, previous highs / lows, the first-presented FVG and the macro windows — every level with its state, only the near ones drawn. Context, not signals.")
    table.cell(dash, 1, r, syminfo.ticker + " · " + timeframe.period + " · " + hhmm + " " + (inTz == "America/New_York" ? "NY" : inTz), text_color = th.accent, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
    r += 1
    table.cell(dash, 0, r, "● " + fmt(close), text_color = th.txtHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg)
    table.cell(dash, 1, r, str.tostring(activeN) + " levels tracked · " + str.tostring(hiddenN) + " hidden beyond " + str.tostring(inProxAtr, "0.0") + " × daily ATR", text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
    r += 1
    section(dash, r, "NEAREST ABOVE")
    r += 1
    if array.size(nearUpTxt) == 0
        row2(dash, r, "—", "nothing tracked above price", th.flat)
        r += 1
    else
        for i = 0 to math.min(2, array.size(nearUpTxt) - 1)
            row2(dash, r, "▲ " + str.tostring(i + 1), array.get(nearUpTxt, i), th.txt)
            r += 1
    section(dash, r, "NEAREST BELOW")
    r += 1
    if array.size(nearDnTxt) == 0
        row2(dash, r, "—", "nothing tracked below price", th.flat)
        r += 1
    else
        for i = 0 to math.min(2, array.size(nearDnTxt) - 1)
            row2(dash, r, "▼ " + str.tostring(i + 1), array.get(nearDnTxt, i), th.txt)
            r += 1
    section(dash, r, "TODAY")
    r += 1
    if inMidOn
        row2(dash, r, "00:00 open", midTxt, th.txt)
        r += 1
    if inDrOn
        row2(dash, r, "Dealing range", drTxt, na(drEqPx) ? th.flat : close > drEqPx ? cPrem : cDisc)
        r += 1
    if inNdogOn
        row2(dash, r, "NDOG", gapTxt(todayNdog), na(todayNdog) ? th.flat : todayNdog.done ? th.flat : th.txt)
        r += 1
    if inNwogOn
        row2(dash, r, "NWOG", gapTxt(todayNwog), na(todayNwog) ? th.flat : todayNwog.done ? th.flat : th.txt)
        r += 1
    if inFpOn
        row2(dash, r, "FPFVG", fpTxt, na(todayFp) ? th.flat : todayFp.done ? th.flat : (todayFp.dir == 1 ? th.up : th.down))
        r += 1
    row2(dash, r, "Macro", macTxt, macIdx >= 0 ? th.accentHi : th.txt)
    r += 1
    if inShowRec
        section(dash, r, "BASE RATES ON THIS CHART · DESCRIPTIVE, NOT PREDICTIVE")
        r += 1
        row2(dash, r, "NDOG filled same day", pct(st.nNdogFilled, st.nNdog), th.txt)
        r += 1
        row2(dash, r, "NWOG filled same week", pct(st.nNwogFilled, st.nNwog), th.txt)
        r += 1
        row2(dash, r, "PDH · PDL taken", pct(st.nPdhTaken, st.nPdh) + " · " + pct(st.nPdlTaken, st.nPdl), th.txt)
        r += 1
        row2(dash, r, "FPFVG revisited · inverted", pct(st.nFpTouched, st.nFp) + " · " + str.tostring(st.nFpInverted), th.txt)
        r += 1
    string mods = (inNdogOn ? "NDOG " + str.tostring(inNdogBack) + "d · " : "") + (inNwogOn ? "NWOG ×" + str.tostring(inNwogKeep) + " · " : "") + (inMidOn or in830On or in930On ? "opens " + str.tostring(inOpenBack) + "d · " : "") + (inPdOn ? "PD " : "") + (inPwOn ? "PW " : "") + (inPmOn ? "PM " : "") + (inPdOn or inPwOn or inPmOn ? str.tostring(inLvlBack) + "d · " : "") + (inFpOn ? "FPFVG " + str.tostring(inFpBack) + "d" : "")
    table.cell(dash, 0, r, mods, text_color = th.flat, text_size = szSub(), text_halign = text.align_left, bgcolor = th.bg)
    table.cell(dash, 1, r, "gaps " + inCloseTime + " → " + inOpenTime + " · " + (inProxOn ? "proximity on" : "proximity off"), text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)

// ─────────────────────────────────────────────────────────────────── CO-PILOT
wrap(string s, int width) =>
    array<string> words = str.split(s, " ")
    string out = ""
    string line = ""
    for w in words
        if str.length(line) + str.length(w) + 1 > width and line != ""
            out := out + line + "\n"
            line := w
        else
            line := line == "" ? w : line + " " + w
    out + line
narrNow() =>
    "price " + fmt(close) + " · " + sitTxt + (na(todayNdog) ? "" : "; today's NDOG " + fmt(todayNdog.bot) + "–" + fmt(todayNdog.top) + " is " + stateWord(todayNdog)) + (na(todayFp) ? "" : "; the " + todayFp.anchor + " first-presented FVG is " + (todayFp.dir == 1 ? "bullish" : "bearish") + " and " + stateWord(todayFp)) + "; " + macTxt
narrWhy() =>
    "The ICT reading of these levels: opening gaps (NDOG / NWOG) are inefficiencies the market tends to revisit — their midline is the consequent encroachment; the midnight open frames the day (above it is premium, below it discount), the 08:30 and 09:30 opens do the same for the sessions; previous highs and lows are resting liquidity that gets swept before reversals; the first FVG after the open is the day's first imbalance; macro windows are the minutes the algorithm is said to seek liquidity. This desk shows where they are and what state each one is in — it does not say what price will do."
narrWatch() =>
    (array.size(nearUpTxt) > 0 ? "Above: " + array.get(nearUpTxt, 0) + ". " : "") + (array.size(nearDnTxt) > 0 ? "Below: " + array.get(nearDnTxt, 0) + ". " : "") + (macIdx >= 0 ? "A macro is in progress — sweeps of the nearest level here are the classic read. " : macNext >= 0 ? "The next macro is " + array.get(macN, macNext) + ". " : "") + "Levels hidden by the proximity filter come back when price gets within " + str.tostring(inProxAtr, "0.0") + " × the daily ATR."
narrRates() =>
    st.nNdog + st.nPdh < 10 ? "Too few days on this chart to say anything yet." : "On this chart the NDOG filled the same day " + pct(st.nNdogFilled, st.nNdog) + ", the NWOG the same week " + pct(st.nNwogFilled, st.nNwog) + ", PDH was taken " + pct(st.nPdhTaken, st.nPdh) + " of days and PDL " + pct(st.nPdlTaken, st.nPdl) + ", the first-presented FVG was revisited " + pct(st.nFpTouched, st.nFp) + ". Descriptive base rates of the past on THIS chart — not a forecast."
narrPosOf(string p) =>
    p == "Top Left" ? position.top_left : p == "Top Right" ? position.top_right : p == "Bottom Right" ? position.bottom_right : p == "Middle Left" ? position.middle_left : p == "Middle Right" ? position.middle_right : position.bottom_left
narrBodySz() => inNarrSize == "Small" ? size.small : inNarrSize == "Medium" ? size.normal : inNarrSize == "Large" ? size.large : szBody()
narrHeadSz() => inNarrSize == "Small" ? size.normal : inNarrSize == "Medium" ? size.large : inNarrSize == "Large" ? size.large : szLead()

var table narrT = na
if barstate.islast
    if not na(narrT)
        table.delete(narrT)
        narrT := na
    if inNarr != "Off"
        array<string> labs = array.new<string>()
        array<string> vals = array.new<string>()
        array.push(labs, "NOW")
        array.push(vals, wrap(narrNow() + " (context, not a trade instruction)", inNarrWrap))
        bool deep = inNarr == "Standard" or inNarr == "Detailed"
        if deep
            array.push(labs, "WHY")
            array.push(vals, wrap(narrWhy(), inNarrWrap))
            array.push(labs, "WATCH")
            array.push(vals, wrap(narrWatch(), inNarrWrap))
        if inNarr == "Detailed"
            array.push(labs, "BASE RATES")
            array.push(vals, wrap(narrRates(), inNarrWrap))
            array.push(labs, "GLOSSARY")
            array.push(vals, wrap("NDOG / NWOG = the new-day / new-week opening gap between the day-close and the day-open prices. CE = consequent encroachment, the midline of a gap or FVG. FPFVG = the first fair value gap presented after an open. Swept = the wick went through a high or low and the bar closed back. Broken = a close beyond it. Inverted = a close through the far side of an FVG.", inNarrWrap))
        int nr = array.size(labs)
        bool atBottom = inNarrPos == "Bottom Left" or inNarrPos == "Bottom Right"
        int rows = nr + 2 + (atBottom ? 1 : 0)
        narrT := table.new(narrPosOf(inNarrPos), 2, rows, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_color = color.new(th.frame, 70), border_width = 1)
        table.cell(narrT, 0, 0, "◆ CO-PILOT", text_color = th.accentHi, bgcolor = color.new(th.accent, 88), text_size = narrHeadSz(), text_halign = text.align_left)
        table.cell(narrT, 1, 0, (macIdx >= 0 ? "MACRO IN PROGRESS" : "LEVELS") + " · " + str.upper(inNarr), text_color = th.accent, bgcolor = color.new(th.accent, 88), text_size = narrBodySz(), text_halign = text.align_right)
        for i = 0 to nr - 1
            table.cell(narrT, 0, i + 1, array.get(labs, i), text_color = th.accent, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
            table.cell(narrT, 1, i + 1, array.get(vals, i), text_color = th.txt, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
        table.cell(narrT, 0, nr + 1, "", bgcolor = th.bg)
        table.cell(narrT, 1, nr + 1, "Honest: levels are where price has been, not where it must go. The base rates describe this chart's past.", text_color = th.flat, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left)
        if atBottom
            table.cell(narrT, 0, nr + 2, " ", bgcolor = th.bg)
            table.cell(narrT, 1, nr + 2, " ", bgcolor = th.bg)

// ─────────────────────────────────────────────────────────────────── ALERTS (closed bars by construction)
payload(string evName, string what) =>
    string js = '{"src":"ICTLevelsDesk","symbol":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","event":"' + evName + '","what":"' + what + '","price":' + fmt(close) + ',"nearestAbove":"' + (array.size(nearUpTxt) > 0 ? array.get(nearUpTxt, 0) : "") + '","nearestBelow":"' + (array.size(nearDnTxt) > 0 ? array.get(nearDnTxt, 0) : "") + '","macro":"' + macTxt + '"}'
    string tx = "ICT Levels Desk · " + syminfo.ticker + " " + timeframe.period + " · " + evName + (what != "" ? " · " + what : "") + " · " + fmt(close)
    inAlertJson ? js : tx
alertcondition(ev.ndog, "NDOG formed", "ICT Levels Desk: a new-day opening gap formed")
alertcondition(ev.nwog, "NWOG formed", "ICT Levels Desk: a new-week opening gap formed")
alertcondition(ev.gapEntered, "Price entered a gap", "ICT Levels Desk: price traded into an opening gap")
alertcondition(ev.ceTouched, "Gap CE touched", "ICT Levels Desk: price touched a gap's consequent encroachment")
alertcondition(ev.pdhSwept, "PDH swept", "ICT Levels Desk: the previous day high was swept (wick through, close back)")
alertcondition(ev.pdlSwept, "PDL swept", "ICT Levels Desk: the previous day low was swept (wick through, close back)")
alertcondition(ev.pwhSwept, "PWH swept", "ICT Levels Desk: the previous week high was swept")
alertcondition(ev.pwlSwept, "PWL swept", "ICT Levels Desk: the previous week low was swept")
alertcondition(ev.fpFormed, "First-presented FVG formed", "ICT Levels Desk: the first FVG after the open printed")
alertcondition(ev.fpTouched, "First-presented FVG touched", "ICT Levels Desk: price traded back into the first-presented FVG")
alertcondition(ev.fpInverted, "First-presented FVG inverted", "ICT Levels Desk: a bar closed through the first-presented FVG")
alertcondition(ev.macroOpen, "Macro window opening", "ICT Levels Desk: a macro window just opened")
alertcondition(ev.midCross, "Midnight open crossed", "ICT Levels Desk: price closed across the midnight open")
alertcondition(ev.near, "Price near a level", "ICT Levels Desk: price came within the set distance of a live level")
alertcondition(ev.kzSwept, "Session high / low swept", "ICT Levels Desk: a killzone session high or low was swept")
if ev.ndog
    alert(payload("NDOG", ""), alert.freq_once_per_bar_close)
if ev.nwog
    alert(payload("NWOG", ""), alert.freq_once_per_bar_close)
if ev.gapEntered
    alert(payload("GAP_ENTERED", ev.what), alert.freq_once_per_bar_close)
if ev.pdhSwept or ev.pdlSwept or ev.pwhSwept or ev.pwlSwept
    alert(payload("SWEEP", ev.pdhSwept ? "PDH" : ev.pdlSwept ? "PDL" : ev.pwhSwept ? "PWH" : "PWL"), alert.freq_once_per_bar_close)
if ev.fpFormed
    alert(payload("FPFVG_FORMED", ""), alert.freq_once_per_bar_close)
if ev.fpInverted
    alert(payload("FPFVG_INVERTED", ""), alert.freq_once_per_bar_close)
if ev.macroOpen
    alert(payload("MACRO_OPEN", macTxt), alert.freq_once_per_bar_close)
if ev.near
    alert(payload("NEAR_LEVEL", ev.nearWhat), alert.freq_once_per_bar_close)
Download the indicator .pine file
Which clock times should I use?

The shipped New York preset is 16:00 → 18:00. The source gives 16:00 → 09:30 for regular-hours stocks. Match the session your chart actually contains; these are not universal market hours. Missing clock-time candles can change which references appear.

Go deeper, one question at a time.

All reference types, rules and settings remain here.

What are all the references?
  1. Old pricesPrevious day, week, month or four-hour highs and lows; midnight, 08:30 and 09:30 opening prices.
  2. Price gapsAn opening gap compares a tracked close with the next open. FVG means a gap between the first and third candles of a three-candle pattern.
  3. Time and rangesSession boxes, their completed highs and lows, fixed macro windows and the previous-day midpoint.

NDOG names a new-day opening gap; NWOG a new-week opening gap. FPFVG means the first qualifying FVG from an enabled anchor time. These names describe recorded prices and conventions, not confirmed orders.

When does a line or box finish?

High or low: resting until the first strict wick-through. That first test is swept only if it closes back inside; a close exactly on the level is tested. A strict close beyond breaks it. Tested and swept remain live until broken. A later return cannot relabel the first test or revive a broken line.

Opening gap: open, entered, midpoint touched, filled. A wick reaching the far edge fills it, even on its birth candle. It can skip intermediate states.

First FVG: fresh, touched, inverted. A later candle must close strictly beyond the far edge to invert it. A wick through, or a close exactly on the edge, is not enough. Its birth candle cannot touch or invert it.

Same wick to 100

For an already-existing upward box from 100 to 106, a candle with low 100 and close 104 fills an opening gap.

Different kind of box

The same candle only touches a bullish FVG. Inversion requires a later close below 100.

Filled, broken and inverted drawings end at their event candle and fade. Separate switches remove filled gaps or broken high/low lines; neither removes inverted FVGs. Opening-price lines stay live after a crossing.

Why are some references hidden?

The proximity filter draws registered references within 2 daily ATR by default. ATR measures recent price movement, including gaps. Distance is to a line or a box’s nearest edge; inside a box, it is zero.

Hidden references still update and may appear in the nearest lists. Box midpoint decides its above/below list. The tracked count includes retained finished references. Session boxes, macro shading and previous-day range shading are separate from this filter.

Proximity and gap-size rules use the last closed daily ATR 14, falling back to ten times chart ATR 14. FVG size uses chart ATR 14. Near alerts have their own radius.

When is each reference created?

Previous periods: completed day, week, month or four-hour extremes are received on confirmed chart bars. A new pair is created when the returned high/low pair changes; an identical pair does not create another. Daily history is limited by age; other period pairs replace their predecessors.

Opening gaps: compare the last tracked close inside the configured clock window with the opening bar’s open. Creation commits at that bar’s close. A close captured on Friday or Saturday makes a weekend gap, not a second daily gap. The minimum size is inclusive.

Opening prices: use available bars at or after their clock time. The first local-date bar can stand in for midnight. Separate 08:30 and 09:30 opens are suppressed if that candle is also the first date bar.

First FVG: bullish means third-candle low above first-candle high; bearish mirrors this. The first bar at or after an anchor can confirm the pattern using two earlier bars. If several anchors are armed, priority is 09:30, then 08:30, then midnight; one pattern uses one anchor.

Session lines: born on the first closed candle outside the session, after that candle’s state checks. Their tests start on the following candle.

What do the shaded areas and clock mean?

Session boxes, also called killzones, expand as session candles close. Their highs and lows can become references afterward. Turning off session tracking also stops those lines being created.

The previous day’s midpoint splits its range: premium is the upper half, discount the lower half. These describe position, not fair value or an entry.

Macro windows are fixed time conventions, not tested predictions. London: 02:33–03:00 and 04:03–04:30. New York AM: 08:50–09:10, 09:50–10:10 and 10:50–11:10. Lunch: 11:50–12:10. New York PM: 13:10–13:40 and 15:15–15:45.

All use the selected timezone. Four groups can be enabled; their eight times are not editable. Hiding macro drawings leaves the enabled countdown available. It follows chart-bar time, not a running wall clock.

How do I read the dashboard and Co-Pilot?

Price: current price, tracked count and hidden count. Nearest above / below: three unfinished references each way, their state and distance in points and daily ATR.

Open: which side of midnight price is on. NDOG / NWOG / FPFVG: each current box’s range, direction and state. Dealing range: position around the midpoint. Macro: current or next enabled window. Base rates: counts from loaded history.

Brief Co-Pilot shows NOW. Standard adds WHY and WATCH. Detailed adds BASE RATES and GLOSSARY. These describe the map; they do not choose a trade.

Gold fifteen-minute TradingView chart with full ICT Levels Desk labels, four session boxes, previous-day range, opening gap, macro shading and Standard Co-Pilot.
TradingView snapshot · gold · full labels and Standard Co-PilotOpen full-size chart ↗
What does each setting change?

All fifty-six inputs, in the order shown in settings. Defaults are on the right.

Clock

Every configured clock time uses this timezone. The chart’s displayed timezone does not replace this setting.

Timezone used by the tool
America/New_York

Opening gaps

Compare the last tracked close inside the configured clock window with the next day-open price. NDOG is a daily gap; NWOG is a weekend gap. Filled gaps fade unless removal is enabled.

Track new-day opening gaps
On
Track new-week opening gaps
On
End of the tracked trading-day window
16:00
Start of the next trading-day window
18:00
Smallest opening gap to keep
Daily ATR × 0.02
Daily gap history to keep
3 days
Number of weekend gaps to keep
5
Remove opening gaps after they fill
Off

Opening prices

These are price references. The first available bar may stand in for midnight; missing session bars can change which opening lines appear.

Track the midnight opening price
On
Track the 08:30 opening price
On
Track the 09:30 opening price
On
Opening-price history to keep
1 day

Previous highs and lows

Use completed-period extremes. Daily pairs have a history limit; week, month and four-hour pairs are replaced. Swept references remain live until broken.

Track the previous day’s high and low
On
Track the previous week’s high and low
On
Track the previous month’s high and low
Off
Track the previous four-hour high and low
Off
Daily high/low history to keep
3 days
Remove high/low lines after a closing break
Off

First three-candle gap

Track the first qualifying FVG confirmed from each enabled anchor time. The main switch enables this module; its optional anchors depend on it. The size filter uses chart ATR.

Enable first-FVG tracking from 09:30
On
Also track the first FVG from 08:30
Off
Also track the first FVG from midnight
Off
Smallest three-candle gap to keep
Chart ATR × 0
First-FVG history to keep
1 day

Macro time windows

Eight fixed windows are grouped below. Their times are not editable. Hiding their drawings leaves the enabled windows’ countdown available; it uses chart-bar time.

Track the two London macro windows
On
Track the three New York morning windows
On
Track the lunch window
On
Track the two New York afternoon windows
On
Choose how macro windows are drawn
Shade + markers
History for macro boundary markers
1 day

Session ranges

Boxes develop while each session runs. After it ends, its high and low can become tracked references. Turning this module off stops their creation too.

Track and draw the four session ranges
On
Asia session window
20:00–00:00
London session window
02:00–05:00
New York morning window
08:30–11:00
New York afternoon window
13:30–16:00
Keep completed session highs and lows as lines
On
Session history to keep
2 days

Previous-day range

The midpoint divides the previous day’s range into upper and lower halves. Premium and discount describe location, not fair value or an entry.

Show the previous-day range and midpoint
On

Chart appearance

The proximity filter hides distant registered references while tracking continues. Session boxes, macro shading and range shading are separate. Choose Aurora Neon, Royal Gold or Colorblind-safe.

Choose the color theme
Aurora Neon
Hide distant reference drawings
On
Maximum drawing distance from price
Daily ATR × 2
Choose names, names with prices, or no labels
Short
Place labels this many candles to the right
0
Finished-line transparency; higher is fainter
80
Show sweep and FVG-inversion marks
On
Name boxes and the range halves
On

Dashboard

Lists nearby unfinished references and their states. The hidden count includes retained references hidden by distance. Base rates describe loaded chart history.

Show the dashboard
On
Choose dashboard position
Middle Left
Choose dashboard text size
Medium
Show the historical counts
On

Co-Pilot

Brief shows NOW. Standard adds WHY and WATCH. Detailed adds BASE RATES and GLOSSARY. Off hides the explanation.

Choose the amount of explanation
Brief
Choose explanation position
Bottom Left
Choose explanation text size
Auto
Wrap text after this line width
64 characters

Alerts

Near alerts have their own distance, separate from drawing visibility. They rearm after price leaves an unfinished reference. Zero disables this proximity alert.

Send dynamic messages as JSON
On
Distance that triggers a near-level alert
Daily ATR × 0.15
Which events can send an alert?

Fifteen named conditions: NDOG formed, NWOG formed, gap entered, gap midpoint touched; previous day/week high/low swept; first FVG formed, touched or inverted; macro opening; midnight crossed; near a level; session high/low swept.

Any alert() function call sends eight event types: NDOG, NWOG, GAP_ENTERED, SWEEP, FPFVG_FORMED, FPFVG_INVERTED, MACRO_OPEN and NEAR_LEVEL. Choose JSON or readable text. It omits midpoint, FVG touch, midnight crossing and session-sweep events. There is no separate gap-filled condition.

{"src":"ICTLevelsDesk","symbol":"ES1!","tf":"5","event":"SWEEP","what":"PDH","price":6410.5}

The example shows the core JSON fields; messages also carry nearestAbove, nearestBelow and macro context. Near alerts rearm after price leaves an unfinished level. Zero disables them.

Dynamic messages use once-per-bar-close frequency. Most named events follow confirmed state changes; macro opening is not explicitly close-gated. Select Once Per Bar Close when needed, and recreate alerts after changing inputs.

How do I use it without adding clutter?
  1. Match the clock to your chart’s session.
  2. Start with previous highs and lows. Follow one nearby reference through closed candles.
  3. Add opening gaps, then compare their different finishing rule.
  4. Turn the proximity filter off briefly to see what was hidden.
  5. Record your own trade rules and outcomes separately; this desk has no entry, stop or target engine.
SPY five-minute regular-hours TradingView chart with ICT Levels Desk opening gap, session ranges, first FVG, nearest references and Detailed Co-Pilot with chart-specific counts.
TradingView snapshot · SPY regular hours · Detailed Co-PilotOpen full-size chart ↗
What did we count?

We counted opening-gap fills, highs and lows taken, and first-FVG revisits on three charts: index futures, gold and a stock ETF. The samples covered weeks or months and differed by market.

Descriptive, not predictive. These are level counts, not trade returns. Some outcomes settle at a period rollover while totals start at birth; shared daily flags can group older levels or multiple anchors. The rows do not give an independent chance for your next trade.

The lab notes carry the measurements and caveats together.

The lab notes are in review and will be linked here.

What can change the reading?

A live candle can change before close. Committed reference states use closed candles, while session ranges develop across their session. Different feeds, history, timeframes or settings can recalculate the map.

Context, not signals. It does not predict price, verify resting orders or account for news, fills or trade size. Trading risk is entirely yours; this is research tooling, not financial advice.

  • Opening Range Desk

    Move from session references to the specific checks around an opening range.

  • Camarilla Desk

    Compare recorded highs, lows and opens with calculated Camarilla pivot levels and their session rules.

Built by ProEA Lab. Compare all free indicators.

05 · THE LOG

Every change, dated

What shipped and what changed in ICT Levels Desk, newest first. Ask for the next change below; what gets built lands here with a date, and on the lab queue on the front page.

  1. ICT Levels Desk v1.2.0 Shipped

    Tracks nearby highs, lows, opens and gaps as price moves.

  2. The ICT key-levels wishlist Request ICT Levels Desk

    A commenter listed the levels needed, with days back so the chart does not overclutter. Every item is in, each with a state.

    1. Received
    2. Building
    3. Done

    ndogs, nwogs, 1st presentation fvgs, midnight open, weekly/daily highs/lows … an option to change the number of days back such levels are drawn

    a commenter on the r/pinescript thread · Asked 3 Sep 2026 · Done 4 Sep 2026
  3. Counted on three charts before it shipped Lab note ICT Levels Desk

    The desk counts its own levels: gap fills, highs and lows taken, the first FVG revisited. Three markets, three answers. Numbers in the notes.

BUILT WITH TRADERS

Something to change in ICT Levels Desk?

A small friction or a big idea. Tell us what would make this tool work better for you.

What would help?

Describe your rules and what you want the tool to help you see.

Add chart details Optional

For a question about your idea or an update if it ships.

Guides & new tools Optional

Add an email above to choose this.

Show us on your chart Optional

Crop out any account details you don't want to share.

No account needed. Sent privately to the lab.
  1. We read it
  2. We choose what to build
  3. Shipped changes get a date

Requests for a new tool go on the same queue: see what is asked for and what is being built.

THE FRAMEWORK BEHIND THIS FREE TOOLSee how it is built.The six-kit framework is $46 once, with 5 finished indicators included, all 11 at one price.
© 2026 ProEA · All rights reservedBuilt for MT5 · Not affiliated with MetaQuotes