Indicator · Pine Script v6 · On the chart

HTF Statistical Zones

by samuelharting

HTF Statistical Zones plots historical percentile bands around the 18:00 New York day open, plus session boxes, opening-range lines, and projected targets on the price chart.

View on GitHub

Repaints

GitHub stars18on the repository
Pine versionv6current
Lines of code2,382
Settings40inputs
LicenceMPL-2.0
Last update2 weeks ago11 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

2,382 lines of Pine Script v6 from indicators/htf-zones/HTF_Statistical_Zones.pine, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

HTF_Statistical_Zones.pine
//@version=6
// HTF Statistical Zones (MNQ / MES / MYM / MGC)
// Day zones: Yahoo daily 2001-2026 (context-adjusted). Sessions, opening ranges, targets, gap fill, invalidation,
// setups and signals: Databento 1-minute 2010-2026. Every shown stat held in both 2010-18 and 2019-26.
// Fixed levels are set once (18:00 day open, 9:30 / 9:45 / 10:00 / session starts) and never move.
// Rebuild with pine/assemble.py after editing; constants are generated by research/*.py.
indicator("HTF Statistical Zones", "HTF Zones", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500)

// ─────────────────────────────────────────────────────────── inputs
bool biasMode = true   // the old "Full stats" live rows were removed 2026-10-05 (token limit)
minimal    = input.bool(false, "Minimal mode (AM zones, next-move odds, top 2 targets)", tooltip = "Hides the day zones, opening-range lines, midday/PM boxes, invalid / far-side / setup lines and all but the two highest-odds targets. Nothing is deleted - turn it off to see everything again.")
G1 = "Day zones (fixed at the 18:00 NY open)"
showDaily  = input.bool(true, "Show day zones", group = G1) and not minimal
innerPct   = input.string("70", "Zone from", options = ["50", "70", "80"], group = G1, inline = "pct")
outerPct   = input.string("90", "to percentile", options = ["80", "90", "95"], group = G1, inline = "pct", tooltip = "The 90% line: historically the day's high stayed below it (or the low above it) on 90% of days, after adjusting for today's context.")
showDLbl   = input.bool(true, "Price labels", group = G1)
showOpen   = input.bool(true, "18:00 open line", group = G1)
daysBack   = input.int(2, "Days to keep", minval = 1, maxval = 30, group = G1)
fitMode    = input.string("Inner edges", "Fit zones in view", options = ["Off", "Inner edges", "Full zones"], group = G1, tooltip = "TradingView auto-scale ignores boxes. Inner edges keeps the start of both zones on screen; Full zones keeps the outer lines too (squeezes candles).")
upBase     = input.color(#e05252, "Upper", group = G1, inline = "dc")
dnBase     = input.color(#2aa889, "Lower", group = G1, inline = "dc")
zoneW      = input.int(1, "Zone edge width", minval = 1, maxval = 4, group = G1)
openCol    = input.color(#8b949e73, "18:00 open line", group = G1, inline = "op")
openStyle  = input.string("Dotted", "", options = ["Solid", "Dashed", "Dotted"], group = G1, inline = "op")


G4 = "Session boxes (each fixed at its own start)"
showW1  = input.bool(true, "AM 9:30-11:30", group = G4)
showW2  = input.bool(true, "Midday 11:30-13:30", group = G4) and not minimal
showW3  = input.bool(true, "PM 13:30-16:00", group = G4) and not minimal
sessCol = input.color(#7a8ca8, "Box color", group = G4)
sessDays = input.int(1, "Days of session / OR boxes to keep", minval = 1, maxval = 5, group = G4)

G5 = "Opening range (9:30 NY)"
showOR15 = input.bool(true, "OR 9:30-9:45", group = G5) and not minimal
showOR30 = input.bool(true, "OR 9:30-10:00", group = G5) and not minimal
orCol    = input.color(#4fa3e0, "OR color", group = G5)
showTags = input.bool(true, "Likely targets by 11:30 (overnight / prior-day highs & lows)", group = G5)
tagCol   = input.color(#b07cd8, "Target color", group = G5)
tgtOffB  = input.int(55, "Target lines: start bars to the right", minval = 0, maxval = 200, group = G5, inline = "tof", tooltip = "Purple target lines start this many bars to the right of the bar that draws them and run for the length, so they stay out of the price action.")
tgtLenB  = input.int(8, "length (bars)", minval = 3, maxval = 200, group = G5, inline = "tof")
amzCol   = input.color(#7a8ca8, "Likely AM high / low zones", group = G5)
tagStyle = input.string("Dashed", "Target line", options = ["Solid", "Dashed", "Dotted"], group = G5, inline = "tgs")
tagW     = input.int(1, "width", minval = 1, maxval = 4, group = G5, inline = "tgs")
orW      = input.int(1, "Opening-range line width", minval = 1, maxval = 4, group = G5)
invCol   = input.color(#9aa0a6, "Stop / invalidation", group = G5, inline = "inv")
invStyle = input.string("Dotted", "", options = ["Solid", "Dashed", "Dotted"], group = G5, inline = "inv")
farCol   = input.color(#e8a33d, "Failed-break far side", group = G5)
clearHit = input.bool(true, "Remove a level once price trades through it", group = G5)
minOdds  = input.int(40, "Min target odds %", minval = 10, maxval = 90, step = 5, group = G5, tooltip = "Hide target labels below these odds (the tested odds never change, only what is drawn). Raise it for a cleaner chart.") / 100.0
clear1130 = input.bool(true, "Clear morning levels after 11:30", group = G5)

G2 = "Live"
showTable  = input.bool(true, "Table", group = G2)
tblFull    = input.string("Compact", "Table detail", options = ["Compact", "Full"], group = G2, tooltip = "Compact hides rows the chart already shows (opening-range boxes, target labels, zone highlight) and the minor week / volume rows.") == "Full"
lblCol     = input.color(#000000, "Chart label text", group = G2, inline = "lbl")
pushMin    = input.int(60, "Show push-target line when new high / low odds ≥ %", minval = 0, maxval = 100, step = 5, group = G2) / 100.0
lblBig     = input.bool(true, "bigger", group = G2, inline = "lbl")
tblPos     = input.string("Top right", "Table position", options = ["Top right", "Bottom right", "Top left", "Bottom left"], group = G2)

G3 = "Extra layers (off = cleaner chart)"
show4H     = input.bool(false, "4H block 80% lines (18/22/02/06/10/14 NY)", group = G3)
showSess   = input.bool(false, "Prior day / Asia / London / overnight levels", group = G3)
asiaStat   = input.string("18:00-0:00", "Asia stat boxes", options = ["18:00-0:00", "20:00-0:00", "Off"], group = G3, inline = "as")
asiaCol    = input.color(#7a8ca8, "", group = G3, inline = "as")
hCol       = input.color(#6c8fb8, "4H", group = G3, inline = "oc")
h4Style    = input.string("Solid", "4H line", options = ["Solid", "Dashed", "Dotted"], group = G3, inline = "h4s")
h4W        = input.int(2, "width", minval = 1, maxval = 4, group = G3, inline = "h4s")

G0 = "Advanced"
symOverride = input.string("Auto", "Statistics set", options = ["Auto", "NQ", "ES", "YM", "GC"], group = G0, tooltip = "Auto reads the chart root (MNQ/NQ, MES/ES, MYM/YM, MGC/GC).")
useIV       = input.bool(true, "Blend implied volatility into the equity scale (VXN/VIX/VXD)", group = G0)

f_ls(string st) => st == "Solid" ? line.style_solid : st == "Dashed" ? line.style_dashed : line.style_dotted

// ─────────────────────────────────────────────────────────── instrument
string root = syminfo.root
int autoIdx = root == "MNQ" or root == "NQ" ? 0 : root == "MES" or root == "ES" ? 1 : root == "MYM" or root == "YM" ? 2 : root == "MGC" or root == "GC" ? 3 : -1
int symIdx = symOverride == "NQ" ? 0 : symOverride == "ES" ? 1 : symOverride == "YM" ? 2 : symOverride == "GC" ? 3 : autoIdx
bool supported = symIdx >= 0
int S = supported ? symIdx : 0
string symName = S == 0 ? "NQ" : S == 1 ? "ES" : S == 2 ? "YM" : "GC"
string ivSym = S == 0 ? "CBOE:VXN" : S == 1 ? "CBOE:VIX" : S == 2 ? "CBOE:VXD" : "CBOE:GVZ"
bool blendScale = S != 3 and useIV          // gold uses the HAR scale (blend under-covered gold upside out of sample)

// ===== GENERATED by research/gen_constants.py on 2026-10-04 from Yahoo NQ/ES/YM/GC data. Do not edit by hand. =====

// context feature order: prev_exp, prev_con, clv_hi, clv_lo, pw_above, pw_below, pw_lower, r20_top, r20_bot, vol_hi, vol_lo, ev_FOMC, ev_NFP, ev_CPI, after_FOMC, after_NFP, m_first3, m_last2, mon, fri_opex
f_beta(int s) =>
    switch s
        0 => array.from(-0.0915, 0.0316, -0.0652, 0.0062, -0.0890, 0.0566, 0.0552, -0.0740, 0.0282, 0.0268, 0.0251, 0.1078, 0.1254, 0.0709, 0.1867, -0.0782, 0.0485, -0.0341, -0.0344, -0.0494)
        1 => array.from(-0.1182, 0.0280, -0.0528, -0.0065, -0.1022, 0.0583, 0.0610, -0.1153, 0.0702, 0.0229, 0.0144, 0.1680, 0.1562, 0.0846, 0.2105, -0.0728, 0.0525, -0.0182, -0.0547, -0.0243)
        2 => array.from(-0.0789, 0.0122, -0.0441, -0.0267, -0.0768, 0.0977, 0.0813, -0.0860, 0.0627, 0.0238, 0.0017, 0.1715, 0.1307, 0.0805, 0.1842, -0.0254, 0.0407, -0.0092, -0.0680, -0.0030)
        3 => array.from(-0.1409, 0.1021, -0.1158, -0.1088, 0.0044, 0.0305, 0.0278, 0.1357, -0.0079, -0.0635, 0.0584, 0.2332, 0.2702, 0.1203, 0.2637, -0.0422, 0.2560, 0.2015, -0.0448, -0.1016)

f_dq_up(int s) =>
    switch s
        0 => array.from(0.4877, 0.7187, 0.8816, 1.1401, 1.3658)
        1 => array.from(0.4694, 0.7077, 0.8671, 1.1005, 1.3220)
        2 => array.from(0.4562, 0.6941, 0.8600, 1.1244, 1.3354)
        3 => array.from(0.3361, 0.5846, 0.7857, 1.0925, 1.4449)

f_dq_dn(int s) =>
    switch s
        0 => array.from(0.4455, 0.7305, 0.9354, 1.2774, 1.5662)
        1 => array.from(0.4498, 0.7290, 0.9329, 1.2550, 1.5802)
        2 => array.from(0.4381, 0.6924, 0.9086, 1.2428, 1.5630)
        3 => array.from(0.3546, 0.6057, 0.8120, 1.1663, 1.4942)

// weekly quantiles p70/p80/p90 by open location: 0 below prev low, 1 inside, 2 above prev high








f_h4q_up(int s) =>
    switch s
        0 => array.from(0.6087, 0.7607, 1.0290)
        1 => array.from(0.6008, 0.7694, 1.0235)
        2 => array.from(0.6314, 0.7835, 1.0679)
        3 => array.from(0.6199, 0.7680, 1.0362)

f_h4q_dn(int s) =>
    switch s
        0 => array.from(0.6469, 0.8527, 1.1710)
        1 => array.from(0.6326, 0.8281, 1.1876)
        2 => array.from(0.6235, 0.8145, 1.1344)
        3 => array.from(0.6415, 0.8408, 1.1998)

// live tables: index = hourIndex*3 + usedBucket (hourIndex 0 = 18:00 bar; used <0.6 / 0.6-0.9 / >0.9)






// next move: index = timeBucket*3 + posBucket (time 18-00,00-03,03-07,07-09,09-11,11-14,14-17; pos lower/middle/upper third of day range)





// week-so-far: index = upperHalf*6 + usedBucket*2 + newsAhead (used <0.5 / 0.5-0.8 / >0.8)


f_cal_FOMC() => array.from(20240131, 20240320, 20240501, 20240612, 20240731, 20240918, 20241107, 20241218, 20250129, 20250319, 20250507, 20250618, 20250730, 20250917, 20251029, 20251210, 20260128, 20260318, 20260429, 20260617, 20260729, 20260916, 20261028, 20261209)

f_cal_CPI() => array.from(20240111, 20240213, 20240312, 20240410, 20240515, 20240612, 20240711, 20240814, 20240911, 20241010, 20241113, 20241211, 20250115, 20250212, 20250312, 20250410, 20250513, 20250611, 20250715, 20250812, 20250911, 20251024, 20251218, 20260113, 20260213, 20260311, 20260410, 20260512, 20260610, 20260714, 20260812, 20260911, 20261014, 20261110, 20261210)

f_cal_NFP() => array.from(20240105, 20240202, 20240308, 20240405, 20240503, 20240607, 20240705, 20240802, 20240906, 20241004, 20241206, 20250110, 20250207, 20250307, 20250404, 20250502, 20250606, 20250703, 20250905, 20251120, 20251216, 20260109, 20260211, 20260306, 20260403, 20260508, 20260605, 20260702, 20260807, 20260904, 20261002, 20261106, 20261204)


// ===== GENERATED tag odds by 11:30 (Databento 1m 2010-2026). order: ONH, ONL, PDH, PDL x 6 distance bins (x daily range: .1 .2 .35 .5 1 inf)
f_tag(int s) =>
    switch s
        0 => array.from(0.858, 0.623, 0.427, 0.278, 0.142, 0.013, 0.854, 0.662, 0.434, 0.290, 0.153, 0.000, 0.900, 0.551, 0.390, 0.207, 0.066, 0.005, 0.888, 0.689, 0.470, 0.220, 0.086, 0.012)
        1 => array.from(0.833, 0.623, 0.370, 0.197, 0.101, 0.012, 0.857, 0.619, 0.402, 0.224, 0.118, 0.000, 0.869, 0.551, 0.340, 0.156, 0.037, 0.007, 0.914, 0.670, 0.430, 0.175, 0.072, 0.005)
        2 => array.from(0.871, 0.633, 0.370, 0.239, 0.092, 0.038, 0.842, 0.623, 0.420, 0.235, 0.116, 0.000, 0.868, 0.618, 0.329, 0.201, 0.033, 0.005, 0.922, 0.645, 0.431, 0.205, 0.077, 0.010)
        3 => array.from(0.825, 0.543, 0.288, 0.139, 0.052, 0.009, 0.821, 0.556, 0.323, 0.143, 0.066, 0.005, 0.821, 0.502, 0.297, 0.116, 0.028, 0.006, 0.847, 0.514, 0.334, 0.129, 0.037, 0.008)


// gap-fill odds by 11:30 to the prior 15:59 close, 6 bins of |gap| x daily range: .05 .1 .2 .35 .6 inf
f_gapfill(int s) =>
    switch s
        0 => array.from(0.960, 0.838, 0.708, 0.481, 0.252, 0.088)
        1 => array.from(0.962, 0.843, 0.675, 0.435, 0.213, 0.065)
        2 => array.from(0.947, 0.865, 0.664, 0.418, 0.219, 0.067)
        3 => array.from(0.935, 0.786, 0.572, 0.294, 0.140, 0.039)


// ===== GENERATED invalidation stats for OR breaks (Databento 1m 2010-26). Do not edit.
f_inv15(int s) =>
    switch s
        0 => array.from(0.490, 0.383, 0.173, 0.056)
        1 => array.from(0.545, 0.384, 0.201, 0.065)
        2 => array.from(0.489, 0.371, 0.178, 0.060)
        3 => array.from(0.535, 0.433, 0.233, 0.085)

f_inv30(int s) =>
    switch s
        0 => array.from(0.400, 0.333, 0.123, 0.040)
        1 => array.from(0.437, 0.349, 0.154, 0.042)
        2 => array.from(0.426, 0.327, 0.129, 0.037)
        3 => array.from(0.457, 0.411, 0.188, 0.057)


// generated by research/gen_hold_constants.py: OR15 far-end hold %, 5 cells x [lo10, hi10, lo1130, hi1130]
f_hold(int s) =>
    switch s
        0 => array.from(74, 79, 41, 43, 82, 90, 54, 60, 95, 95, 68, 68, 62, 67, 30, 35, 22, 28, 12, 12)
        1 => array.from(74, 78, 37, 45, 83, 87, 47, 50, 92, 94, 60, 63, 61, 61, 28, 32, 26, 34, 15, 15)
        2 => array.from(80, 81, 40, 47, 89, 91, 57, 58, 95, 98, 69, 69, 65, 67, 32, 33, 26, 37, 16, 18)
        3 => array.from(70, 74, 33, 39, 81, 87, 43, 46, 91, 97, 58, 60, 62, 63, 28, 32, 32, 34, 15, 18)
// 9:45-10:00 trades beyond the OR15 close-side end: 5 cells x [lo%, hi%, median extension x100 in OR15 widths]
f_ext(int s) =>
    switch s
        0 => array.from(76, 77, 41, 72, 78, 29, 73, 79, 23, 39, 44, 29, 12, 20, 32)
        1 => array.from(76, 77, 39, 73, 76, 33, 74, 77, 24, 44, 45, 32, 18, 34, 40)
        2 => array.from(76, 76, 39, 74, 78, 30, 71, 75, 23, 34, 42, 29, 10, 23, 34)
        3 => array.from(77, 78, 50, 70, 72, 38, 64, 70, 27, 39, 44, 36, 25, 32, 42)


// generated by research/gen_hold30_constants.py: OR30 plan, 5 cells x [beyond lo, hi, median push x100, hold lo, hi]
f_hold30(int s) =>
    switch s
        0 => array.from(85, 87, 55, 60, 61, 83, 86, 50, 70, 74, 83, 86, 39, 83, 85, 56, 57, 54, 43, 44, 43, 47, 53, 21, 28)
        1 => array.from(84, 84, 66, 55, 59, 85, 86, 55, 66, 69, 84, 85, 42, 77, 80, 63, 64, 55, 42, 44, 42, 57, 44, 17, 26)
        2 => array.from(83, 85, 58, 57, 58, 83, 83, 49, 71, 71, 82, 83, 36, 81, 84, 55, 59, 48, 45, 46, 33, 40, 42, 15, 21)
        3 => array.from(86, 90, 85, 50, 57, 83, 85, 61, 65, 66, 74, 78, 43, 76, 80, 58, 60, 65, 43, 43, 34, 42, 59, 13, 15)


// generated by research/gen_tag10.py: tag odds by 10:00, order ONH, ONL, PDH, PDL x 6 distance bins; gap fill by 10:00 x 6 bins


// generated in cycle 34: tag odds 9:45-11:30 for levels not yet tagged, by distance from the 9:45 close (x daily range: .1 .2 .35 .5 inf)


// generated by research/gen_tag945_size.py: 9:45 target odds by OR15 size (small/normal/large) x distance bin
f_tag945sz(int s) =>
    switch s
        0 => array.from(0.791, 0.491, 0.265, 0.160, 0.031, 0.834, 0.597, 0.348, 0.196, 0.041, 0.812, 0.672, 0.417, 0.259, 0.058)
        1 => array.from(0.754, 0.460, 0.238, 0.111, 0.023, 0.779, 0.595, 0.315, 0.156, 0.033, 0.863, 0.653, 0.378, 0.249, 0.055)
        2 => array.from(0.746, 0.471, 0.231, 0.122, 0.018, 0.855, 0.531, 0.353, 0.171, 0.043, 0.792, 0.652, 0.377, 0.203, 0.039)
        3 => array.from(0.756, 0.410, 0.213, 0.082, 0.012, 0.810, 0.555, 0.270, 0.112, 0.016, 0.768, 0.595, 0.333, 0.151, 0.036)


// generated: 9:30 target-odds multipliers by overnight range tercile (quiet/normal/big, the Expect-row cuts) x distance bin
f_tagOnMul(int s) =>
    switch s
        0 => array.from(0.97, 0.97, 0.90, 0.81, 0.53, 1.00, 1.01, 1.03, 1.03, 1.00, 0.89, 1.00, 1.04, 1.04, 1.10, 1.14, 1.29, 1.00)
        1 => array.from(0.97, 0.91, 0.86, 0.64, 0.54, 1.00, 1.01, 1.06, 1.05, 1.00, 1.02, 1.00, 1.05, 1.11, 1.22, 1.32, 1.24, 1.00)
        2 => array.from(0.96, 0.89, 0.89, 0.81, 0.82, 1.00, 1.02, 1.08, 0.98, 0.94, 0.96, 1.00, 1.03, 1.14, 1.31, 1.28, 1.16, 1.00)
        3 => array.from(0.93, 0.91, 0.81, 0.76, 0.56, 1.00, 1.04, 1.05, 1.14, 0.99, 0.85, 1.00, 1.05, 1.13, 1.29, 1.57, 1.48, 1.00)


// generated: Asia session reach from its open (x daily HAR), q25/q50/q75/q90 pooled high+low; order: 18-0 other, 18-0 Monday, 20-0 other, 20-0 Monday
f_asiaZ(int s) =>
    switch s
        0 => array.from(0.050, 0.109, 0.196, 0.313, 0.063, 0.142, 0.249, 0.382, 0.038, 0.088, 0.160, 0.257, 0.038, 0.086, 0.163, 0.273)
        1 => array.from(0.053, 0.110, 0.200, 0.311, 0.063, 0.138, 0.260, 0.387, 0.040, 0.089, 0.162, 0.260, 0.038, 0.088, 0.169, 0.274)
        2 => array.from(0.052, 0.109, 0.199, 0.312, 0.059, 0.146, 0.266, 0.409, 0.038, 0.085, 0.159, 0.257, 0.037, 0.088, 0.169, 0.273)
        3 => array.from(0.070, 0.140, 0.239, 0.366, 0.083, 0.174, 0.305, 0.478, 0.057, 0.120, 0.205, 0.328, 0.056, 0.132, 0.240, 0.369)


// generated by research/cycle20_live_recheck.py: live tables from Databento hourly 2010-26 (replaces the Yahoo 2024-26 tables)


// ===== GENERATED by research/gen_session_constants.py (Databento 1m 2010-2026). Do not edit. =====
f_ss_am(int s) =>
    switch s
        0 => array.from(0.3577, 0.5806, 0.3729, 0.6848)
        1 => array.from(0.3217, 0.5170, 0.3424, 0.6168)
        2 => array.from(0.3408, 0.5614, 0.3533, 0.6327)
        3 => array.from(0.2800, 0.4960, 0.2945, 0.5426)

f_ss_mid(int s) =>
    switch s
        0 => array.from(0.2195, 0.3639, 0.2297, 0.4362)
        1 => array.from(0.2213, 0.3686, 0.2267, 0.4286)
        2 => array.from(0.2186, 0.3759, 0.2231, 0.4200)
        3 => array.from(0.1749, 0.3062, 0.1822, 0.3335)

f_ss_pm(int s) =>
    switch s
        0 => array.from(0.2520, 0.4192, 0.2607, 0.4837)
        1 => array.from(0.2711, 0.4618, 0.2725, 0.5280)
        2 => array.from(0.2593, 0.4411, 0.2591, 0.5046)
        3 => array.from(0.1541, 0.2850, 0.1558, 0.3015)

f_or15_cut(int s) =>
    switch s
        0 => array.from(0.2196, 0.3065)
        1 => array.from(0.1862, 0.2587)
        2 => array.from(0.2092, 0.2960)
        3 => array.from(0.1393, 0.2048)

f_or15_u50(int s) =>
    switch s
        0 => array.from(0.5380, 0.3080, 0.1940)
        1 => array.from(0.6060, 0.4210, 0.2310)
        2 => array.from(0.5000, 0.3160, 0.1500)
        3 => array.from(0.7590, 0.5170, 0.2120)

f_or15_u80(int s) =>
    switch s
        0 => array.from(1.3450, 0.9700, 0.7900)
        1 => array.from(1.5960, 1.1400, 0.8090)
        2 => array.from(1.3700, 1.0000, 0.7150)
        3 => array.from(2.1500, 1.4760, 0.9180)

f_or15_d50(int s) =>
    switch s
        0 => array.from(0.4920, 0.3280, 0.1590)
        1 => array.from(0.5000, 0.3860, 0.2750)
        2 => array.from(0.5000, 0.3170, 0.1310)
        3 => array.from(0.8420, 0.4250, 0.2000)

f_or15_d80(int s) =>
    switch s
        0 => array.from(1.5340, 1.1630, 0.8820)
        1 => array.from(1.5490, 1.3150, 1.0260)
        2 => array.from(1.4750, 1.1990, 0.7740)
        3 => array.from(2.3330, 1.4520, 1.0000)

f_or15_ft(int s) =>
    switch s
        0 => array.from(0.4910, 0.5200, 0.5490)
        1 => array.from(0.4980, 0.5050, 0.5170)
        2 => array.from(0.5020, 0.5400, 0.5340)
        3 => array.from(0.5720, 0.5430, 0.5550)

f_or30_cut(int s) =>
    switch s
        0 => array.from(0.2817, 0.3937)
        1 => array.from(0.2405, 0.3385)
        2 => array.from(0.2658, 0.3786)
        3 => array.from(0.1917, 0.2778)

f_or30_u50(int s) =>
    switch s
        0 => array.from(0.2410, 0.1250, 0.0150)
        1 => array.from(0.3070, 0.1670, 0.0570)
        2 => array.from(0.2520, 0.1070, 0.0000)
        3 => array.from(0.3570, 0.1760, 0.0000)

f_or30_u80(int s) =>
    switch s
        0 => array.from(0.8010, 0.6230, 0.4330)
        1 => array.from(0.9300, 0.6740, 0.4800)
        2 => array.from(0.8200, 0.6120, 0.4180)
        3 => array.from(1.2740, 0.8270, 0.4850)

f_or30_d50(int s) =>
    switch s
        0 => array.from(0.1700, 0.0760, 0.0000)
        1 => array.from(0.1780, 0.1420, 0.0490)
        2 => array.from(0.1740, 0.0830, 0.0000)
        3 => array.from(0.3460, 0.1460, 0.0000)

f_or30_d80(int s) =>
    switch s
        0 => array.from(0.8970, 0.6810, 0.5150)
        1 => array.from(0.9440, 0.8520, 0.5730)
        2 => array.from(0.8910, 0.7250, 0.4370)
        3 => array.from(1.2980, 0.9180, 0.5200)

f_or30_ft(int s) =>
    switch s
        0 => array.from(0.5120, 0.5250, 0.5770)
        1 => array.from(0.5140, 0.5300, 0.5090)
        2 => array.from(0.5050, 0.5470, 0.5170)
        3 => array.from(0.5720, 0.5770, 0.6500)


var array<float> BETA  = f_beta(S)
var array<float> DQU   = f_dq_up(S)
var array<float> DQD   = f_dq_dn(S)
var array<float> HQU   = f_h4q_up(S)
var array<float> HQD   = f_h4q_dn(S)
var array<int>   CAL_FOMC = f_cal_FOMC()
var array<int>   CAL_CPI  = f_cal_CPI()
var array<int>   CAL_NFP  = f_cal_NFP()

pctIndex(string p) => p == "50" ? 0 : p == "70" ? 1 : p == "80" ? 2 : p == "90" ? 3 : 4
int iIn  = pctIndex(innerPct)
int iOut = pctIndex(outerPct)
bool twoTier = iIn < 2 and iOut > 2                // shade inner..80 lighter and 80..outer stronger

// colors derived from the two base colors
color upFillIn  = color.new(upBase, 90)
color upFillOut = color.new(upBase, 80)
color dnFillIn  = color.new(dnBase, 90)
color dnFillOut = color.new(dnBase, 80)
color upHot     = color.new(upBase, 68)
color dnHot     = color.new(dnBase, 68)
color upOld     = color.new(upBase, 94)
color dnOld     = color.new(dnBase, 94)
color upEdge    = color.new(upBase, 25)
color dnEdge    = color.new(dnBase, 25)
color txtDim    = color.new(#8b949e, 0)
color txtMain   = color.new(#d0d7de, 0)

// ─────────────────────────────────────────────────────────── helpers
dateInt(int t) => year(t, "UTC") * 10000 + month(t, "UTC") * 100 + dayofmonth(t, "UTC")

evType(int d) =>
    array.includes(CAL_FOMC, d) ? "FOMC" : array.includes(CAL_CPI, d) ? "CPI" : array.includes(CAL_NFP, d) ? "NFP" : ""

evTime(string e) => e == "FOMC" ? "2:00" : "8:30"

dayName(int t) =>
    int dw = dayofweek(t, "UTC")
    dw == dayofweek.monday ? "Mon" : dw == dayofweek.tuesday ? "Tue" : dw == dayofweek.wednesday ? "Wed" : dw == dayofweek.thursday ? "Thu" : "Fri"

// next weekday after the trading date t (UTC midnight)
nextWeekday(int t) =>
    int dw = dayofweek(t, "UTC")
    t + (dw == dayofweek.friday ? 3 : dw == dayofweek.saturday ? 2 : 1) * 86400000

fmtP(float p) => na(p) ? "–" : str.tostring(math.round(p * 100)) + "%"
fmtPx(float x) => na(x) ? "–" : str.tostring(x, format.mintick)
prng(int a, int b) => a == b ? str.tostring(a) + "%" : str.tostring(a) + "-" + str.tostring(b) + "%"
fmtR(float x) => na(x) ? "–" : x >= 50 ? str.tostring(math.round(x)) : str.tostring(math.round(x / syminfo.mintick) * syminfo.mintick, "#,##0.##")
fmtPts(float x) => na(x) ? "–" : str.tostring(math.round(x / syminfo.mintick) * syminfo.mintick, "#,##0.##")

// ─────────────────────────────────────────────────────────── higher-timeframe data (non-repainting)
// Back-adjusted extended-session series so prior-week / 20-day levels are comparable to today's contract.
string tid = ticker.new(syminfo.prefix, syminfo.ticker, session.extended, backadjustment = backadjustment.on, settlement_as_close = settlement_as_close.on)

// Every value below uses only completed daily bars ([1] or older) plus state from earlier days,
// so lookahead_on cannot leak future data.
f_daily() =>
    float r = high - low
    float har = (r[1] + ta.sma(r, 5)[1] + ta.sma(r, 22)[1]) / 3.0
    float prevRatio = r[1] / har[1]
    float clv = (close[1] - low[1]) / math.max(high[1] - low[1], syminfo.mintick)
    float hi20 = ta.highest(high, 20)[1]
    float lo20 = ta.lowest(low, 20)[1]
    float vp = ta.percentrank(har / close[1], 250) / 100.0
    float s20 = ta.sma(close, 20)[1]
    float s50 = ta.sma(close, 50)[1]
    float s200 = ta.sma(close, 200)[1]
    float pdDir = math.sign(close[1] - open[1])
    float vote = math.sign(math.sign(close[1] - close[43]) + math.sign(close[1] - close[16]) + math.sign(close[1] - close[4]))
    var float wh = na
    var float wl = na
    bool nw = timeframe.change("W")
    float sfH = nw ? na : wh
    float sfL = nw ? na : wl
    wh := nw ? high : math.max(wh, high)
    wl := nw ? low : math.min(wl, low)
    [har, prevRatio, clv, hi20, lo20, vp, sfH, sfL, s20, s50, s200, vote, pdDir, high[1], low[1]]

f_week() =>
    float r = high - low
    [(r[1] + ta.sma(r, 4)[1] + ta.sma(r, 13)[1]) / 3.0, high[1], low[1], open]

f_month() =>
    float r = high - low
    [(r[1] + ta.sma(r, 3)[1] + ta.sma(r, 6)[1]) / 3.0, open]

// 4H blocks by NY hour: 0=18-22 1=22-02 2=02-06 3=06-10 4=10-14 5=14-17. Scale per block =
// (last range + mean of last 5 + mean of last 20 ranges of the same block), from completed blocks only.
f_blocks() =>
    int blk = int(math.floor(((hour(time, "America/New_York") - 18 + 24) % 24) / 4))
    var float bh = na
    var float bl = na
    var int cb = -1
    var array<float> hist = array.new<float>(120, na)
    if blk != cb
        if cb >= 0 and not na(bh)
            array.insert(hist, cb * 20, bh - bl)
            array.remove(hist, cb * 20 + 20)
        cb := blk
        bh := high
        bl := low
    else
        bh := math.max(bh, high)
        bl := math.min(bl, low)
    float s5 = 0.0
    float s20 = 0.0
    int n5 = 0
    int n20 = 0
    for i = 0 to 19
        float v = array.get(hist, blk * 20 + i)
        if not na(v)
            if i < 5
                s5 += v
                n5 += 1
            s20 += v
            n20 += 1
    float r0 = array.get(hist, blk * 20)
    float sc = n20 >= 5 and not na(r0) ? (r0 + s5 / n5 + s20 / n20) / 3.0 : na
    [sc, blk]

// 9:30-9:45 volume relative to its own 20-day average, in the 15m context (value available when the bar closes)

[dHar, dPrevRatio, dClv, dHi20, dLo20, dVolP, dSfH, dSfL, dS20, dS50, dS200, dVote, dPdDir, dPdH, dPdL] = request.security(tid, "D", f_daily(), lookahead = barmerge.lookahead_on)
float dOpenHTF = request.security(tid, "D", open, lookahead = barmerge.lookahead_on)
[dH2, dL2] = request.security(tid, "D", [high[2], low[2]], lookahead = barmerge.lookahead_on)
[wSig, pwh, pwl, wOpenHTF] = request.security(tid, "W", f_week(), lookahead = barmerge.lookahead_on)
[mSig, mOpenHTF] = request.security(tid, "M", f_month(), lookahead = barmerge.lookahead_on)
// Implied vol: read ONLY on the first bar of the futures day (18:00 NY), when the latest index bar is the
// previous session's completed bar. Later in the day this value is never used.
float ivClose = request.security(ivSym, "D", close, lookahead = barmerge.lookahead_on, ignore_invalid_symbol = true)
[hScale, hBlk] = request.security(tid, "60", f_blocks(), lookahead = barmerge.lookahead_on)

// ─────────────────────────────────────────────────────────── futures-day state
bool intraday = timeframe.isintraday
int  tday = time_tradingday
int  tdChg = ta.change(tday)
bool newDay = intraday and tdChg != 0
int  nyHour = hour(time, "America/New_York")
int  nyMin  = minute(time, "America/New_York")
int  barMs  = timeframe.in_seconds() * 1000
int  tOff   = tgtOffB * barMs
int  tLen   = tgtLenB * barMs

var int    prevDate = na
var int    monthCount = 0
var int    lastMonth = na
var float  dOpen = na
var float  sigma = na
var float  ctxMult = na
var string ctxText = ""
var string newsTxt = ""
var bool   newsIsToday = false
var bool   newsAhead = false
var bool   ivUsed = false
var float  dayHi = na
var float  dayLo = na
var int    dayStart = na
var int    dayEnd = na
var float  uIn = na
var float  uOut = na
var float  lIn = na
var float  lOut = na
var int    bias = 0
var int    monDir = 0
var float  monH = na
var float  monL = na
var string biasWhy = "trend"
var string biasPct = "53-55%"
var bool   upTouched = false
var bool   dnTouched = false
var array<float> feat = array.new<float>(20, 0.0)
// drawings: today's handles + history queues
var box upBoxIn = na
var box upBoxOut = na
var box dnBoxIn = na
var box dnBoxOut = na
var array<box>   hBoxQ = array.new<box>()
var array<line>  hLineQ = array.new<line>()
var array<label> hLblQ = array.new<label>()


if newDay
    int dd = dateInt(tday)
    int y = year(tday, "UTC")
    int mo = month(tday, "UTC")
    int dom = dayofmonth(tday, "UTC")
    int dw = dayofweek(tday, "UTC")
    dayStart := time
    dayEnd := timestamp("America/New_York", y, mo, dom, 17, 0)
    dOpen := na(dOpenHTF) ? open : dOpenHTF
    // trading-day count within the month and weekdays left after today
    monthCount := na(lastMonth) or mo != lastMonth ? 1 : monthCount + 1
    lastMonth := mo
    int dim = mo == 2 ? (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) ? 29 : 28) : (mo == 4 or mo == 6 or mo == 9 or mo == 11) ? 30 : 31
    int left = 0
    if dom < dim                                  // Pine for-loops count down when start > end
        for k = dom + 1 to dim
            int wd = dayofweek(timestamp("UTC", y, mo, k, 0, 0), "UTC")
            if wd != dayofweek.saturday and wd != dayofweek.sunday
                left += 1
    // news with the research priority FOMC > CPI > NFP; today > tomorrow > yesterday
    string evT = evType(dd)
    int tmr = nextWeekday(tday)
    string evN = evType(dateInt(tmr))
    string evY = na(prevDate) ? "" : evType(prevDate)
    string later = ""
    int scan = nextWeekday(tmr)
    for k = 0 to 3
        if dayofweek(scan, "UTC") == dayofweek.monday
            break
        string e = evType(dateInt(scan))
        if e != ""
            later := later + (later == "" ? "" : ", ") + e + " " + dayName(scan)
        scan := nextWeekday(scan)
    newsIsToday := evT != ""
    newsAhead := evT != "" or evN != "" or later != ""
    newsTxt := evT != "" ? evT + " today " + evTime(evT) : ""
    if evN != ""
        newsTxt := newsTxt + (newsTxt == "" ? "" : " · ") + evN + " " + (dw == dayofweek.friday ? "Mon" : "tmrw") + " " + evTime(evN)
    if later != ""
        newsTxt := newsTxt + (newsTxt == "" ? "" : " · ") + later
    bool afterF = evT == "" and evN == "" and evY == "FOMC"
    bool afterN = evT == "" and evN == "" and evY == "NFP"
    prevDate := dd
    // context features (order fixed by gen_constants.py)
    float p20 = (dOpen - dLo20) / math.max(dHi20 - dLo20, syminfo.mintick)
    float mid = (pwh + pwl) / 2.0
    array.set(feat, 0, dPrevRatio > 1.3 ? 1.0 : 0.0)
    array.set(feat, 1, dPrevRatio < 0.7 ? 1.0 : 0.0)
    array.set(feat, 2, dClv > 0.8 ? 1.0 : 0.0)
    array.set(feat, 3, dClv < 0.2 ? 1.0 : 0.0)
    array.set(feat, 4, dOpen > pwh ? 1.0 : 0.0)
    array.set(feat, 5, dOpen < pwl ? 1.0 : 0.0)
    array.set(feat, 6, dOpen >= pwl and dOpen < mid ? 1.0 : 0.0)
    array.set(feat, 7, p20 >= 0.8 ? 1.0 : 0.0)
    array.set(feat, 8, p20 < 0.2 ? 1.0 : 0.0)
    array.set(feat, 9, dVolP > 0.67 ? 1.0 : 0.0)
    array.set(feat, 10, dVolP < 0.33 ? 1.0 : 0.0)
    array.set(feat, 11, evT == "FOMC" ? 1.0 : 0.0)
    array.set(feat, 12, evT == "NFP" ? 1.0 : 0.0)
    array.set(feat, 13, evT == "CPI" ? 1.0 : 0.0)
    array.set(feat, 14, afterF ? 1.0 : 0.0)
    array.set(feat, 15, afterN ? 1.0 : 0.0)
    array.set(feat, 16, monthCount <= 3 ? 1.0 : 0.0)
    array.set(feat, 17, left <= 1 ? 1.0 : 0.0)
    array.set(feat, 18, dw == dayofweek.monday ? 1.0 : 0.0)
    array.set(feat, 19, dw == dayofweek.friday and dom >= 15 and dom <= 21 ? 1.0 : 0.0)
    float xb = 0.0
    for k = 0 to 19
        xb += array.get(feat, k) * array.get(BETA, k)
    ctxMult := math.exp(xb)
    float ivMove = na(ivClose) ? na : ivClose / 100.0 / math.sqrt(252.0) * dOpen
    ivUsed := blendScale and not na(ivMove)
    float baseSc = ivUsed ? math.sqrt(dHar * ivMove) : dHar
    sigma := baseSc * ctxMult
    // readable context summary (table tooltip)
    string c = ""
    c += dPrevRatio > 1.3 ? "• after an expansion day\n" : dPrevRatio < 0.7 ? "• after a quiet day\n" : ""
    c += dClv > 0.8 ? "• prior close near its high\n" : dClv < 0.2 ? "• prior close near its low\n" : ""
    c += dOpen > pwh ? "• open above last week's high\n" : dOpen < pwl ? "• open below last week's low\n" : dOpen < mid ? "• open in lower half of last week\n" : ""
    c += p20 >= 0.8 ? "• top of the 20-day range\n" : p20 < 0.2 ? "• bottom of the 20-day range\n" : ""
    c += dVolP > 0.67 ? "• high-volatility regime\n" : dVolP < 0.33 ? "• low-volatility regime\n" : ""
    c += evT != "" ? "• " + evT + " today\n" : afterF ? "• day after FOMC\n" : afterN ? "• day after NFP\n" : ""
    c += monthCount <= 3 ? "• first days of the month\n" : left <= 1 ? "• last days of the month\n" : ""
    c += dw == dayofweek.monday ? "• Monday\n" : ""
    ctxText := (c == "" ? "• nothing unusual\n" : c) + "Context multiplier x" + str.tostring(ctxMult, "#.##") + (ivUsed ? " · scale = HAR x implied vol" : " · scale = HAR range")
    // daily bias: trend filter, the only rule that held up 2001-2026 (direction hit ~53-55% equities, ~52-53% gold)
    // trend rule, full futures day (18:00 -> close), Databento 2010-26: NQ 55.7/54.7%, ES 54.3/54.9%, YM 55.3/52.5%;
    // gold 49-50% (no edge -> neutral). For 9:30-11:30 only it is weaker (51-55%).
    // audit 2026-10-05: the trend rule's LONG calls (53-59%) only matched the index drift (all days up 53-56%) and its
    // SHORT calls were right 12-52% -> no day call by default
    bias := 0
    biasWhy := ""
    biasPct := ""
    // Monday's direction for the weekly hint (Yahoo daily 2001-26, both eras): Monday up -> its low is the week
    // low 41-46% (base ~20%); Monday down -> its high is the week high 29-38%.
    if dw == dayofweek.monday
        monDir := 0
    else if dw == dayofweek.tuesday
        monDir := int(dPdDir)
        monH := dPdH
        monL := dPdL
    // tested overrides (Databento 2010-26, >=56% in both eras, n>=150 each)
    if S == 0 and dw == dayofweek.monday
        bias := 1
        biasWhy := "NQ Monday"
        biasPct := "59-63%"
    // "NQ fade a big day" removed 2026-10-05 audit: 54.9/54.5% (below the bar)
    // YM quiet day + above its 20-day average: up 62/57/57% (short side 44-67%, dropped)
    else if S == 2 and dPrevRatio < 0.7 and dOpen > dS20
        bias := 1
        biasWhy := "quiet day + above 20d avg"
        biasPct := "57-62%"
    dayHi := high
    dayLo := low
    upTouched := false
    dnTouched := false
    uIn := na
    uOut := na
    lIn := na
    lOut := na

    // ── fade yesterday, draw today's fixed zones
    if not na(upBoxIn)
        box.set_bgcolor(upBoxIn, upOld)
        box.set_bgcolor(dnBoxIn, dnOld)
    if not na(upBoxOut)
        box.set_bgcolor(upBoxOut, upOld)
        box.set_bgcolor(dnBoxOut, dnOld)
    if showDaily and not na(sigma)
        uIn := dOpen + array.get(DQU, iIn) * sigma
        uOut := dOpen + array.get(DQU, iOut) * sigma
        lIn := dOpen - array.get(DQD, iIn) * sigma
        lOut := dOpen - array.get(DQD, iOut) * sigma
        float uMid = dOpen + array.get(DQU, 2) * sigma
        float lMid = dOpen - array.get(DQD, 2) * sigma
        upBoxIn := twoTier ? box.new(dayStart, uMid, dayEnd, uIn, xloc = xloc.bar_time, border_width = 0, bgcolor = upFillIn) : na
        upBoxOut := box.new(dayStart, uOut, dayEnd, twoTier ? uMid : uIn, xloc = xloc.bar_time, border_width = 0, bgcolor = upFillOut)
        if biasMode and bias != 0
            array.push(hLblQ, label.new(dayStart, bias == 1 ? lIn : uIn, (bias == 1 ? "LONG " : "SHORT ") + biasPct, xloc = xloc.bar_time, style = bias == 1 ? label.style_label_up : label.style_label_down, color = color.new(bias == 1 ? dnBase : upBase, 15), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Trend bias locked at the 18:00 open: 50d avg vs 200d avg and open vs 20d avg. Right on the full-day direction (18:00 to close) " + biasPct + " since 2010 (both eras); weaker for 9:30-11:30 alone (51-55%). Gold: no edge, so no trend bias. A lean, not a signal."))
        dnBoxIn := twoTier ? box.new(dayStart, lIn, dayEnd, lMid, xloc = xloc.bar_time, border_width = 0, bgcolor = dnFillIn) : na
        dnBoxOut := box.new(dayStart, twoTier ? lMid : lIn, dayEnd, lOut, xloc = xloc.bar_time, border_width = 0, bgcolor = dnFillOut)
        array.push(hBoxQ, upBoxIn)
        array.push(hBoxQ, upBoxOut)
        array.push(hBoxQ, dnBoxIn)
        array.push(hBoxQ, dnBoxOut)
        array.push(hLineQ, line.new(dayStart, uOut, dayEnd, uOut, xloc = xloc.bar_time, color = upEdge, width = zoneW))
        array.push(hLineQ, line.new(dayStart, lOut, dayEnd, lOut, xloc = xloc.bar_time, color = dnEdge, width = zoneW))
        if showOpen
            array.push(hLineQ, line.new(dayStart, dOpen, dayEnd, dOpen, xloc = xloc.bar_time, color = openCol, style = f_ls(openStyle)))
        if showDLbl
            string tipU = "Day high stayed below this line on " + outerPct + "% of days (context-adjusted)."
            string tipD = "Day low stayed above this line on " + outerPct + "% of days (context-adjusted)."
            array.push(hLblQ, label.new(dayEnd, uOut, outerPct + "%  " + fmtPx(uOut), xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = tipU))
            array.push(hLblQ, label.new(dayEnd, uIn, innerPct + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny))
            array.push(hLblQ, label.new(dayEnd, lIn, innerPct + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny))
            array.push(hLblQ, label.new(dayEnd, lOut, outerPct + "%  " + fmtPx(lOut), xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = tipD))
        // keep only the last N days
        while array.size(hBoxQ) > daysBack * 4
            box.delete(array.shift(hBoxQ))
        while array.size(hLineQ) > daysBack * (showOpen ? 3 : 2)
            line.delete(array.shift(hLineQ))
        while array.size(hLblQ) > daysBack * 5
            label.delete(array.shift(hLblQ))
else if intraday
    dayHi := math.max(dayHi, high)
    dayLo := math.min(dayLo, low)

// zones brighten once price trades into them
if not na(uIn) and not upTouched and dayHi >= uIn
    upTouched := true
    box.set_bgcolor(upBoxOut, upHot)
    if not na(upBoxIn)
        box.set_bgcolor(upBoxIn, color.new(upBase, 82))
if not na(lIn) and not dnTouched and dayLo <= lIn
    dnTouched := true
    box.set_bgcolor(dnBoxOut, dnHot)
    if not na(dnBoxIn)
        box.set_bgcolor(dnBoxIn, color.new(dnBase, 82))
// invisible plots so auto-scale includes the zones (today only)
bool fitIn = fitMode != "Off" and intraday and time >= dayStart
bool fitOut = fitMode == "Full zones" and intraday and time >= dayStart
plot(fitIn ? uIn : na, "fit upper", color.new(color.black, 100), display = display.pane, editable = false)
plot(fitIn ? lIn : na, "fit lower", color.new(color.black, 100), display = display.pane, editable = false)
plot(fitOut ? uOut : na, "fit upper outer", color.new(color.black, 100), display = display.pane, editable = false)
plot(fitOut ? lOut : na, "fit lower outer", color.new(color.black, 100), display = display.pane, editable = false)
// tested: upper zone first hit between 9:00 and 11:59 NY -> NQ/ES kept going 56-65% in both eras (2010-26, 1m)
// audit 2026-10-05 (70% line, +/-0.2x race): NQ 59.0/53.0%, ES 57.3/55.0% -> fails 2019-26, claim removed (alerts kept)

var float pdH = na
var float pdL = na
var float tdH = na
var float tdL = na
var float onH = na
var float onL = na
var float c16 = na
var float c16prev = na

// ─────────────────────────────────────────────────────────── overnight 4H candles (18-22, 22-02, 02-06) for the AM bias
var float h4o = na
var int h4a = 0
var int h4b = 0
var int h4c = 0
var float o930 = na
if newDay
    h4o := open
    h4a := 0
    h4b := 0
    h4c := 0
if intraday and nyHour != nz(nyHour[1]) and (nyHour == 22 or nyHour == 2 or nyHour == 6)
    int dirC = close[1] > h4o ? 1 : close[1] < h4o ? -1 : 0
    if nyHour == 22
        h4a := dirC
    else if nyHour == 2
        h4b := dirC
    else
        h4c := dirC
    h4o := open
int h4agree = h4a != 0 and h4a == h4b and h4b == h4c ? h4a : 0

// ─────────────────────────────────────────────────────────── session boxes (16-year sizes, fixed at each start)
var array<box> sQ = array.new<box>()
var array<label> sL = array.new<label>()
var string sessTxt = ""
var color sessC = color.gray
int nyT = nyHour * 60 + nyMin
int nyTprev = nz(nyHour[1]) * 60 + nz(nyMin[1])
startOf(int sMin) => intraday and timeframe.in_seconds() <= 1800 and nyT >= sMin and (nyTprev < sMin or nyTprev > nyT)

// q = [u70, u90, d70, d90] in units of the daily HAR range (Databento 2010-2026)
drawSess(string nm, int eMin, array<float> q, int b, string why) =>
    float sc = dHar * (na(ctxMult) ? 1.0 : ctxMult)
    float o = open
    int tE = timestamp("America/New_York", year(time, "America/New_York"), month(time, "America/New_York"), dayofmonth(time, "America/New_York"), int(math.floor(eMin / 60)), eMin % 60)
    box bu = box.new(time, o + array.get(q, 1) * sc, tE, o + array.get(q, 0) * sc, xloc = xloc.bar_time, border_color = color.new(sessCol, 35), border_width = 1, bgcolor = color.new(sessCol, b == 1 ? 88 : 96))
    box bd = box.new(time, o - array.get(q, 2) * sc, tE, o - array.get(q, 3) * sc, xloc = xloc.bar_time, border_color = color.new(sessCol, 35), border_width = 1, bgcolor = color.new(sessCol, b == -1 ? 88 : 96))
    string arrow = b == 1 ? "▲" : b == -1 ? "▼" : ""
    string tip = nm + " box: on 70-90% of sessions since 2010 the " + nm + " high/low stayed inside these distances from the session open (Databento 1m, scaled to the daily range and today's context).\n" + why
    label lb = b == 0 ? na : label.new(time, b == -1 ? o - array.get(q, 3) * sc : o + array.get(q, 1) * sc, nm + " " + arrow, xloc = xloc.bar_time, style = b == -1 ? label.style_label_upper_left : label.style_label_lower_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = tip)
    [bu, bd, lb]

var array<line> amEdgeQ = array.new<line>()
var float onUsed930 = na
var int amBias = 0
if newDay
    amBias := 0
if intraday and startOf(570)
    o930 := open
    onUsed930 := na(onH) or na(dHar) ? na : (onH - onL) / dHar
if not na(dHar)
    if showW1 and startOf(570)
        // AM: all three overnight 4H candles agree -> lean that way (NQ/ES/YM held 53-56% after 9:45, 2010-26; not gold)
        // gold instead: a big move since 18:00 (>0.8x daily range) kept going 9:30-11:30 55% / 55.5% (n=225/218)
        float s18 = na(dOpen) ? 0.0 : (open - dOpen) / dHar
        // audit 2026-10-05: "all overnight 4H agree" as an AM (9:30-11:30) bias was 46-55% for the index futures -> removed
        int b = S != 3 ? 0 : math.abs(s18) > 0.8 ? int(math.sign(s18)) : 0
        // Monday after Friday closed in its upper half (rechecked 2026-10-05: 56-64%, n 88-111/era) and 9:30 above the 18:00 open -> AM up NQ 60/60%, ES 60/57%, YM 66/60% (n~120/era each)
        // removed 2026-10-05: 2023-26 only 41-48% (pooled ~45%, n~130) on NQ/ES/YM - the edge faded recently
        bool monUp = false
        if monUp
            b := 1
        // NQ on monthly OPEX Friday: 9:30-11:30 closed down 56% / 61% (n=100/90)
        bool opexDn = S == 0 and dayofweek(time, "America/New_York") == dayofweek.friday and dayofmonth(time, "America/New_York") >= 15 and dayofmonth(time, "America/New_York") <= 21
        if opexDn
            b := -1
        // Monday after monthly OPEX: 9:30-11:30 up NQ 60/57%, ES 56/59%, YM 58/57% (n~100/93 per era)
        bool opexMon = S != 3 and dayofweek(time, "America/New_York") == dayofweek.monday and dayofmonth(time, "America/New_York") >= 18 and dayofmonth(time, "America/New_York") <= 24
        if opexMon
            b := 1
        string why = opexMon ? "Bias: Monday after monthly options expiry; the 9:30-11:30 closed up 56-60% of such Mondays (NQ/ES/YM, both 2010-18 and 2019-26)." : opexDn ? "Bias: monthly options-expiry Friday; NQ's 9:30-11:30 closed down 56% (2010-18) / 61% (2019-26), and 9:30-10:00 down 59-62%." : monUp ? "Bias: Monday, Friday closed in its upper half and 9:30 is above the 18:00 open; the AM went up 56-63% of such Mondays (NQ/ES/YM, both 2010-18 and 2019-26)." : S == 3 ? (b != 0 ? "Bias: gold already moved " + str.tostring(math.abs(s18), "#.##") + "x its daily range since 18:00; big overnight moves kept going into 11:30 55% of the time (2010-26)." : "No tested AM edge today (gold needs a move >0.8x the daily range since 18:00).") : "No tested AM edge today (calendar rules only: OPEX Friday, Monday after OPEX, Monday after a strong Friday)."
        [bu, bd, lb] = drawSess("AM", 690, f_ss_am(S), b, why)
        // merged with the blue "likely AM high / low" zones: the yellow AM box is hidden and only its outer (90%) edge is
        // drawn as a line (a zero-height box does not render)
        for bx in array.from(bu, bd)
            box.set_bgcolor(bx, color.new(sessCol, 100))
            box.set_border_color(bx, color.new(sessCol, 100))
        array.push(sQ, bu)
        array.push(sQ, bd)
        array.push(sL, lb)
        sessTxt := b == 0 ? "AM · no edge" : (b == 1 ? "AM ▲ " : "AM ▼ ") + (opexMon ? "Mon after OPEX (56-60%)" : opexDn ? "OPEX Friday (56-61%)" : monUp ? "Mon after strong Fri (57-66%)" : S == 3 ? "big overnight move (55%)" : "4H aligned (53-56%)")
        sessC := b == 1 ? color.new(dnBase, 0) : b == -1 ? color.new(upBase, 0) : txtDim
        amBias := b
    // 10:00 check of the AM bias (NQ/ES): 9:30-10:00 went with it -> 10:00-11:30 kept going NQ 56/59%, ES 54/58%
    // (n 320-410/era); went against it -> coin flip after 10:00 (45-52%). YM: no difference, not shown.
    if (S == 0 or S == 1) and amBias != 0 and intraday and nyT >= 600 and nyTprev < 600 and not na(o930)
        int e10 = close[1] > o930 ? 1 : close[1] < o930 ? -1 : 0
        if e10 == amBias and S == 0
            sessTxt := sessTxt + " · on track (57-59% after 10)"
        else if e10 != 0
            sessTxt := (amBias == 1 ? "AM ▲" : "AM ▼") + " off track · coin flip after 10:00"
            sessC := txtDim
    if showW2 and startOf(690)
        // Midday: NQ only, 9:30 open above prior-day high -> midday up 56% in both eras
        // NQ: 9:30 open above PDH -> midday up 58/59% (n~400/era); plus green 9:30-9:45 candle -> 64/63% (n~190-200)
        bool aboveP = S == 0 and not na(o930) and not na(pdH) and o930 > pdH
        int b = aboveP ? 1 : 0
        string why = aboveP ? "Bias: 9:30 opened above yesterday's high; NQ midday went up 57.5% / 58.8% of such days (2010-18 / 2019-26)." : "No tested midday edge today."
        [bu, bd, lb] = drawSess("Mid", 810, f_ss_mid(S), b, why)
        array.push(sQ, bu)
        array.push(sQ, bd)
        array.push(sL, lb)
        sessTxt := b == 1 ? "Mid ▲ above PDH (57-59%)" : "Mid · no edge"
        sessC := b == 1 ? color.new(dnBase, 0) : txtDim
    if showW3 and startOf(810)
        // NQ/YM: big overnight range (top third) + 9:30 open above PDH -> PM up 58-61% (both eras, n~160/era)
        // ES: Friday, gap direction agrees with the 2m/3w/3d vote -> PM that way 62% / 59% (n~220/era)
        float cutOn = S == 0 ? 0.612 : S == 2 ? 0.646 : 9.0
        bool bigOnAbove = (S == 0 or S == 2) and not na(onUsed930) and onUsed930 > cutOn and not na(o930) and not na(pdH) and o930 > pdH
        int gapD = na(c16prev) or na(o930) ? 0 : o930 > c16prev ? 1 : -1
        bool esFri = S == 1 and dayofweek(time_tradingday, "UTC") == dayofweek.friday and gapD != 0 and gapD == dVote
        int b = bigOnAbove ? 1 : esFri ? gapD : 0
        string why = bigOnAbove ? "Bias: big overnight range and 9:30 opened above yesterday's high; PM went up 57-61% of such days (NQ 58/61%, YM 57/58%, both eras)." : esFri ? "Bias: Friday, gap direction matches the 2-month/3-week/3-day trend vote; ES PM went that way 61% / 58% (2010-18 / 2019-26)." : "No tested PM edge today."
        [bu, bd, lb] = drawSess("PM", 960, f_ss_pm(S), b, why)
        array.push(sQ, bu)
        array.push(sQ, bd)
        array.push(sL, lb)
        sessTxt := b == 0 ? "PM · no edge" : (b == 1 ? "PM ▲ " : "PM ▼ ") + (esFri ? "Fri gap + trend (58-61%)" : "big ON + above PDH (57-61%)")
        sessC := b == 1 ? color.new(dnBase, 0) : b == -1 ? color.new(upBase, 0) : txtDim
    while array.size(sQ) > sessDays * 6
        box.delete(array.shift(sQ))
    while array.size(sL) > sessDays * 3
        label.delete(array.shift(sL))
if newDay
    sessTxt := ""
    o930 := na

// 9:30 opened beyond the 70% / 80% day-zone line (Databento 2010-26, all 4 markets, both eras): next zone line out
// reached by 11:30 73-86% (from 70-80) / 59-78% (from 80-90); back to the 18:00 open only 2-10%. 90%+: too few days.
var string zoneTxt = ""
if newDay
    zoneTxt := ""
if intraday and startOf(570) and not na(sigma) and not na(dOpen)
    float zu = (open - dOpen) / sigma
    float zd = (dOpen - open) / sigma
    if zu >= array.get(DQU, 1) and zu < array.get(DQU, 2)
        zoneTxt := "▲ opened past 70%: 80% line by 11:30 73-86% · back to 18:00 open <10%"
    else if zu >= array.get(DQU, 2) and zu < array.get(DQU, 3)
        zoneTxt := "▲ opened past 80%: 90% line by 11:30 59-78% · back to 18:00 open <10%"
    else if zd >= array.get(DQD, 1) and zd < array.get(DQD, 2)
        zoneTxt := "▼ opened past 70%: 80% line by 11:30 73-86% · back to 18:00 open <10%"
    else if zd >= array.get(DQD, 2) and zd < array.get(DQD, 3)
        zoneTxt := "▼ opened past 80%: 90% line by 11:30 59-78% · back to 18:00 open <10%"
// zone time stop: opened past 70% but the 80% line not reached by 10:30 -> by 11:30 only ~20% (pooled 4 markets, 22% / 20%, n ~130/era)
var float zoneLine = na
var int zoneSide = 0
var bool zoneHit = false
if intraday and startOf(570)
    zoneLine := na
    zoneSide := 0
    zoneHit := false
    if not na(sigma) and not na(dOpen)
        float zu2 = (open - dOpen) / sigma
        float zd2 = (dOpen - open) / sigma
        if zu2 >= array.get(DQU, 1) and zu2 < array.get(DQU, 2)
            zoneSide := 1
            zoneLine := dOpen + array.get(DQU, 2) * sigma
        else if zd2 >= array.get(DQD, 1) and zd2 < array.get(DQD, 2)
            zoneSide := -1
            zoneLine := dOpen - array.get(DQD, 2) * sigma
if zoneSide != 0 and nyT >= 570 and nyT < 630 and not na(zoneLine) and (zoneSide == 1 ? high >= zoneLine : low <= zoneLine)
    zoneHit := true
bool zoneStall = intraday and zoneSide != 0 and nyT >= 630 and nyTprev < 630 and not zoneHit and zoneTxt != ""
if zoneStall
    zoneTxt := (zoneSide == 1 ? "▲" : "▼") + " opened past 70% · 80% line not hit by 10:30 -> ~20% by 11:30"
if intraday and nyT >= 690 and nyTprev < 690
    zoneTxt := ""

// 8:30-9:29 range (news reaction) for CPI/NFP mornings
var float r830H = na
var float r830L = na
var float r820H = na
var float r820L = na
var float r800H = na
var float r800L = na
if intraday and startOf(480)
    r800H := high
    r800L := low
else if intraday and nyT > 480 and nyT < 570 and not na(r800H)
    r800H := math.max(r800H, high)
    r800L := math.min(r800L, low)
if intraday and startOf(500)
    r820H := high
    r820L := low
else if intraday and nyT > 500 and nyT < 570 and not na(r820H)
    r820H := math.max(r820H, high)
    r820L := math.min(r820L, low)
if intraday and startOf(510)
    r830H := high
    r830L := low
else if intraday and nyT > 510 and nyT < 570 and not na(r830H)
    r830H := math.max(r830H, high)
    r830L := math.min(r830L, low)
if newDay
    r830H := na
    r830L := na
    r800H := na
    r800L := na
    r820H := na
    r820L := na

// ─────────────────────────────────────────────────────────── expected 9:30-10:00 range (shown 9:30-10:00)
// Median OR30 width x daily range by overnight-range tercile (quiet / normal / big), stable in both eras;
// CPI/NFP mornings ~1.12x, FOMC mornings ~0.88x.
var string expTxt = ""
var string ewStr = ""
var string expTip = ""
var string awStr = ""
var float f5H = na
var float f5L = na
if newDay
    expTxt := ""
    expTip := ""
if intraday and startOf(570) and not na(dHar) and not na(onUsed930)
    float c1 = S == 0 ? 0.423 : S == 1 ? 0.454 : S == 2 ? 0.445 : 0.581
    float c2 = S == 0 ? 0.612 : S == 1 ? 0.656 : S == 2 ? 0.646 : 0.848
    float mq = S == 0 ? 0.288 : S == 1 ? 0.236 : S == 2 ? 0.278 : 0.195
    float mn = S == 0 ? 0.331 : S == 1 ? 0.289 : S == 2 ? 0.313 : 0.230
    float mb = S == 0 ? 0.386 : S == 1 ? 0.336 : S == 2 ? 0.368 : 0.272
    int tq = onUsed930 < c1 ? 0 : onUsed930 > c2 ? 2 : 1
    float nm = newsIsToday ? (str.contains(newsTxt, "FOMC") ? 0.88 : 1.12) : 1.0
    float ew = (tq == 0 ? mq : tq == 2 ? mb : mn) * nm * dHar
    // median 9:30-11:30 range x daily range by the same overnight tercile (eras within 0.04)
    float aq = S == 0 ? 0.449 : S == 1 ? 0.394 : S == 2 ? 0.436 : 0.357
    float an = S == 0 ? 0.542 : S == 1 ? 0.486 : S == 2 ? 0.491 : 0.414
    float ab = S == 0 ? 0.613 : S == 1 ? 0.573 : S == 2 ? 0.598 : 0.496
    // news mornings: CPI/NFP 1.03-1.16x, FOMC 0.77-0.96x of the normal 9:30-11:30 range (both eras)
    float nmA = newsIsToday ? (str.contains(newsTxt, "FOMC") ? (S == 3 ? 0.94 : 0.86) : 1.08) : 1.0
    // Mondays: 9:30-11:30 range 0.89-0.96x normal on all 4 markets, both eras (gold 9:30-10 also ~0.9x)
    bool isMon = dayofweek(time, "America/New_York") == dayofweek.monday
    // yesterday was an inside day -> 9:30-11:30 range NQ 1.11-1.18x, ES 1.04-1.10x, YM 1.12-1.13x, gold 1.04-1.08x (both eras)
    bool insideY = not na(dH2) and not na(dPdH) and dPdH <= dH2 and dPdL >= dL2
    float insM = insideY ? (S == 0 ? 1.14 : S == 1 ? 1.07 : S == 2 ? 1.12 : 1.06) : 1.0
    float aw = (tq == 0 ? aq : tq == 2 ? ab : an) * nmA * (isMon ? 0.92 : 1.0) * insM * dHar
    // inside day also lifts the 9:30-10:00 range for the index futures: NQ 1.07/1.05, ES 1.08/1.05, YM 1.12/1.07 (gold mixed)
    if insideY and S != 3
        ew := ew * (S == 2 ? 1.09 : 1.06)
    // CPI/NFP mornings: 9:30-11:30 range by the size of the 8:30 reaction (8:30-9:29 range / daily range, terciles),
    // both eras: NQ 0.49/0.61/0.71, ES 0.44/0.54/0.67, YM 0.44/0.56/0.67, gold 0.41/0.50/0.57
    if newsIsToday and not str.contains(newsTxt, "FOMC") and not na(r830H)
        float rxN = (r830H - r830L) / dHar
        float k1 = S == 0 ? 0.274 : S == 1 ? 0.302 : S == 2 ? 0.282 : 0.437
        float k2 = S == 0 ? 0.471 : S == 1 ? 0.504 : S == 2 ? 0.466 : 0.811
        int qN = rxN < k1 ? 0 : rxN > k2 ? 2 : 1
        float mN = S == 0 ? (qN == 0 ? 0.493 : qN == 1 ? 0.612 : 0.707) : S == 1 ? (qN == 0 ? 0.442 : qN == 1 ? 0.542 : 0.671) : S == 2 ? (qN == 0 ? 0.444 : qN == 1 ? 0.558 : 0.67) : (qN == 0 ? 0.41 : qN == 1 ? 0.504 : 0.568)
        aw := mN * dHar
        // 9:30-10:00 range by the same terciles (index futures; gold not monotonic in 2010-18): NQ .33/.36/.42, ES .29/.32/.40, YM .30/.34/.40
        if S != 3
            float eN = S == 0 ? (qN == 0 ? 0.326 : qN == 1 ? 0.359 : 0.416) : S == 1 ? (qN == 0 ? 0.291 : qN == 1 ? 0.319 : 0.395) : (qN == 0 ? 0.298 : qN == 1 ? 0.34 : 0.402)
            ew := eN * dHar
    if isMon and S == 3
        ew := ew * 0.9
    // gold: after a big day (>1.3x) the 9:30-11:30 range was 0.87-0.89x, after a quiet day (<0.7x) 1.08-1.15x (both eras)
    if S == 3 and not na(dPrevRatio)
        aw := aw * (dPrevRatio > 1.3 ? 0.88 : dPrevRatio < 0.7 ? 1.1 : 1.0)
    // index futures, non-news days: 8:00-9:29 range terciles -> 9:30-11:30 range 0.87-0.93x (quiet) / 1.08-1.13x (busy), both eras
    if S != 3 and not newsIsToday and not na(r800H)
        float p8 = (r800H - r800L) / dHar
        float pq1 = S == 0 ? 0.169 : 0.179
        float pq2 = S == 0 ? 0.239 : S == 1 ? 0.261 : 0.26
        aw := aw * (p8 < pq1 ? 0.9 : p8 > pq2 ? 1.1 : 1.0)
        ew := ew * (p8 < pq1 ? 0.9 : p8 > pq2 ? 1.1 : 1.0)
    // gold, non-news days: 8:20-9:29 (COMEX open) range terciles (<0.258 / >0.38 x daily range) -> 9:30-11:30 range 0.88-0.92x / 1.06-1.11x
    if S == 3 and not newsIsToday and not na(r820H)
        float g82 = (r820H - r820L) / dHar
        aw := aw * (g82 < 0.258 ? 0.9 : g82 > 0.38 ? 1.08 : 1.0)
        ew := ew * (g82 < 0.258 ? 0.9 : g82 > 0.38 ? 1.09 : 1.0)
    ewStr := fmtR(ew)
    awStr := fmtR(aw)
    expTxt := "10:00 ~" + fmtR(ew) + " · 11:30 ~" + fmtR(aw) + " (" + (tq == 0 ? "quiet" : tq == 2 ? "big" : "normal") + " ON" + (newsIsToday ? ", news" : "") + (insideY ? ", inside day" : "") + ")"
    // gold: big / normal overnight range and 9:30 in its middle 40% -> 9:30-11:30 stayed inside the overnight range 69-72% / 52-53% (both eras)
    if S == 3 and not na(onH) and not na(onL) and onH > onL
        float onPos = (open - onL) / (onH - onL)
        if onPos > 0.3 and onPos < 0.7 and tq >= 1
            expTxt := expTxt + " · inside ON range to 11:30 " + (tq == 2 ? "69-72%" : "52-53%")
    // monthly OPEX Friday (3rd Friday): 9:30-10:00 closed down NQ 59/62%, ES 65/60%, YM 62/68% (n~100/90 per era); gold none
    if S != 3 and dayofweek(time, "America/New_York") == dayofweek.friday and dayofmonth(time, "America/New_York") >= 15 and dayofmonth(time, "America/New_York") <= 21
        expTxt := expTxt + " · OPEX ▼10:00 " + (S == 0 ? "59-62%" : S == 1 ? "60-65%" : "62-68%")
    // Monday after OPEX: 9:30-10:00 up NQ 63/63%, ES 56/61%, YM 56/59%
    if S != 3 and dayofweek(time, "America/New_York") == dayofweek.monday and dayofmonth(time, "America/New_York") >= 18 and dayofmonth(time, "America/New_York") <= 24
        expTxt := expTxt + " · post-OPEX ▲10:00 " + (S == 0 ? "63%" : S == 1 ? "56-61%" : "56-59%")
if nyT >= 690 or nyT < 570
    expTxt := ""
// 9:35 update: the first 5-minute range is the best early read of the 9:30-10:00 range (median by its tercile, both eras):
// NQ .26/.33/.42, ES .23/.28/.37, YM .24/.31/.41, gold .18/.23/.29 x daily range
if intraday and startOf(570)
    f5H := high
    f5L := low
else if intraday and nyT > 570 and nyT < 575 and not na(f5H)
    f5H := math.max(f5H, high)
    f5L := math.min(f5L, low)
if intraday and nyT >= 575 and nyTprev < 575 and expTxt != "" and ewStr != "" and not na(f5H) and not na(dHar)
    float r5 = (f5H - f5L) / dHar
    float q1 = S == 0 ? 0.142 : S == 1 ? 0.12 : S == 2 ? 0.14 : 0.082
    float q2 = S == 0 ? 0.195 : S == 1 ? 0.165 : S == 2 ? 0.2 : 0.121
    int z5 = r5 < q1 ? 0 : r5 > q2 ? 2 : 1
    float m5 = S == 0 ? (z5 == 0 ? 0.263 : z5 == 1 ? 0.326 : 0.423) : S == 1 ? (z5 == 0 ? 0.226 : z5 == 1 ? 0.281 : 0.37) : S == 2 ? (z5 == 0 ? 0.24 : z5 == 1 ? 0.305 : 0.412) : (z5 == 0 ? 0.179 : z5 == 1 ? 0.228 : 0.29)
    expTxt := str.replace(expTxt, "10:00 ~" + ewStr, "10:00 ~" + fmtR(m5 * dHar) + " (5m)", 0)
    // 11:30 by the same first-5-min tercile (non-news days; CPI/NFP keep the 8:30-reaction estimate), both eras:
    // NQ .44/.52/.64, ES .39/.47/.60, YM .41/.51/.62, gold .35/.41/.51
    if not newsIsToday
        float a5 = S == 0 ? (z5 == 0 ? 0.439 : z5 == 1 ? 0.516 : 0.641) : S == 1 ? (z5 == 0 ? 0.393 : z5 == 1 ? 0.474 : 0.598) : S == 2 ? (z5 == 0 ? 0.409 : z5 == 1 ? 0.508 : 0.615) : (z5 == 0 ? 0.347 : z5 == 1 ? 0.409 : 0.505)
        expTxt := str.replace(expTxt, "11:30 ~" + awStr, "11:30 ~" + fmtR(a5 * dHar), 0)
        // 75th percentile of the 9:30-11:30 range by the same tercile (both eras): NQ .58/.68/.84, ES .51/.61/.77, YM .54/.65/.79, gold .48/.55/.68
        float p75 = S == 0 ? (z5 == 0 ? 0.578 : z5 == 1 ? 0.679 : 0.842) : S == 1 ? (z5 == 0 ? 0.51 : z5 == 1 ? 0.609 : 0.774) : S == 2 ? (z5 == 0 ? 0.535 : z5 == 1 ? 0.65 : 0.791) : (z5 == 0 ? 0.48 : z5 == 1 ? 0.545 : 0.677)
        expTip := "From the first 5-minute bar: typical 9:30-11:30 range ~" + fmtR(a5 * dHar) + "; 1 morning in 4 reached ~" + fmtR(p75 * dHar) + " or more (Databento 2010-26, both eras). Typical, not a cap."

// ─────────────────────────────────────────────────────────── opening ranges (16-year stats, drawn at the OR close)
var float or15H = na
var float or15L = na
var float or30H = na
var float or30L = na
var array<box> oQ = array.new<box>()
var array<line> oLQ = array.new<line>()
var array<label> oLbQ = array.new<label>()
var string or15Txt = ""
var string or30Txt = ""
var string or15Tip = ""
var string or30Tip = ""
bool inOR15 = nyT >= 570 and nyT < 585
bool inOR30 = nyT >= 570 and nyT < 600
if intraday and startOf(570)
    or15H := high
    or15L := low
    or30H := high
    or30L := low
    or15Txt := ""
    or30Txt := ""
else
    if inOR15
        or15H := math.max(or15H, high)
        or15L := math.min(or15L, low)
    if inOR30
        or30H := math.max(or30H, high)
        or30L := math.min(or30L, low)

// cut = [p33, p67] of OR width / daily HAR; ext arrays and ft are by size (small, normal, large)
drawOR(string nm, float h, float l, array<float> cutA, array<float> u50, array<float> u80, array<float> d50, array<float> d80, array<float> ft) =>
    float w = h - l
    int t930 = timestamp("America/New_York", year(time, "America/New_York"), month(time, "America/New_York"), dayofmonth(time, "America/New_York"), 9, 30)
    int t1130 = t930 + 120 * 60000
    float wn = w / dHar
    int sz = wn < array.get(cutA, 0) ? 0 : wn > array.get(cutA, 1) ? 2 : 1
    string grade = sz == 0 ? "small" : sz == 2 ? "large" : "normal"
    box bx = box.new(t930, h, t1130, l, xloc = xloc.bar_time, border_color = color.new(orCol, 100), border_width = 0, bgcolor = color.new(orCol, 100))   // box hidden (lines + midpoint kept)
    line l1 = line.new(time, h + array.get(u50, sz) * w, t1130, h + array.get(u50, sz) * w, xloc = xloc.bar_time, color = color.new(orCol, 55), style = line.style_dotted, width = orW)
    line l2 = line.new(time, h + array.get(u80, sz) * w, t1130, h + array.get(u80, sz) * w, xloc = xloc.bar_time, color = color.new(orCol, 10), width = orW)
    line l3 = line.new(time, l - array.get(d50, sz) * w, t1130, l - array.get(d50, sz) * w, xloc = xloc.bar_time, color = color.new(orCol, 55), style = line.style_dotted, width = orW)
    line l4 = line.new(time, l - array.get(d80, sz) * w, t1130, l - array.get(d80, sz) * w, xloc = xloc.bar_time, color = color.new(orCol, 10), width = orW)
    float f = array.get(ft, sz)
    string tip = nm + " is " + grade + " (" + fmtPts(w) + " pts). Since 2010, for " + grade + " opening ranges: by 11:30 price got past the OR high by the dashed line on half of days and the solid line on 80% (same below). After the first break it reached one more OR width before tagging the other side " + str.tostring(math.round(f * 100)) + "% of the time. Which side breaks first matched a coin flip."
    label lb = label.new(t1130, h + array.get(u80, sz) * w, nm + " ext", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny, tooltip = tip)
    string txt = grade + " " + fmtPts(w) + " · FT " + str.tostring(math.round(f * 100)) + "%"
    [bx, l1, l2, l3, l4, lb, txt, tip]

if not na(dHar) and showOR15 and intraday and timeframe.in_seconds() <= 900 and nyT >= 585 and nyTprev < 585 and nyTprev >= 570
    [bx, l1, l2, l3, l4, lb, txt, tip] = drawOR("OR15", or15H, or15L, f_or15_cut(S), f_or15_u50(S), f_or15_u80(S), f_or15_d50(S), f_or15_d80(S), f_or15_ft(S))
    array.push(oQ, bx)
    array.push(oLQ, l1)
    array.push(oLQ, l2)
    array.push(oLQ, l3)
    array.push(oLQ, l4)
    array.push(oLbQ, lb)
    or15Txt := txt
    or15Tip := tip
if not na(dHar) and showOR30 and intraday and timeframe.in_seconds() <= 1800 and nyT >= 600 and nyTprev < 600 and nyTprev >= 570
    [bx, l1, l2, l3, l4, lb, txt, tip] = drawOR("OR30", or30H, or30L, f_or30_cut(S), f_or30_u50(S), f_or30_u80(S), f_or30_d50(S), f_or30_d80(S), f_or30_ft(S))
    array.push(oQ, bx)
    array.push(oLQ, l1)
    array.push(oLQ, l2)
    array.push(oLQ, l3)
    array.push(oLQ, l4)
    array.push(oLbQ, lb)
    or30Txt := txt
    or30Tip := tip
while array.size(oQ) > sessDays * 2
    box.delete(array.shift(oQ))
while array.size(oLQ) > sessDays * 8
    line.delete(array.shift(oLQ))
while array.size(oLbQ) > sessDays * 2
    label.delete(array.shift(oLbQ))
if newDay
    or15Txt := ""
    or30Txt := ""

// ─────────────────────────────────────────────────────────── heavy opening volume (tested NQ / YM)
// 9:30-9:45 volume > 1.25x its 20-day average: OR15 breaks followed through 54/59% (NQ) and 53/58% (YM) in the
// two eras; YM also kept the 9:30-9:45 candle direction into 11:30 57/56%. ES weak, gold reversed: not shown.
var string volTxt = ""
var float rvAt945 = na
if newDay
    volTxt := ""
    rvAt945 := na
// summed from the chart's own bars (a 15m request reads the unfinished bar live and in Replay)
var float v15acc = na
var array<float> v15hist = array.new<float>()
var float orMinVol = na
float rv15 = na
if intraday and startOf(570)
    v15acc := 0.0
if intraday and nyT >= 570 and nyT < 585 and not na(v15acc)
    v15acc += nz(volume)
if intraday and nyT >= 585 and nyTprev < 585 and not na(v15acc)
    rv15 := array.size(v15hist) >= 10 ? v15acc / array.avg(v15hist) : na
    array.push(v15hist, v15acc)
    if array.size(v15hist) > 20
        array.shift(v15hist)
    orMinVol := v15acc / 15.0
    v15acc := na
if intraday and nyT >= 585 and nyTprev < 585 and not na(rv15)
    rvAt945 := rv15
    if rv15 > 1.25 and (S == 0 or S == 2)
        int dirOR = close[1] > o930 ? 1 : -1
        // audit 2026-10-05: OR break follow-through on heavy volume 53-54% in 2010-18 -> claim removed; YM lean 56.3/56.3% kept
        volTxt := "heavy x" + str.tostring(rv15, "#.#") + (S == 2 ? " · 9:45-11:30 lean " + (dirOR == 1 ? "▲" : "▼") + " 56%" : "")

// ─────────────────────────────────────────────────────────── 9:45 setups (combination search, 2010-26 1m)
// Kept only combos that held >=56% in both eras (n>=100 each) and also held on the other equity index futures.
// Outcome: direction from 9:45 to 11:30. Equities only.
var string setupTxt = ""
var color setupC = color.gray
var int setupDir = 0
var int setupKind = 0
if newDay
    setupTxt := ""
    setupDir := 0
    setupKind := 0
// gold: heavy 9:30-9:45 volume + all three overnight 4H candles + move since 18:00 agree -> 58% / 58% (n~104 each era)
// overnight VWAP (18:00-9:29) and prior regular-session VWAP (9:30-16:00), hlc3 x volume
var float onPV = 0.0
var float onVol = 0.0
var float rthPV = 0.0
var float rthVol = 0.0
var float prevRthVwap = na
var float onVwap930 = na
if newDay
    prevRthVwap := rthVol > 0 ? rthPV / rthVol : prevRthVwap
    onPV := 0.0
    onVol := 0.0
    rthPV := 0.0
    rthVol := 0.0
if intraday and (nyT >= 1080 or nyT < 570)
    onPV += hlc3 * nz(volume)
    onVol += nz(volume)
if intraday and nyT >= 570 and nyT < 960
    rthPV += hlc3 * nz(volume)
    rthVol += nz(volume)
if intraday and startOf(570)
    onVwap930 := onVol > 0 ? onPV / onVol : na
var float mnOpen = na
if newDay
    mnOpen := na
if intraday and startOf(0)
    mnOpen := open
// daily profile from the midnight open: 9:30 above it -> 9:30-11:30 makes the futures-day low only 6-16% (high 24-36%);
// below it -> day high only 5-14% (low 26-36%). Databento 1m, every era 2010-18 / 2019-22 / 2023-26, n 417-1111 each.
var string profTxt = ""
if intraday and startOf(570) and not na(mnOpen) and open != mnOpen
    bool abM = open > mnOpen
    // gap (vs yesterday's 4:00 PM close) on the same side as the midnight open sharpens it; the opposite gap weakens it
    bool agree = na(c16prev) or (abM ? open > c16prev : open < c16prev)
    string pLo = agree ? (S == 0 ? "13-15%" : S == 1 ? "8-10%" : S == 2 ? "8-13%" : "5-7%") : (S == 0 ? "18-22%" : S == 1 ? "16-19%" : S == 2 ? "13-20%" : "11-20%")
    string pHi = agree ? (S == 0 ? "9-12%" : S == 1 ? "5-8%" : S == 2 ? "5-10%" : "4-5%") : (S == 0 ? "14-19%" : S == 1 ? "10-18%" : S == 2 ? "16-21%" : "9-12%")
    profTxt := (abM ? "above 0:00 open · AM makes the day low only " + pLo : "below 0:00 open · AM makes the day high only " + pHi) + (agree ? "" : " (gap disagrees)")
    // weekly: 9:30 above the week open (Sun 18:00) -> this AM sets the week low 0-6%; below -> week high 0-5% (all markets, every era)
    if not na(wOpenHTF) and open != wOpenHTF
        profTxt := profTxt + (open > wOpenHTF ? " · week low here ≤6%" : " · week high here ≤5%")
if intraday and (newDay or startOf(690))
    profTxt := ""
// touch odds by 11:30 by distance from the 9:30 open (x daily range: .05 .1 .2 .35 inf), Databento 2010-26, eras within ~0.06
f_vwOn(int s) =>
    switch s
        0 => array.from(0.915, 0.825, 0.685, 0.525, 0.33)
        1 => array.from(0.935, 0.795, 0.655, 0.50, 0.315)
        2 => array.from(0.94, 0.80, 0.64, 0.475, 0.245)
        => array.from(0.915, 0.73, 0.57, 0.37, 0.15)
f_vwRth(int s) =>
    switch s
        0 => array.from(0.92, 0.82, 0.65, 0.415, 0.155)
        1 => array.from(0.935, 0.83, 0.62, 0.39, 0.135)
        2 => array.from(0.915, 0.82, 0.62, 0.40, 0.14)
        => array.from(0.91, 0.77, 0.565, 0.32, 0.085)
vwBin(float d) => d <= 0.05 ? 0 : d <= 0.1 ? 1 : d <= 0.2 ? 2 : d <= 0.35 ? 3 : 4
// same, re-measured from the 9:45 close for VWAPs not touched by 9:45
f_vw945On(int s) =>
    switch s
        0 => array.from(0.82, 0.74, 0.59, 0.36, 0.19)
        1 => array.from(0.92, 0.72, 0.56, 0.35, 0.21)
        2 => array.from(0.90, 0.75, 0.59, 0.34, 0.18)
        => array.from(0.80, 0.68, 0.49, 0.31, 0.11)
f_vw945Rth(int s) =>
    switch s
        0 => array.from(0.84, 0.77, 0.60, 0.34, 0.09)
        1 => array.from(0.85, 0.75, 0.55, 0.35, 0.09)
        2 => array.from(0.90, 0.75, 0.58, 0.30, 0.09)
        => array.from(0.73, 0.66, 0.51, 0.28, 0.06)

// at 9:45: typical extra range beyond the 9:30-9:45 range by 11:30 (median x daily range by OR15 size, eras within ~0.03)
if intraday and nyT >= 585 and nyTprev < 585 and expTxt != "" and not na(or15H) and not na(dHar)
    array<float> cutE = f_or15_cut(S)
    float wE = (or15H - or15L) / dHar
    int zE = wE < array.get(cutE, 0) ? 0 : wE > array.get(cutE, 1) ? 2 : 1
    float addE = S == 0 ? (zE == 0 ? 0.235 : zE == 1 ? 0.252 : 0.29) : S == 1 ? (zE == 0 ? 0.222 : zE == 1 ? 0.252 : 0.287) : S == 2 ? (zE == 0 ? 0.222 : zE == 1 ? 0.247 : 0.252) : (zE == 0 ? 0.225 : zE == 1 ? 0.236 : 0.233)
    expTxt := expTxt + " · +~" + fmtR(addE * dHar) + " past 15m"
// gold: 9:30-10:00 stayed inside the 8:20-9:29 range -> that range breaks by 11:30 70-74%; side nearer the 10:00 price
// 54-56%, far side only 21-22% (Databento 1m, 2010-18 / 2019-22 / 2023-26, n 219-842 per era)
if S == 3 and intraday and nyT >= 600 and nyTprev < 600 and expTxt != "" and not na(r820H) and not na(or30H) and or30H <= r820H and or30L >= r820L
    expTxt := expTxt + " · inside 8:20 range: breaks by 11:30 70-74% (far side 21-22%)"
// OR15 far end as the 9:30-10 stop: green 9:30-9:45 -> its low unbroken (red: its high), by where it closed and its size
// held to 10:00 74-96% / to 11:30 39-72% when it closed in the far third, all 4 markets, both eras (Databento 2010-26)
var string holdTxt = ""
var string holdTip = ""
var string hold11 = ""
var int plan30Dir = 0
var bool plan30Ext = false
var float holdLv = na
var int holdDir = 0
if intraday and startOf(570)
    holdTxt := ""
    holdLv := na
    holdDir := 0
if intraday and nyT >= 585 and nyTprev < 585 and not na(o930) and not na(or15H) and not na(dHar)
    int d15 = close[1] > o930 ? 1 : close[1] < o930 ? -1 : 0
    if d15 != 0
        float rg = math.max(or15H - or15L, syminfo.mintick)
        float clp = (close[1] - or15L) / rg
        array<float> cutH = f_or15_cut(S)
        float wN = (or15H - or15L) / dHar
        bool strongC = d15 == 1 ? clp > 2.0 / 3.0 : clp < 1.0 / 3.0
        bool weakC = d15 == 1 ? clp < 1.0 / 3.0 : clp > 2.0 / 3.0
        int cell = strongC ? (wN < array.get(cutH, 0) ? 0 : wN > array.get(cutH, 1) ? 2 : 1) : weakC ? 4 : 3
        array<int> hv = f_hold(S)
        int i = cell * 4
        string p10 = prng(array.get(hv, i), array.get(hv, i + 1))
        string p11 = prng(array.get(hv, i + 2), array.get(hv, i + 3))
        holdDir := d15
        holdLv := d15 == 1 ? or15L : or15H
        array<int> ev = f_ext(S)
        int j = cell * 3
        string pe = prng(array.get(ev, j), array.get(ev, j + 1))
        float typ = array.get(ev, j + 2) / 100.0 * rg
        string tag = strongC ? "" : weakC ? " (weak close)" : " (mid close)"
        string lvS = (d15 == 1 ? "low " : "high ") + str.tostring(holdLv, format.mintick)
        holdTxt := (d15 == 1 ? "new high " : "new low ") + pe + " (+" + fmtR(typ) + ") · " + lvS + " holds " + p10 + tag
        hold11 := lvS + " holds to 11:30 " + p11 + tag
        // gold only: large 9:30-9:45 candle closed near its end -> 9:45-10:00 went the other way 56% / 59% (n=434/477)
        if S == 3 and cell == 2
            holdTxt := holdTxt + " · pullback to 10:00 56-59%"
            holdTip := holdTip + " Gold: after a large opening candle that closed near its end, 9:45-10:00 closed against it 56% (2010-18) / 59% (2019-26) of the time."
        holdTip := "9:45-10:00: chance price trades beyond the 9:30-9:45 candle's " + (d15 == 1 ? "high" : "low") + " (" + pe + "; typical push when it does: +" + fmtPts(typ) + "), and chance its other end stays unbroken until 10:00 (" + p10 + ") / 11:30 (" + p11 + "). Based on candles that closed " + (strongC ? "near their end (" + (cell == 0 ? "small" : cell == 2 ? "large" : "normal") + " size)" : weakC ? "back near their start" : "mid-range") + ". Ranges = 2010-18 vs 2019-26 values, all on Databento 1-minute data." + (strongC ? " Timing: after a candle like this the 9:30-11:30 " + (d15 == 1 ? "high" : "low") + " printed at a median ~10:20; only 30-36% by 10:00 and 41-49% after 10:30 (all 4 markets, both eras), so early exits often leave the best price on the table." : "")
// 10:00 plan from the 9:30-10:00 range (Databento 2010-26, both eras): chance 10:00-11:30 trades beyond its close-side
// end (strong close 76-89%) and that its far end holds to 11:30 (strong close 49-85% by size)
if intraday and nyT >= 600 and nyTprev < 600 and not na(o930) and not na(or30H) and not na(dHar)
    int d30 = close[1] > o930 ? 1 : close[1] < o930 ? -1 : 0
    if d30 != 0
        float rg30 = math.max(or30H - or30L, syminfo.mintick)
        float clp30 = (close[1] - or30L) / rg30
        array<float> cut30 = f_or30_cut(S)
        float wN30 = (or30H - or30L) / dHar
        bool st30 = d30 == 1 ? clp30 > 2.0 / 3.0 : clp30 < 1.0 / 3.0
        bool wk30 = d30 == 1 ? clp30 < 1.0 / 3.0 : clp30 > 2.0 / 3.0
        int c30 = st30 ? (wN30 < array.get(cut30, 0) ? 0 : wN30 > array.get(cut30, 1) ? 2 : 1) : wk30 ? 4 : 3
        array<int> h30 = f_hold30(S)
        int q = c30 * 5
        string pb = prng(array.get(h30, q), array.get(h30, q + 1))
        string ph = prng(array.get(h30, q + 3), array.get(h30, q + 4))
        float push = array.get(h30, q + 2) / 100.0 * rg30
        holdDir := d30
        holdLv := d30 == 1 ? or30L : or30H
        plan30Dir := st30 ? d30 : 0
        plan30Ext := false
        string tag30 = st30 ? "" : wk30 ? " (weak close)" : " (mid close)"
        holdTxt := (d30 == 1 ? "new high " : "new low ") + pb + " (+" + fmtR(push) + ") · " + (d30 == 1 ? "low " : "high ") + str.tostring(holdLv, format.mintick) + " holds " + ph + tag30
        holdTip := "10:00-11:30: chance price trades beyond the 9:30-10:00 range's " + (d30 == 1 ? "high" : "low") + " (" + pb + "; typical push when it does +" + fmtPts(push) + "), and chance the range's other end stays unbroken until 11:30 (" + ph + "). Ranges = 2010-18 vs 2019-26 values (Databento 1-minute)."
        float sv10 = rthVol > 0 ? rthPV / rthVol : na
        if not na(sv10)
            float dv10 = math.abs(close[1] - sv10) / dHar
            string pv10 = dv10 <= 0.05 ? "89-92%" : dv10 <= 0.1 ? "73-79%" : dv10 <= 0.2 ? "55-61%" : ""
            if pv10 != ""
                holdTip := holdTip + " Today's 9:30-10 VWAP " + str.tostring(sv10, format.mintick) + ": price came back to it by 11:30 " + pv10 + " of the time from this distance (side of VWAP gave no direction edge)."
    else if holdDir != 0
        holdTxt := hold11
if intraday and holdDir != 0 and nyT > 585 and nyT < 690 and not na(holdLv)
    if (holdDir == 1 and low < holdLv) or (holdDir == -1 and high > holdLv)
        holdTxt := (nyT >= 600 ? "30m " : "15m ") + (holdDir == 1 ? "low " : "high ") + str.tostring(holdLv, format.mintick) + " broken"
        holdDir := 0
// 10:30 time stop for the 10:00 plan (range closed near its end): no push past it by 10:30 -> only 28-34% by 11:30 (all 4 markets, both eras)
if intraday and plan30Dir != 0 and nyT >= 600 and nyT < 630 and not na(or30H)
    if (plan30Dir == 1 and high > or30H) or (plan30Dir == -1 and low < or30L)
        plan30Ext := true
bool stall30 = intraday and plan30Dir != 0 and nyT >= 630 and nyTprev < 630 and not plan30Ext
if stall30 and holdTxt != ""
    holdTxt := holdTxt + " · no new " + (plan30Dir == 1 ? "high" : "low") + " by 10:30 -> 28-34% by 11:30"
if newDay
    plan30Dir := 0
if intraday and nyT >= 690 and nyTprev < 690
    holdTxt := ""
    holdDir := 0
if intraday and S == 3 and nyT >= 585 and nyTprev < 585 and not na(o930) and not na(dOpen)
    int s18d = o930 > dOpen ? 1 : o930 < dOpen ? -1 : 0
    if not na(rvAt945) and rvAt945 > 1.25 and h4agree != 0 and h4agree == s18d
        setupDir := h4agree
        setupTxt := (h4agree == 1 ? "▲" : "▼") + " B: heavy vol + 4H + overnight agree (56-58%)"
        setupC := h4agree == 1 ? color.new(dnBase, 0) : color.new(upBase, 0)
if intraday and S != 3 and nyT >= 585 and nyTprev < 585 and not na(o930) and not na(dHar)
    int orDir = close[1] > o930 ? 1 : close[1] < o930 ? -1 : 0
    int h4maj = int(math.sign(h4a + h4b + h4c))
    array<float> cut15 = f_or15_cut(S)
    bool abovePDH = not na(pdH) and o930 > pdH
    bool heavy = not na(rvAt945) and rvAt945 > 1.25
    // audited 2026-10-05 as displayed (one setup per day, this priority), 9:45 -> 11:30, Databento 2010-26:
    // NQ A 62.6/59.9% (n 190/212), NQ B 56.5/55.9% (n 207/136), ES B 62.7/57.3% (n 241/192). Dropped: "open > PDH + small OR"
    // and "all 4H + open agree" (coin flips once earlier setups are taken), ES A (55%) and all YM setups (54-55% in 2019-26).
    if S == 0 and abovePDH and orDir == 1
        setupDir := 1
        setupKind := 1
        setupTxt := "▲ A: open > PDH + green 9:30-45 (60-63%)"
    else if (S == 0 or S == 1) and heavy and orDir != 0 and h4maj == orDir
        setupDir := orDir
        setupKind := 3
        setupTxt := (orDir == 1 ? "▲" : "▼") + " B: heavy vol + 4H agree (" + (S == 0 ? "56-57%" : "57-63%") + ")"
    setupC := setupDir == 1 ? color.new(dnBase, 0) : setupDir == -1 ? color.new(upBase, 0) : txtDim

// 10:00 setups removed 2026-10-05: as displayed (only on days without a 9:45 setup) they ran 47-58% and failed 2010-18.

// setup targets (entry = 9:45 close, until 11:30, x daily HAR range; Databento 2010-26):
// [typical run p50, good run p75, winners' pullback p75] per symbol (NQ, ES, YM) and setup kind (1-4)
f_setupTgt(int s, int k) =>
    array<float> a = array.from(0.201, 0.318, 0.126, 0.171, 0.277, 0.125, 0.262, 0.494, 0.165, 0.206, 0.378, 0.167)
    if s == 1
        a := array.from(0.183, 0.303, 0.112, 0.139, 0.267, 0.112, 0.262, 0.449, 0.187, 0.206, 0.354, 0.156)
    else if s == 2
        a := array.from(0.191, 0.315, 0.126, 0.154, 0.252, 0.107, 0.273, 0.427, 0.192, 0.192, 0.350, 0.167)
    array.slice(a, (k - 1) * 3, k * 3)
var array<line> stQ = array.new<line>()
var float setT1 = na
var float setPB = na
var bool t1Done = false
var bool pbDone = false
if newDay
    setT1 := na
    setPB := na
    t1Done := false
    pbDone := false
var array<label> stLQ = array.new<label>()
// gold setup targets from the 9:44 close (x daily range, both eras): T1 0.20, T2 0.37, winners' pullback 0.17
if S == 3 and intraday and nyT >= 585 and nyTprev < 585 and setupDir != 0 and not na(dHar)
    float eg = close[1]
    int t1130g = timestamp("America/New_York", year(time, "America/New_York"), month(time, "America/New_York"), dayofmonth(time, "America/New_York"), 11, 30)
    setT1 := eg + setupDir * 0.20 * dHar
    setPB := eg - setupDir * 0.17 * dHar
    color gcC = setupDir == 1 ? dnBase : upBase
    array.push(stQ, line.new(time, setT1, t1130g, setT1, xloc = xloc.bar_time, color = color.new(gcC, 20)))
    array.push(stQ, line.new(time, eg + setupDir * 0.37 * dHar, t1130g, eg + setupDir * 0.37 * dHar, xloc = xloc.bar_time, color = color.new(gcC, 45), style = line.style_dashed))
    array.push(stQ, line.new(time, setPB, t1130g, setPB, xloc = xloc.bar_time, color = color.new(invCol, 30), style = f_ls(invStyle)))
    array.push(stLQ, label.new(t1130g, setT1, "setup T1 " + fmtPx(setT1), xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny, tooltip = "Gold setup: median run by 11:30 (0.20x daily range); dashed = 75th pct (0.37x), dotted = where 75% of winners held (0.17x)."))
if setupKind != 0 and setupKind[1] == 0 and S != 3 and not na(dHar)
    array<float> tg = f_setupTgt(S, setupKind)
    float e = close[1]
    int t1130 = timestamp("America/New_York", year(time, "America/New_York"), month(time, "America/New_York"), dayofmonth(time, "America/New_York"), 11, 30)
    float t1 = e + setupDir * array.get(tg, 0) * dHar
    float t2 = e + setupDir * array.get(tg, 1) * dHar
    float pb = e - setupDir * array.get(tg, 2) * dHar
    setT1 := t1
    setPB := pb
    color sc2 = setupDir == 1 ? dnBase : upBase
    array.push(stQ, line.new(time, t1, t1130, t1, xloc = xloc.bar_time, color = color.new(sc2, 20), width = 1))
    array.push(stQ, line.new(time, t2, t1130, t2, xloc = xloc.bar_time, color = color.new(sc2, 45), style = line.style_dashed))
    array.push(stQ, line.new(time, pb, t1130, pb, xloc = xloc.bar_time, color = color.new(invCol, 30), style = f_ls(invStyle)))
    array.push(stLQ, label.new(t1130, t1, "setup T1 " + fmtPx(t1), xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny, tooltip = "Typical run (median) from the 9:45 close by 11:30 after this setup, 2010-26. After T1 is hit, T2 came before a return to the 9:45 entry only 39-53% of the time (both eras), so a breakeven stop gets hit about half the time; banking part at T1 fits the data."))
    array.push(stLQ, label.new(t1130, t2, "T2 " + fmtPx(t2), xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny, tooltip = "Good run (75th percentile) by 11:30 after this setup."))
    array.push(stLQ, label.new(t1130, pb, "pullback " + fmtPx(pb), xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny, tooltip = "75% of winning setups never pulled back past this line before 11:30. Beyond it, the setup is usually not working."))
    while array.size(stQ) > sessDays * 3
        line.delete(array.shift(stQ))
    while array.size(stLQ) > sessDays * 3
        label.delete(array.shift(stLQ))
var bool set10 = false

// alerts for the setup's targets / pullback and the OR invalidation lines
bool t1Hit = not na(setT1) and not t1Done and nyT < 690 and (setupDir == 1 ? high >= setT1 : low <= setT1)
bool pbHit = not na(setPB) and not pbDone and nyT < 690 and (setupDir == 1 ? low <= setPB : high >= setPB)
if t1Hit
    t1Done := true
if pbHit
    pbDone := true
// setup time stop: no T1 by 10:30 -> T1 later only 17-30%, and the setup finished ahead of the 9:45 entry only 36-52% (NQ/ES/YM, both eras)
int stallT = set10 ? 645 : 630
bool setStall = intraday and not na(setT1) and not t1Done and nyT >= stallT and nyTprev < stallT
if setStall and setupTxt != ""
    setupTxt := setupTxt + (set10 ? " · no T1 by 10:45: edge gone (T1 later 18-21%)" : " · no T1 by 10:30: edge gone (T1 later 17-30%)")
    setupC := txtDim

// ─────────────────────────────────────────────────────────── OR break + invalidation (Databento 1m 2010-26)
// After the first break of the OR (before 11:30): winners rarely came back >0.5 OR width inside; once they did,
// success (one more OR width before the opposite edge) fell to ~12-23% (stable in both eras, all four markets).
var array<float> INV15 = f_inv15(S)
var array<float> INV30 = f_inv30(S)
var int brk15 = 0
var int brk30 = 0
var string invTxt = ""
var float invLv15 = na
var int brkBar15 = na
var float invLv30 = na
var array<line> iLQ = array.new<line>()
var array<label> iLbQ = array.new<label>()
if newDay
    brk15 := 0
    brk30 := 0
    invTxt := ""
    invLv15 := na
    invLv30 := na
checkBreak(int L, float h, float l, int brk, array<float> inv) =>
    int nb = brk
    string tx = ""
    line ln = na
    label lb = na
    if brk == 0 and nyT >= 570 + L and nyT < 690 and not na(h) and h > l
        float w = h - l
        if high > h and not (low < l)
            nb := 1
        else if low < l and not (high > h)
            nb := -1
        if nb != 0
            float lv = nb == 1 ? h - 0.5 * w : l + 0.5 * w
            int t1130 = timestamp("America/New_York", year(time, "America/New_York"), month(time, "America/New_York"), dayofmonth(time, "America/New_York"), 11, 30)
            ln := line.new(time, lv, t1130, lv, xloc = xloc.bar_time, color = color.new(invCol, 20), style = f_ls(invStyle))
            string pct = str.tostring(math.round(array.get(inv, 2) * 100)) + "%"
            lb := label.new(t1130, lv, "OR" + str.tostring(L) + " invalid", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny, tooltip = "If price comes back here (0.5 OR width inside) after the " + (nb == 1 ? "upside" : "downside") + " break, the break went on to work only " + pct + " of the time since 2010 (winners rarely came back this far).")
            tx := "OR" + str.tostring(L) + (nb == 1 ? " ▲ break" : " ▼ break") + " · invalid " + fmtPx(lv) + " (then " + pct + ")"
    [nb, tx, ln, lb]
if intraday and timeframe.in_seconds() <= 900
    [nb15, tx15, ln15, lb15] = checkBreak(15, or15H, or15L, brk15, INV15)
    if nb15 != brk15
        brk15 := nb15
        invTxt := tx15
        brkBar15 := bar_index
        invLv15 := nb15 == 1 ? or15H - 0.5 * (or15H - or15L) : or15L + 0.5 * (or15H - or15L)
        array.push(iLQ, ln15)
        array.push(iLbQ, lb15)
// NQ on a 1-minute chart: break minute volume > 1.3x the average 9:30-9:45 minute -> follow-through (0.5 OR width before
// back to mid) 62.9% / 57.8% (n 485/408); judged on the closed break bar. Other markets / 5m bars: no stable edge.
if S == 0 and timeframe.in_seconds() == 60 and not na(brkBar15) and bar_index == brkBar15 + 1 and not na(orMinVol) and orMinVol > 0 and brk15 != 0
    if volume[1] / orMinVol > 1.3 and str.startswith(invTxt, "OR15")
        invTxt := invTxt + " · heavy break, FT 58-63%"
// Early failed break: first OR15 break, then a bar closes back past the OR15 mid before 10:00 -> the far side of the
// OR15 trades by 11:30 (1m/5m, era min-max 2010-18 / 2019-22 / 2023-26): NQ 75-81%, ES 73-82%, YM 70-84%, GC 76-87%
// (n 90-365 per era) vs 27-36% when the break never fails. A touch of the mid later than 10:00 is much weaker (35-65%).
var bool fail15 = false
if newDay
    fail15 := false
if intraday and timeframe.in_seconds() <= 300 and brk15 != 0 and not fail15 and not na(invLv15) and nyT + timeframe.in_seconds() / 60 <= 600
    if brk15 == 1 ? close < invLv15 : close > invLv15
        fail15 := true
        float farLv = brk15 == 1 ? or15L : or15H
        string fp = S == 0 ? "75-81%" : S == 1 ? "73-82%" : S == 2 ? "70-84%" : "76-87%"
        int t1130f = timestamp("America/New_York", year(time, "America/New_York"), month(time, "America/New_York"), dayofmonth(time, "America/New_York"), 11, 30)
        array.push(iLQ, line.new(time, farLv, t1130f, farLv, xloc = xloc.bar_time, color = color.new(farCol, 40), style = line.style_dotted))
        array.push(iLbQ, label.new(t1130f, farLv, "OR15 far side " + fp, xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny, tooltip = "The first OR15 break failed before 10:00 (a bar closed back past the OR15 mid). Since 2010 the far side of the 9:30-9:45 range then traded by 11:30 " + fp + " of the time (vs ~30% when the break holds). Stable in 2010-18, 2019-22 and 2023-26."))
        if str.startswith(invTxt, "OR15")
            invTxt := "OR15 " + (brk15 == 1 ? "▲" : "▼") + " break failed · far side " + fmtPx(farLv) + " by 11:30 " + fp
if intraday and timeframe.in_seconds() <= 1800
    [nb30, tx30, ln30, lb30] = checkBreak(30, or30H, or30L, brk30, INV30)
    if nb30 != brk30
        brk30 := nb30
        invTxt := tx30
        invLv30 := nb30 == 1 ? or30H - 0.5 * (or30H - or30L) : or30L + 0.5 * (or30H - or30L)
        array.push(iLQ, ln30)
        array.push(iLbQ, lb30)
// OR30 break checked at 10:30 (first break before 10:30, 1m/5m bars, era min-max 2010-18 / 2019-22 / 2023-26):
// no bar closed back past the OR30 mid by 10:30 -> far side by 11:30 only NQ 8-11%, ES 11-12%, YM 8-12%, GC 9-14%
// (n 555-1461 per era); a close back past the mid before 10:30 -> far side by 11:30 1m: ES 59-66, YM 56-61, GC 58-68%
// (NQ 54-59%: not shown); 5m: NQ 60-64, ES 65-73, YM 58-68, GC 60-73% (n 127-443 per era).
var bool fail30 = false
var bool held30Done = false
if newDay
    fail30 := false
    held30Done := false
bool tf5ok = timeframe.in_seconds() <= 300
if intraday and tf5ok and brk30 != 0 and not fail30 and not held30Done and not na(invLv30) and nyT + timeframe.in_seconds() / 60 <= 630
    if brk30 == 1 ? close < invLv30 : close > invLv30
        fail30 := true
        bool showF = not (S == 0 and timeframe.in_seconds() < 300)
        if showF
            float far30 = brk30 == 1 ? or30L : or30H
            bool is1m = timeframe.in_seconds() < 300
            string fp30 = is1m ? (S == 1 ? "59-66%" : S == 2 ? "56-61%" : "58-68%") : (S == 0 ? "60-64%" : S == 1 ? "65-73%" : S == 2 ? "58-68%" : "60-73%")
            int t1130g = timestamp("America/New_York", year(time, "America/New_York"), month(time, "America/New_York"), dayofmonth(time, "America/New_York"), 11, 30)
            array.push(iLQ, line.new(time, far30, t1130g, far30, xloc = xloc.bar_time, color = color.new(farCol, 40), style = line.style_dotted))
            array.push(iLbQ, label.new(t1130g, far30, "OR30 far side " + fp30, xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.small : size.tiny, tooltip = "The first OR30 break failed before 10:30 (a bar closed back past the OR30 mid). Since 2010 the far side of the 9:30-10:00 range then traded by 11:30 " + fp30 + " of the time. Stable in 2010-18, 2019-22 and 2023-26."))
            invTxt := "OR30 " + (brk30 == 1 ? "▲" : "▼") + " break failed · far side " + fmtPx(far30) + " by 11:30 " + fp30
if intraday and tf5ok and brk30 != 0 and not fail30 and not held30Done and nyT >= 630 and nyT < 690
    held30Done := true
    string hp = S == 0 ? "8-11%" : S == 1 ? "11-12%" : S == 2 ? "8-12%" : "9-14%"
    invTxt := "OR30 " + (brk30 == 1 ? "▲" : "▼") + " break held to 10:30 · far side " + fmtPx(brk30 == 1 ? or30L : or30H) + " by 11:30 only " + hp
// OR15 run one full OR width past the range (9:45-11:30) -> back to the broken OR edge by 11:30 only NQ 19-27, ES 28-33,
// YM 23-30, GC 39-45% (Databento 1m, 2010-18 / 2019-22 / 2023-26, n 371-1320 per era): don't wait for a retest.
var bool ext15Done = false
if newDay
    ext15Done := false
if intraday and not ext15Done and not na(or15H) and or15H > or15L and nyT >= 585 and nyT < 690
    float w15 = or15H - or15L
    int xd = high >= or15H + w15 ? 1 : low <= or15L - w15 ? -1 : 0
    if xd != 0
        ext15Done := true
        invTxt := "OR15 " + (xd == 1 ? "▲" : "▼") + " ran 1x OR · retest of the OR edge by 11:30 only " + (S == 0 ? "19-27%" : S == 1 ? "28-33%" : S == 2 ? "23-30%" : "39-45%")
// OR15 break that never closed back past the OR15 mid (and never tagged the far side) -> far side by 11:30, 1m/5m,
// era min-max: held to 10:00 NQ 27-31, ES 31-37, YM 26-33, GC 31-38%; held to 10:30 NQ 9-13, ES 12-15, YM 10-14,
// GC 10-16% (n 451-1465 per era).
var int held15 = 0
if newDay
    held15 := 0
if intraday and tf5ok and brk15 != 0 and not fail15 and not na(or15H) and nyT < 690
    bool farHit = brk15 == 1 ? low < or15L : high > or15H
    if farHit
        held15 := 3
    else if held15 == 0 and nyT >= 600 and str.startswith(invTxt, "OR15")
        held15 := 1
        invTxt := "OR15 " + (brk15 == 1 ? "▲" : "▼") + " break held to 10:00 · far side " + fmtPx(brk15 == 1 ? or15L : or15H) + " by 11:30 " + (S == 0 ? "27-31%" : S == 1 ? "31-37%" : S == 2 ? "26-33%" : "31-38%")
    else if held15 <= 1 and nyT >= 630 and (str.startswith(invTxt, "OR15") or brk30 == 0)
        held15 := 2
        invTxt := "OR15 " + (brk15 == 1 ? "▲" : "▼") + " break held to 10:30 · far side " + fmtPx(brk15 == 1 ? or15L : or15H) + " by 11:30 only " + (S == 0 ? "9-13%" : S == 1 ? "12-15%" : S == 2 ? "10-14%" : "10-16%")
while array.size(iLQ) > sessDays * 2
    line.delete(array.shift(iLQ))
while array.size(iLbQ) > sessDays * 2
    label.delete(array.shift(iLbQ))

// ─────────────────────────────────────────────────────────── likely targets by 11:30 (tag odds by distance, 2010-26)
var array<float> TAG = f_tag(S)
var array<float> TAGONMUL = f_tagOnMul(S)
var array<float> GAPF = f_gapfill(S)
if intraday and nyT >= 960 and nyTprev < 960
    c16 := close[1]
if newDay
    c16prev := c16
gapBin(float g) => g <= 0.05 ? 0 : g <= 0.1 ? 1 : g <= 0.2 ? 2 : g <= 0.35 ? 3 : g <= 0.6 ? 4 : 5
var array<line> tLQ = array.new<line>()
var array<label> tLbQ = array.new<label>()
var string tagTxt = ""
var float bestLv = na
var bool bestDone = false
tagBin(float d) => d <= 0.1 ? 0 : d <= 0.2 ? 1 : d <= 0.35 ? 2 : d <= 0.5 ? 3 : d <= 1.0 ? 4 : 5
if showTags and not na(dHar) and intraday and timeframe.in_seconds() <= 1800 and startOf(570)
    float bestP = 0.0
    bestLv := na
    bestDone := false
    string bestN = ""
    int t1130 = time + tOff + tLen
    // k 4/5: prior-week high / low (HTF liquidity) - reached by 11:30 on the prior-day curve (checked: within ~0.1 in
    // every era); skipped when it sits within 0.05x of the prior-day or overnight level on the same side
    // overnight range tercile (Expect-row cuts) scales the odds: big ON -> far targets likelier, quiet ON -> less
    float c1T = S == 0 ? 0.423 : S == 1 ? 0.454 : S == 2 ? 0.445 : 0.581
    float c2T = S == 0 ? 0.612 : S == 1 ? 0.656 : S == 2 ? 0.646 : 0.848
    int tqT = na(onUsed930) ? 1 : onUsed930 < c1T ? 0 : onUsed930 > c2T ? 2 : 1
    for k = 0 to 5
        float lv = k == 0 ? onH : k == 1 ? onL : k == 2 ? pdH : k == 3 ? pdL : k == 4 ? pwh : pwl
        int side = k == 0 or k == 2 or k == 4 ? 1 : -1
        string nm = k == 0 ? "ONH" : k == 1 ? "ONL" : k == 2 ? "PDH" : k == 3 ? "PDL" : k == 4 ? "PWH" : "PWL"
        int kk = k >= 4 ? k - 2 : k
        bool dup = k >= 4 and not na(lv) and (math.abs(lv - (k == 4 ? pdH : pdL)) < 0.05 * dHar or math.abs(lv - (k == 4 ? onH : onL)) < 0.05 * dHar)
        if not na(lv) and not dup
            float d = (lv - open) * side / dHar
            if d > 0
                float pr = math.min(0.97, array.get(TAG, kk * 6 + tagBin(d)) * array.get(TAGONMUL, tqT * 6 + tagBin(d)))
                if pr >= minOdds
                    array.push(tLQ, line.new(time + tOff, lv, t1130, lv, xloc = xloc.bar_time, color = color.new(tagCol, pr >= 0.5 ? 15 : 50), style = f_ls(tagStyle), width = tagW))
                    array.push(tLbQ, label.new(t1130, lv, nm + " " + str.tostring(math.round(pr * 100)) + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Chance this level gets tagged between 9:30 and 11:30, given how far it is from the 9:30 open (Databento 1m, 2010-2026, stable in both halves)."))
                if pr > bestP
                    bestP := pr
                    bestN := nm
                    bestLv := lv
    if not na(c16prev) and c16prev != open
        float g = math.abs(open - c16prev) / dHar
        float pg = array.get(GAPF, gapBin(g))
        // merge with a target label that sits within 0.05x the daily range, so labels never stack
        label near = na
        if array.size(tLbQ) > 0
            for li2 = 0 to array.size(tLbQ) - 1
                label lbx = array.get(tLbQ, li2)
                if not na(lbx) and label.get_x(lbx) == t1130 and math.abs(label.get_y(lbx) - c16prev) <= 0.05 * dHar
                    near := lbx
        if pg >= minOdds and not na(near)
            label.set_text(near, label.get_text(near) + " · GAP " + str.tostring(math.round(pg * 100)) + "%")
        if pg >= minOdds and na(near)
            array.push(tLQ, line.new(time + tOff, c16prev, t1130, c16prev, xloc = xloc.bar_time, color = color.new(tagCol, pg >= 0.5 ? 15 : 50), style = f_ls(tagStyle), width = tagW))
            array.push(tLbQ, label.new(t1130, c16prev, "GAP " + str.tostring(math.round(pg * 100)) + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Chance price trades back to yesterday's 4:00 PM close by 11:30, given the gap size (Databento 1m, 2010-2026, stable in both halves)."))
        if pg > bestP
            bestP := pg
            bestN := "GAP"
            bestLv := c16prev
        // gap CE (midpoint of yesterday's 4:00 PM close and the 9:30 open), ICT opening-gap target: reached by 11:30
        // by its distance (x daily range, bins .05/.1/.175/.25/.35); Databento 1m, eras within ~0.1, n 70+ per cell
        float ce = (open + c16prev) / 2.0
        float dce = math.abs(open - ce) / dHar
        if dce > 0.01
            int cb = dce <= 0.05 ? 0 : dce <= 0.1 ? 1 : dce <= 0.175 ? 2 : dce <= 0.25 ? 3 : dce <= 0.35 ? 4 : 5
            int pce = cb == 0 ? (S == 1 ? 95 : S == 3 ? 90 : 93) : cb == 1 ? (S == 0 ? 83 : S == 1 ? 82 : S == 2 ? 79 : 75) : cb == 2 ? (S == 3 ? 57 : S == 1 ? 67 : S == 2 ? 68 : 69) : cb == 3 ? (S == 0 ? 55 : S == 1 ? 50 : S == 2 ? 49 : 43) : cb == 4 ? (S == 3 ? 34 : S == 0 ? 39 : S == 1 ? 40 : 41) : (S == 0 ? 30 : S == 1 ? 27 : S == 2 ? 24 : 16)
            if pce >= minOdds * 100
                array.push(tLQ, line.new(time + tOff, ce, t1130, ce, xloc = xloc.bar_time, color = color.new(tagCol, pce >= 50 ? 15 : 50), style = f_ls(tagStyle), width = tagW))
                array.push(tLbQ, label.new(t1130, ce, "Gap CE " + str.tostring(pce) + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Midpoint (CE) of the opening gap between yesterday's 4:00 PM close and the 9:30 open. Chance it trades by 11:30 from this distance (Databento 1m 2010-26; 2010-18, 2019-22 and 2023-26 mostly within 10 points). Big gaps reach their CE more often than an ordinary level at the same distance."))
    // week / month open: tag by 11:30 by distance ~94/84/65/42/6% (.05/.1/.2/.35/inf), all 4 markets, both eras; shown when >= 60%
    array<float> WMO = array.from(0.94, 0.84, 0.65, 0.42, 0.06)
    // week-so-far high / low (Tue-Fri, not yet taken): by 11:30 ~90/88/56/35/10% by the same distance bins, both eras
    array<float> WSF = array.from(0.90, 0.88, 0.56, 0.35, 0.10)
    bool wsfUp = not na(dSfH) and open < dSfH
    bool wsfDn = not na(dSfL) and open > dSfL
    // midnight open: by 11:30 ~94/81/65/44/17% by the same bins (all 4 markets, both eras) - like the 18:00 open; shown when >= 60%
    for vk = 0 to 6
        float vlv = vk == 0 ? onVwap930 : vk == 1 ? prevRthVwap : vk == 2 ? wOpenHTF : vk == 3 ? mOpenHTF : vk == 4 ? (wsfUp and onH < dSfH ? dSfH : na) : vk == 5 ? (wsfDn and onL > dSfL ? dSfL : na) : mnOpen
        string vnm = vk == 0 ? "ON VWAP" : vk == 1 ? "VWAP y" : vk == 2 ? "Wk open" : vk == 3 ? "Mo open" : vk == 4 ? "Wk high" : vk == 5 ? "Wk low" : "Midnight"
        bool fresh = (vk == 2 and wOpenHTF == dOpen) or (vk == 3 and mOpenHTF == dOpen)
        if not na(vlv) and vlv != open and not fresh
            int vb = vwBin(math.abs(open - vlv) / dHar)
            float vp = vk == 0 ? array.get(f_vwOn(S), vb) : vk == 1 ? array.get(f_vwRth(S), vb) : vk == 6 ? array.get(array.from(0.94, 0.81, 0.65, 0.44, 0.17), vb) : vk >= 4 ? array.get(WSF, vb) : array.get(WMO, vb)
            if vp >= math.max(minOdds, vk == 6 or vk == 2 or vk == 3 ? 0.6 : 0.5)
                label vnear = na
                if array.size(tLbQ) > 0
                    for li4 = 0 to array.size(tLbQ) - 1
                        label lbv = array.get(tLbQ, li4)
                        if not na(lbv) and label.get_x(lbv) == t1130 and math.abs(label.get_y(lbv) - vlv) <= 0.05 * dHar
                            vnear := lbv
                if not na(vnear)
                    label.set_text(vnear, label.get_text(vnear) + " · " + vnm + " " + str.tostring(math.round(vp * 100)) + "%")
                else
                    array.push(tLQ, line.new(time + tOff, vlv, t1130, vlv, xloc = xloc.bar_time, color = color.new(tagCol, vp >= 0.7 ? 15 : 50), style = f_ls(tagStyle), width = tagW))
                    array.push(tLbQ, label.new(t1130, vlv, vnm + " " + str.tostring(math.round(vp * 100)) + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Chance price trades back to this VWAP (overnight 18:00-9:30, or yesterday's 9:30-16:00) by 11:30, from its distance at the open (Databento 1m 2010-26, both eras). VWAP side gives no direction edge."))
    tagTxt := bestN == "" ? "" : bestN + " · " + str.tostring(math.round(bestP * 100)) + "%"
    while array.size(tLQ) > sessDays * 7
        line.delete(array.shift(tLQ))
    while array.size(tLbQ) > sessDays * 7
        label.delete(array.shift(tLbQ))
// Gold: 8:20 COMEX open retest 9:30-11:30 by |9:30 open - 8:20 open| / daily HAR (<=.05 .1 .2 .35 >.35):
// 90-94 / 75-77 / 55-60 / 36-41 / 14-18% (Databento 1m 2010-18 / 2019-22 / 2023-26, n 123-591 per cell/era).
var float o820 = na
if newDay
    o820 := na
if S == 3 and startOf(500) and nyT < 570
    o820 := open
f_blk1() =>
    float d82 = math.abs(open - o820) / dHar
    int p82 = d82 <= 0.05 ? 91 : d82 <= 0.1 ? 76 : d82 <= 0.2 ? 57 : d82 <= 0.35 ? 38 : 16
    if p82 >= minOdds * 100
        int t1130c = time + tOff + tLen
        array.push(tLQ, line.new(time + tOff, o820, t1130c, o820, xloc = xloc.bar_time, color = color.new(tagCol, p82 >= 50 ? 15 : 50), style = f_ls(tagStyle), width = tagW))
        array.push(tLbQ, label.new(t1130c, o820, "8:20 open " + str.tostring(p82) + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Chance gold trades back to the 8:20 COMEX open between 9:30 and 11:30, given its distance from the 9:30 open (Databento 1m; 2010-18, 2019-22 and 2023-26 within ~6 points)."))
    true
if S == 3 and showTags and not na(dHar) and not na(o820) and startOf(570)
    f_blk1()
// Indices: 8:30 open retest 9:30-11:30 by |9:30 open - 8:30 open| / daily HAR (<=.05 .1 .2 .35; farther: unstable,
// not shown). NQ 92-95/77-84/67-72/54-58, ES 90-97/77-86/61-68/49-58, YM 91-94/80-87/67-74/45-52% (2010-18/2019-22/2023-26).
var float o830 = na
if newDay
    o830 := na
if S != 3 and startOf(510) and nyT < 570
    o830 := open
f_blk2() =>
    float d83 = math.abs(open - o830) / dHar
    int p83 = d83 <= 0.05 ? (S == 1 ? 94 : 93) : d83 <= 0.1 ? (S == 0 ? 80 : S == 1 ? 82 : 83) : d83 <= 0.2 ? (S == 0 ? 69 : S == 1 ? 66 : 70) : d83 <= 0.35 ? (S == 0 ? 56 : S == 1 ? 53 : 49) : 0
    if p83 >= minOdds * 100
        int t1130d = time + tOff + tLen
        array.push(tLQ, line.new(time + tOff, o830, t1130d, o830, xloc = xloc.bar_time, color = color.new(tagCol, p83 >= 50 ? 15 : 50), style = f_ls(tagStyle), width = tagW))
        array.push(tLbQ, label.new(t1130d, o830, "8:30 open " + str.tostring(p83) + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Chance price trades back to the 8:30 open between 9:30 and 11:30, given its distance from the 9:30 open (Databento 1m; 2010-18, 2019-22 and 2023-26 within ~9 points)." + (S == 0 or S == 2 ? " NQ/YM: on the first touch after 9:35, price ran 0.1x the daily range through it before coming back 0.1x on 56-62% of days (every era; fake levels nearby: 3-10 pts less) - a level to trade through, not fade." : "")))
    true
if S != 3 and showTags and not na(dHar) and not na(o830) and startOf(570)
    f_blk2()
// 9:30 open retest from 10:00: P(price trades back to the 9:30 open 10:00-11:30) by |10:00 price - 9:30 open| / daily
// HAR, bins <=.05 .1 .2 .35 >.35. Databento 1m 2010-26, every era (2010-18 / 2019-22 / 2023-26) within ~0.06, n 55-673.
f_open10(int s, int b) =>
    int r = b == 0 ? (s == 3 ? 89 : 91) : b == 1 ? (s == 3 ? 73 : s == 2 ? 76 : 75) : b == 2 ? (s == 3 ? 51 : s == 1 ? 56 : 57) : b == 3 ? (s == 0 ? 34 : s == 1 ? 32 : s == 2 ? 31 : 29) : (s == 0 ? 14 : s == 1 ? 15 : s == 2 ? 17 : 16)
    r
f_blk3() =>
    float dd = math.abs(open - o930) / dHar
    int ob = dd <= 0.05 ? 0 : dd <= 0.1 ? 1 : dd <= 0.2 ? 2 : dd <= 0.35 ? 3 : 4
    int op = f_open10(S, ob)
    if op >= minOdds * 100
        int t1130o = time + tOff + tLen
        array.push(tLQ, line.new(time + tOff, o930, t1130o, o930, xloc = xloc.bar_time, color = color.new(tagCol, op >= 50 ? 15 : 50), style = f_ls(tagStyle), width = tagW))
        array.push(tLbQ, label.new(t1130o, o930, "9:30 open " + str.tostring(op) + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Chance price trades back to the 9:30 open between 10:00 and 11:30, given how far price is from it at 10:00 (Databento 1m 2010-26; 2010-18, 2019-22 and 2023-26 all within ~6 points)."))
    true
if showTags and not na(dHar) and not na(o930) and startOf(600) and nyT < 690
    f_blk3()
// 9:30-10:30 high/low still standing at 11:30, when the 10:30 price is > 0.15x daily HAR away from it, by when it was
// set (Databento 1m 2010-26, 2010-18 / 2019-22 / 2023-26, n 175-1501 per cell/era). Closer than 0.15x: 25-59%, not shown.
f_amHold(int s, int w) =>
    int r = w == 0 ? (s == 0 ? 87 : s == 1 ? 86 : s == 2 ? 88 : 89) : w == 1 ? (s == 0 ? 83 : s == 1 ? 83 : s == 2 ? 81 : 87) : (s == 0 ? 70 : s == 1 ? 72 : s == 2 ? 74 : 78)
    r
var float amH = na
var float amL = na
var int amHt = 0
var int amLt = 0
if intraday and timeframe.in_seconds() <= 900 and nyT >= 570 and nyT < 630
    if nyTprev < 570 or nyTprev > nyT or na(amH)
        amH := high
        amL := low
        amHt := nyT
        amLt := nyT
    else
        if high > amH
            amH := high
            amHt := nyT
        if low < amL
            amL := low
            amLt := nyT
if newDay
    amH := na
    amL := na
f_blk4() =>
    int t1130h = time + tOff + tLen
    for sd = 0 to 1
        float lvA = sd == 0 ? amH : amL
        int tA = sd == 0 ? amHt : amLt
        if math.abs(open - lvA) / dHar > 0.15
            int pA = f_amHold(S, tA < 585 ? 0 : tA < 600 ? 1 : 2)
            array.push(tLQ, line.new(time + tOff, lvA, t1130h, lvA, xloc = xloc.bar_time, color = color.new(invCol, 30), style = f_ls(invStyle), width = 1))
            array.push(tLbQ, label.new(t1130h, lvA, (sd == 0 ? "AM high holds " : "AM low holds ") + str.tostring(pA) + "%", xloc = xloc.bar_time, style = label.style_label_left, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "At 10:30 price is more than 0.15x the daily range away from the 9:30-10:30 " + (sd == 0 ? "high" : "low") + ". Since 2010 that level stood until 11:30 " + str.tostring(pA) + "% of the time (set 9:30-9:45: 85-90%, 9:45-10:00: 79-88%, 10:00-10:30: 69-82%; stable in 2010-18, 2019-22 and 2023-26). Closer than that it is a coin flip."))
    true
if showTags and not na(dHar) and not na(amH) and startOf(630)
    f_blk4()
if newDay
    tagTxt := ""
if intraday and not bestDone and not na(bestLv) and nyT > 570 and nyT < 690 and high >= bestLv and low <= bestLv
    bestDone := true
// labels with their own tested odds (not refreshed by the 9:45 / 10:30 distance curves)
isFixedLb(string tx) => str.startswith(tx, "Gap CE") or str.startswith(tx, "8:30 open") or str.startswith(tx, "8:20 open") or str.startswith(tx, "9:30 open") or str.startswith(tx, "AM high") or str.startswith(tx, "AM low")
// 10:30 refresh of level labels (highs/lows/gap) for levels untouched since 9:30: by 11:30 ~80/62/36/15/2% by distance
// (.05/.1/.2/.35/inf x daily range) for the index futures, gold ~78/55/32/10/1% (both eras)
var float amHi = na
var float amLo = na
if intraday and startOf(570)
    amHi := high
    amLo := low
else if intraday and nyT > 570 and nyT < 690
    amHi := math.max(amHi, high)
    amLo := math.min(amLo, low)
// ─────────────────────────────────────────────────────────── live next-move odds (9:45-11:25)
// Chance price still makes a NEW 9:30-11:30 high / low before 11:30, from minutes left, distance to the high / low,
// range so far, move since 9:30 and the midnight side. Logistic fits on Databento 1m 2010-18; out of sample 2019-22 and
// 2023-26 Brier 0.13 vs 0.23 for a flat guess, calibrated within ~3 pts per decile (research/live_next_extreme.py).
// coef order: intercept, log(dist+.01), sqrt(left/120), log(dist+.01)*sqrt(left/120), range, move, midnight side, log(range+.02)
f_nxCoef(int s, bool up) =>
    array<float> c = array.new<float>()
    if s == 0 and up
        c := array.from(-8.493, -2.552, 7.593, 1.065, -0.27, 0.394, -0.049, 0.665)
    else if s == 0
        c := array.from(-7.313, -2.593, 7.521, 1.31, -1.492, 1.135, 0.039, 1.158)
    else if s == 1 and up
        c := array.from(-8.089, -2.535, 8.136, 1.187, -1.276, 1.045, -0.126, 0.991)
    else if s == 1
        c := array.from(-6.421, -2.557, 7.738, 1.223, -2.326, 1.193, 0.052, 1.864)
    else if s == 2 and up
        c := array.from(-8.846, -2.641, 7.958, 1.178, -0.333, 0.424, -0.093, 0.672)
    else if s == 2
        c := array.from(-7.01, -2.644, 7.491, 1.278, -1.852, 0.988, 0.029, 1.448)
    else if s == 3 and up
        c := array.from(-8.579, -2.676, 7.61, 1.165, -0.486, 0.423, 0.023, 0.842)
    else if s == 3
        c := array.from(-9.627, -2.897, 8.41, 1.461, -0.095, 0.93, -0.003, 0.68)
    c
f_nxProb(array<float> c, float dist, float left, float rngN, float ret, float side) =>
    float ld = math.log(math.max(dist, 0) + 0.01)
    float sq = math.sqrt(math.max(left, 0) / 120.0)
    float z = array.get(c, 0) + array.get(c, 1) * ld + array.get(c, 2) * sq + array.get(c, 3) * ld * sq + array.get(c, 4) * rngN + array.get(c, 5) * ret + array.get(c, 6) * side + array.get(c, 7) * math.log(rngN + 0.02)
    1.0 / (1.0 + math.exp(-z))
// typical push past the old high / low when a new one comes (median x daily range by minutes left <30/30-60/60-90/>90)
f_nxExt(int s, bool up, float left) =>
    int b = left < 30 ? 0 : left < 60 ? 1 : left < 90 ? 2 : 3
    float r = 0.0
    if s == 0
        r := up ? array.get(array.from(0.043, 0.080, 0.119, 0.157), b) : array.get(array.from(0.066, 0.110, 0.146, 0.170), b)
    else if s == 1
        r := up ? array.get(array.from(0.045, 0.077, 0.114, 0.145), b) : array.get(array.from(0.060, 0.100, 0.136, 0.161), b)
    else if s == 2
        r := up ? array.get(array.from(0.046, 0.079, 0.114, 0.145), b) : array.get(array.from(0.059, 0.097, 0.131, 0.157), b)
    else
        r := up ? array.get(array.from(0.054, 0.081, 0.112, 0.140), b) : array.get(array.from(0.058, 0.094, 0.125, 0.147), b)
    r
var array<float> NXU = f_nxCoef(S, true)
var array<float> NXD = f_nxCoef(S, false)
float nxH = na
float nxL = na
float nxEH = na
float nxEL = na
int barMin = int(timeframe.in_seconds() / 60)
int chkT = nyT + barMin
if intraday and timeframe.in_seconds() <= 900 and not na(dHar) and not na(amHi) and not na(o930) and chkT >= 585 and chkT <= 685
    float rN = math.max(amHi - amLo, syminfo.mintick) / dHar
    float sdM = na(mnOpen) ? 0.0 : o930 > mnOpen ? 1.0 : -1.0
    float lft = 690 - chkT
    nxH := f_nxProb(NXU, (amHi - close) / dHar, lft, rN, (close - o930) / dHar, sdM)
    nxL := f_nxProb(NXD, (close - amLo) / dHar, lft, rN, (o930 - close) / dHar, -sdM)
    nxEH := f_nxExt(S, true, lft) * dHar
    nxEL := f_nxExt(S, false, lft) * dHar
// live odds that an untouched ONH/ONL/PDH/PDL/PWH/PWL (beyond the AM high/low so far) trades before 11:30; logistic per
// market & side fitted 2010-18, calibrated out of sample 2019-26 (research/live_level_odds.py). Order: intercept, log(dist+.01),
// sqrt(left/120), product, range, move, midnight side, log(range+.02), log(beyond the AM extreme+.01)
f_lvCoef(int s, bool up) =>
    array<float> c = array.new<float>()
    if s == 0 and up
        c := array.from(-11.327, -3.082, 9.0, 1.365, 0.515, 0.883, -0.058, 0.705, -0.304)
    else if s == 0
        c := array.from(-9.155, -3.203, 7.974, 1.103, -0.122, 0.234, 0.055, 1.322, -0.013)
    else if s == 1 and up
        c := array.from(-9.735, -2.799, 8.204, 0.723, -0.997, 1.05, 0.007, 1.461, -0.341)
    else if s == 1
        c := array.from(-9.615, -3.281, 8.653, 1.205, -0.249, 0.193, -0.011, 1.646, -0.218)
    else if s == 2 and up
        c := array.from(-10.342, -3.248, 8.23, 0.948, -0.237, 0.271, -0.054, 1.097, -0.085)
    else if s == 2
        c := array.from(-8.532, -3.208, 7.917, 1.277, -1.097, 0.847, 0.016, 1.614, -0.117)
    else if s == 3 and up
        c := array.from(-10.169, -3.56, 8.067, 1.468, -0.255, 0.132, 0.085, 0.934, -0.077)
    else if s == 3
        c := array.from(-9.86, -3.38, 7.298, 0.92, 0.634, 0.883, 0.152, 0.727, 0.211)
    c
f_lvProb(array<float> c, float dist, float beyond, float left, float rngN, float ret, float side) =>
    float ld = math.log(math.max(dist, 0) + 0.01)
    float sq = math.sqrt(math.max(left, 0) / 120.0)
    float z = array.get(c, 0) + array.get(c, 1) * ld + array.get(c, 2) * sq + array.get(c, 3) * ld * sq + array.get(c, 4) * rngN + array.get(c, 5) * ret + array.get(c, 6) * side + array.get(c, 7) * math.log(rngN + 0.02) + array.get(c, 8) * math.log(math.max(beyond, 0) + 0.01)
    1.0 / (1.0 + math.exp(-z))
var array<float> LVU = f_lvCoef(S, true)
var array<float> LVD = f_lvCoef(S, false)
f_liveLevels(float lft, float rN, float sdM) =>
    int n = array.size(tLbQ)
    if n > 0
        for i = 0 to n - 1
            label lb = array.get(tLbQ, i)
            string tx = label.get_text(lb)
            if label.get_x(lb) > time and (str.startswith(tx, "ONH") or str.startswith(tx, "ONL") or str.startswith(tx, "PD") or str.startswith(tx, "PW") or str.startswith(tx, "GAP "))
                float lv = label.get_y(lb)
                string nm = array.get(str.split(tx, " "), 0)
                if lv > amHi
                    float pu = f_lvProb(LVU, (lv - close) / dHar, (lv - amHi) / dHar, lft, rN, (close - o930) / dHar, sdM)
                    label.set_text(lb, nm + " " + str.tostring(math.round(pu * 100)) + "%")
                else if lv < amLo
                    float pd = f_lvProb(LVD, (close - lv) / dHar, (amLo - lv) / dHar, lft, rN, (o930 - close) / dHar, -sdM)
                    label.set_text(lb, nm + " " + str.tostring(math.round(pd * 100)) + "%")
    true
// ─────────────────────────────────────────────────────────── likely AM high / low zones (drawn at 9:30)
// Where the 9:30-11:30 high and low land from the 9:30 open (x daily HAR), middle 50% and median, by overnight-range tercile
// (Expect-row cuts). Bands fitted 2010-18 held 45-56% of 2019-26 days (target 50%). Layout per tercile: H25 H50 H75 L25 L50 L75.
f_amZone(int s) =>
    array<float> a = array.new<float>()
    if s == 0
        a := array.from(0.092, 0.188, 0.320, 0.097, 0.203, 0.364, 0.112, 0.235, 0.395, 0.108, 0.232, 0.426, 0.122, 0.271, 0.459, 0.115, 0.263, 0.491)
    else if s == 1
        a := array.from(0.084, 0.167, 0.289, 0.084, 0.174, 0.306, 0.099, 0.207, 0.358, 0.097, 0.221, 0.391, 0.115, 0.252, 0.415, 0.111, 0.249, 0.461)
    else if s == 2
        a := array.from(0.091, 0.187, 0.330, 0.086, 0.182, 0.322, 0.102, 0.209, 0.357, 0.099, 0.218, 0.407, 0.117, 0.259, 0.455, 0.115, 0.269, 0.458)
    else
        a := array.from(0.066, 0.150, 0.264, 0.074, 0.152, 0.283, 0.079, 0.172, 0.307, 0.090, 0.186, 0.332, 0.092, 0.208, 0.376, 0.093, 0.213, 0.397)
    a
var array<float> AMZ = f_amZone(S)
var array<box> amzQ = array.new<box>()
var array<line> amzL = array.new<line>()
var array<label> amzLb = array.new<label>()
f_drawAmZones() =>
    float zc1 = S == 0 ? 0.423 : S == 1 ? 0.454 : S == 2 ? 0.445 : 0.581
    float zc2 = S == 0 ? 0.612 : S == 1 ? 0.656 : S == 2 ? 0.646 : 0.848
    int zq = na(onUsed930) ? 1 : onUsed930 < zc1 ? 0 : onUsed930 > zc2 ? 2 : 1
    int zi = zq * 6
    int zEnd = time + 120 * 60000
    float hA = open + array.get(AMZ, zi) * dHar
    float hM = open + array.get(AMZ, zi + 1) * dHar
    float hB = open + array.get(AMZ, zi + 2) * dHar
    float lA = open - array.get(AMZ, zi + 3) * dHar
    float lM = open - array.get(AMZ, zi + 4) * dHar
    float lB = open - array.get(AMZ, zi + 5) * dHar
    // stretch: out to the AM session box's 90% edge (70-90% of mornings stayed inside), stacked on the likely box
    float scZ = dHar * (na(ctxMult) ? 1.0 : ctxMult)
    float sU = math.max(open + array.get(f_ss_am(S), 1) * scZ, hB)
    float sD = math.min(open - array.get(f_ss_am(S), 3) * scZ, lB)
    array.push(amzQ, box.new(time, hB, zEnd, hA, xloc = xloc.bar_time, border_color = color.new(amzCol, 35), border_width = 1, bgcolor = color.new(amzCol, 93)))
    array.push(amzQ, box.new(time, lA, zEnd, lB, xloc = xloc.bar_time, border_color = color.new(amzCol, 35), border_width = 1, bgcolor = color.new(amzCol, 93)))
    array.push(amzL, line.new(time, sU, zEnd, sU, xloc = xloc.bar_time, color = color.new(amzCol, 40), style = line.style_dotted))
    array.push(amzL, line.new(time, sD, zEnd, sD, xloc = xloc.bar_time, color = color.new(amzCol, 40), style = line.style_dotted))
    array.push(amzL, line.new(time, hM, zEnd, hM, xloc = xloc.bar_time, color = color.new(amzCol, 0), width = 2))
    array.push(amzL, line.new(time, lM, zEnd, lM, xloc = xloc.bar_time, color = color.new(amzCol, 0), width = 2))
    string zTip = "Where the 9:30-11:30 high / low usually lands from the 9:30 open: shaded = middle 50% of days, dotted = median, for mornings with this size of overnight range (Databento 2010-26; 2010-18 bands held 45-56% of 2019-26 days)."
    while array.size(amzQ) > sessDays * 2
        box.delete(array.shift(amzQ))
    while array.size(amzL) > sessDays * 4
        line.delete(array.shift(amzL))
    while array.size(amzLb) > sessDays * 4
        label.delete(array.shift(amzLb))
    true
if showTags and intraday and timeframe.in_seconds() <= 900 and startOf(570) and not na(dHar)
    f_drawAmZones()
var label nxHL = na
var label nxLL = na
var line nxPHL = na
var line nxPLL = na
// typical wait (minutes) until the new high / low comes, given it comes: by time left (<30/30-60/60-90/>90 min) and
// distance (<=0.05 / 0.05-0.15 / >0.15 x daily range); medians, every era within ~2 min (Databento 1m 2010-26)
f_nxWait(float left, float dist) =>
    int lb = left < 30 ? 0 : left < 60 ? 1 : left < 90 ? 2 : 3
    float r = dist <= 0.05 ? 2.0 : dist <= 0.15 ? array.get(array.from(8.0, 12.0, 13.0, 11.0), lb) : array.get(array.from(14.0, 25.0, 31.0, 28.0), lb)
    r
f_byTime(float mins) =>
    int tB = math.min(time + barMs + int(mins * 60000), timestamp("America/New_York", year(time, "America/New_York"), month(time, "America/New_York"), dayofmonth(time, "America/New_York"), 11, 30))
    " by " + str.format_time(tB, "H:mm", "America/New_York")
if barstate.islast and not na(nxH) and showTags
    f_liveLevels(690 - chkT, math.max(amHi - amLo, syminfo.mintick) / dHar, na(mnOpen) ? 0.0 : o930 > mnOpen ? 1.0 : -1.0)
if barstate.islast
    label.delete(nxHL)
    label.delete(nxLL)
    line.delete(nxPHL)
    line.delete(nxPLL)
    if not na(nxH)
        float lftN = 690 - chkT
        // push-target line: where the new high / low typically ends, shown when its odds are >= the setting
        if nxH >= pushMin
            nxPHL := line.new(time, amHi + nxEH, time + 12 * barMs, amHi + nxEH, xloc = xloc.bar_time, color = color.new(dnBase, 20), style = line.style_dashed, width = 2)
        if nxL >= pushMin
            nxPLL := line.new(time, amLo - nxEL, time + 12 * barMs, amLo - nxEL, xloc = xloc.bar_time, color = color.new(upBase, 20), style = line.style_dashed, width = 2)
        nxHL := label.new(time, amHi, "new high " + str.tostring(math.round(nxH * 100)) + "% (+" + fmtPts(nxEH) + ")" + f_byTime(f_nxWait(lftN, (amHi - close) / dHar)), xloc = xloc.bar_time, style = label.style_label_down, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Chance price trades above the 9:30-11:30 high so far before 11:30 (16 yrs, calibrated out of sample).")
        nxLL := label.new(time, amLo, "new low " + str.tostring(math.round(nxL * 100)) + "% (−" + fmtPts(nxEL) + ")" + f_byTime(f_nxWait(lftN, (close - amLo) / dHar)), xloc = xloc.bar_time, style = label.style_label_up, color = color.new(color.black, 100), textcolor = lblCol, size = lblBig ? size.normal : size.small, tooltip = "Chance price trades below the 9:30-11:30 low so far before 11:30 (16 yrs, calibrated out of sample).")
// 9:45 refresh of the target labels: odds re-measured from the 9:45 close for levels not yet tagged (distance only;
// the 9:30-9:45 candle direction made no difference). Databento 2010-26, eras within ~0.05.
// 9:45 odds also depend on the 9:30-9:45 size: a large opening candle makes the 0.2-0.5x targets ~10-20 pts likelier
// than a small one (every era, all markets; research/gen_tag945_size.py)
var array<float> TAG945SZ = f_tag945sz(S)
f_refresh945() =>
    for li3 = 0 to array.size(tLbQ) - 1
        label lb3 = array.get(tLbQ, li3)
        bool fx3 = not na(lb3) and isFixedLb(label.get_text(lb3))
        bool isVw = not na(lb3) and (str.startswith(label.get_text(lb3), "ON VWAP") or str.startswith(label.get_text(lb3), "VWAP y"))
        bool isOp = not na(lb3) and (str.startswith(label.get_text(lb3), "Midnight") or str.startswith(label.get_text(lb3), "Wk open") or str.startswith(label.get_text(lb3), "Mo open"))
        // open-type levels from the 9:45 close (midnight / week / month open, untouched by 9:45), both eras
        if isOp and label.get_x(lb3) > time
            float lvo = label.get_y(lb3)
            string nmo = str.startswith(label.get_text(lb3), "Midnight") ? "Midnight" : str.startswith(label.get_text(lb3), "Wk open") ? "Wk open" : "Mo open"
            int bo = vwBin(math.abs(lvo - close[1]) / dHar)
            array<float> OP9 = array.from(0.87, 0.67, 0.50, 0.27, 0.04)
            if S == 0
                OP9 := array.from(0.92, 0.82, 0.62, 0.38, 0.06)
            else if S == 1
                OP9 := array.from(0.92, 0.77, 0.57, 0.34, 0.05)
            else if S == 2
                OP9 := array.from(0.89, 0.78, 0.54, 0.34, 0.05)
            bool dno = or15H >= lvo and or15L <= lvo
            label.set_text(lb3, nmo + (dno ? " hit" : " " + str.tostring(math.round(array.get(OP9, bo) * 100)) + "%"))
        if isVw and label.get_x(lb3) > time
            float lvv = label.get_y(lb3)
            bool onv = str.startswith(label.get_text(lb3), "ON VWAP")
            bool dv = or15H >= lvv and or15L <= lvv
            int bv = vwBin(math.abs(lvv - close[1]) / dHar)
            float pv9 = onv ? array.get(f_vw945On(S), bv) : array.get(f_vw945Rth(S), bv)
            label.set_text(lb3, (onv ? "ON VWAP" : "VWAP y") + (dv ? " hit" : " " + str.tostring(math.round(pv9 * 100)) + "%"))
        if not na(lb3) and not fx3 and not isVw and not isOp and label.get_x(lb3) > time
            float lv3 = label.get_y(lb3)
            bool done = lv3 >= close[1] ? or15H >= lv3 : or15L <= lv3
            float d3 = math.abs(lv3 - close[1]) / dHar
            int b3 = d3 <= 0.1 ? 0 : d3 <= 0.2 ? 1 : d3 <= 0.35 ? 2 : d3 <= 0.5 ? 3 : 4
            // keep only the level names, then the refreshed odds (cleaner than stacking the 9:30 numbers)
            array<string> parts = str.split(label.get_text(lb3), " · ")
            string names = ""
            for pt in parts
                string w0 = array.get(str.split(pt, " "), 0)
                if w0 == "ONH" or w0 == "ONL" or w0 == "PDH" or w0 == "PDL" or w0 == "PWH" or w0 == "PWL" or w0 == "GAP" or w0 == "Wk" or w0 == "Mo"
                    names := names + (names == "" ? "" : "/") + (w0 == "Wk" ? "Wk " + array.get(str.split(pt, " "), 1) : w0 == "Mo" ? "Mo open" : w0)
            array<float> cut3 = f_or15_cut(S)
            float w3 = (or15H - or15L) / dHar
            int z3 = w3 < array.get(cut3, 0) ? 0 : w3 > array.get(cut3, 1) ? 2 : 1
            label.set_text(lb3, names + (done ? " hit" : " " + str.tostring(math.round(array.get(TAG945SZ, z3 * 5 + b3) * 100)) + "%"))
    true
if showTags and intraday and nyT >= 585 and nyTprev < 585 and not na(dHar) and not na(or15H) and array.size(tLbQ) > 0
    f_refresh945()


// ─────────────────────────────────────────────────────────── live layer
int hrIdx = (nyHour - 18 + 24) % 24

// ─────────────────────────────────────────────────────────── next break + likely close area (live)

// ─────────────────────────────────────────────────────────── week so far (Tue-Fri)

// ─────────────────────────────────────────────────────────── week / month / 4H lines

int myBlk = int(math.floor(hrIdx / 4))
bool newBlk = intraday and timeframe.in_seconds() <= 3600 and (myBlk != myBlk[1] or newDay)
var array<line> h4Q = array.new<line>()
f_blk7() =>
    int bEnd = na(dayEnd) ? time + 4 * 3600000 : math.min(time + 4 * 3600000, dayEnd)
    array.push(h4Q, line.new(time, open + array.get(HQU, 1) * hScale, bEnd, open + array.get(HQU, 1) * hScale, xloc = xloc.bar_time, color = color.new(hCol, 20), style = f_ls(h4Style), width = h4W))
    array.push(h4Q, line.new(time, open - array.get(HQD, 1) * hScale, bEnd, open - array.get(HQD, 1) * hScale, xloc = xloc.bar_time, color = color.new(hCol, 20), style = f_ls(h4Style), width = h4W))
    while array.size(h4Q) > 12
        line.delete(array.shift(h4Q))
    true
if show4H and newBlk and not na(hScale)
    f_blk7()

// ─────────────────────────────────────────────────────────── session levels (context only)
var float asiaH = na
var float asiaL = na
var float lonH = na
var float lonL = na
bool inAsia = hrIdx < 6
bool inLon = (nyHour == 1 and nyMin >= 30) or (nyHour >= 2 and nyHour < 5)
bool inON = hrIdx < 15 or (nyHour == 9 and nyMin < 30)
if newDay
    pdH := tdH
    pdL := tdL
    tdH := high
    tdL := low
    asiaH := na
    asiaL := na
    lonH := na
    lonL := na
    onH := na
    onL := na
else
    tdH := math.max(tdH, high)
    tdL := math.min(tdL, low)
if inAsia
    asiaH := na(asiaH) ? high : math.max(asiaH, high)
    asiaL := na(asiaL) ? low : math.min(asiaL, low)
if inLon
    lonH := na(lonH) ? high : math.max(lonH, high)
    lonL := na(lonL) ? low : math.min(lonL, low)
if inON
    onH := na(onH) ? high : math.max(onH, high)
    onL := na(onL) ? low : math.min(onL, low)
// gold only: first 1h bar through the Asia high / low after 00:00 (same event definition as the research)
var float  hrH = na
var float  hrL = na
var bool   asiaHiDone = false
var bool   asiaLoDone = false
var string gSweep = ""
bool newHour = intraday and timeframe.in_seconds() <= 3600 and timeframe.change("60")
if newDay
    asiaHiDone := false
    asiaLoDone := false
    gSweep := ""
if S == 3 and newHour and not newDay and not na(hrH) and hrIdx > 6 and not na(asiaH)
    // the hour that just completed: high/low tracked below, close = previous bar's close
    if not asiaHiDone and hrH > asiaH
        asiaHiDone := true
        if close[1] <= asiaH and gSweep == ""
            gSweep := "▲ high swept, closed inside"
    if not asiaLoDone and hrL < asiaL
        asiaLoDone := true
        if close[1] >= asiaL and gSweep == ""
            gSweep := "▼ low swept, closed inside"
if newHour or newDay
    hrH := high
    hrL := low
else
    hrH := math.max(hrH, high)
    hrL := math.min(hrL, low)

bool sx = showSess and intraday
plot(sx ? pdH : na, "Prior day high", color.new(#8b949e, 35), 1, plot.style_linebr)
plot(sx ? pdL : na, "Prior day low", color.new(#8b949e, 35), 1, plot.style_linebr)
plot(sx and not inAsia ? asiaH : na, "Asia high", color.new(#e05252, 55), 1, plot.style_linebr)
plot(sx and not inAsia ? asiaL : na, "Asia low", color.new(#e05252, 55), 1, plot.style_linebr)
plot(sx and not inLon and nyHour >= 5 ? lonH : na, "London high", color.new(#4a7fe0, 55), 1, plot.style_linebr)
plot(sx and not inLon and nyHour >= 5 ? lonL : na, "London low", color.new(#4a7fe0, 55), 1, plot.style_linebr)
plot(sx and not inON ? onH : na, "Overnight high", color.new(#e09a3a, 55), 1, plot.style_linebr)
plot(sx and not inON ? onL : na, "Overnight low", color.new(#e09a3a, 55), 1, plot.style_linebr)

// ─────────────────────────────────────────────────────────── table (only what matters)
var table tb = table.new(tblPos == "Top right" ? position.top_right : tblPos == "Bottom right" ? position.bottom_right : tblPos == "Top left" ? position.top_left : position.bottom_left, 2, 21, bgcolor = color.new(#0d1117, 8), frame_color = color.new(#30363d, 0), frame_width = 1, border_width = 0)
cellT(int r, string a, string b, color c, string tip) =>
    table.cell(tb, 0, r, a, text_color = txtDim, text_size = size.normal, text_halign = text.align_left, tooltip = tip)
    table.cell(tb, 1, r, b, text_color = c, text_size = size.normal, text_halign = text.align_right, tooltip = tip)

// table drawing wrapped in a function: keeps the script's main body under Pine's size limit (CE10295)
f_drawTable() =>
    table.clear(tb, 0, 0, 1, 20)
    if not intraday
        cellT(0, "HTF Zones", "use an intraday chart", txtMain, "")
    else if na(sigma)
        cellT(0, symName + " day", "waits for 18:00 open", txtMain, "Zones start at the next futures-day open.")
    else
        string tone = ctxMult < 0.93 ? "quieter" : ctxMult > 1.07 ? "bigger" : "normal"
        color toneC = ctxMult < 0.93 ? color.new(#7aa2c8, 0) : ctxMult > 1.07 ? color.new(#e3b341, 0) : txtMain
        int r = 0
        string bTxt = bias == 1 ? "LONG" : bias == -1 ? "SHORT" : "NEUTRAL"
        color bC = bias == 1 ? color.new(dnBase, 0) : bias == -1 ? color.new(upBase, 0) : txtMain
        string tgt = bias == 1 ? fmtPx(uIn) + " – " + fmtPx(uOut) : bias == -1 ? fmtPx(lIn) + " – " + fmtPx(lOut) : "both zones"
        cellT(r, symName + (supported ? "" : "?") + " " + tone, fmtPts(sigma) + " pts · " + bTxt + (bias != 0 ? " " + biasPct : ""), bias != 0 ? bC : toneC, "Why this day size:\n" + ctxText + "\n\nBias (locked at 18:00): " + biasWhy + " rule, right on day direction " + biasPct + " historically (both eras). Default is the trend filter (50d vs 200d avg, open vs 20d avg); tested overrides: NQ Mondays long (60-62%), YM after a quiet day follow the 20-day trend (57%). A lean, not a signal. Bias-side zone: " + tgt)
        r += 1
        if not na(nxH)
            cellT(r, "Next", "new high " + str.tostring(math.round(nxH * 100)) + "% (+" + fmtPts(nxEH) + ") · new low " + str.tostring(math.round(nxL * 100)) + "% (−" + fmtPts(nxEL) + ")", nxH - nxL >= 0.15 ? color.new(dnBase, 0) : nxL - nxH >= 0.15 ? color.new(upBase, 0) : txtMain, "Live: chance price still makes a new 9:30-11:30 high / low before 11:30, from time left, distance to the high / low, the range so far, the move since 9:30 and the midnight side. Fitted 2010-18; tested 2019-22 and 2023-26: calibrated within ~3 points per decile, Brier 0.13 vs 0.23 for a flat guess, all four markets.")
            r += 1
        if expTxt != ""
            cellT(r, "Expect", expTxt, txtMain, expTip != "" ? expTip : "Typical (median) 9:30-10:00 range for mornings like this one, from the overnight range size and news, then the typical full 9:30-11:30 range (Databento 2010-26, stable in both eras).")
            r += 1
        // Plan (compact): setup + 9:45 / 10:00 target-and-stop (or the OR break state once no plan is live) + best target
        string setS = setupTxt == "" ? "" : str.contains(setupTxt, "edge gone") ? "setup: edge gone" : str.substring(setupTxt, 0, 1) + " setup " + str.match(setupTxt, "[0-9]+-?[0-9]*%")
        // best live target: the highest-odds target label still on the chart (hit ones are removed), else the 9:30 pick
        string tgtS = ""
        float tgtP = -1.0
        int nLb = array.size(tLbQ)
        if nLb > 0 and nyT >= 570 and nyT < 690
            for liT = 0 to nLb - 1
                label lbT = array.get(tLbQ, liT)
                string tx = label.get_text(lbT)
                if label.get_x(lbT) > time and not str.startswith(tx, "AM ") and not str.contains(tx, "hit")
                    array<string> tk = str.split(tx, " ")
                    string nmT = ""
                    float pT = na
                    for w in tk
                        if str.endswith(w, "%") and na(pT)
                            pT := str.tonumber(str.replace(w, "%", ""))
                        else if na(pT)
                            nmT := nmT + (nmT == "" ? "" : " ") + w
                    if not na(pT) and pT > tgtP
                        tgtP := pT
                        tgtS := nmT + " " + fmtPx(label.get_y(lbT)) + " " + str.tostring(pT) + "%"
        if tgtS == "" and tagTxt != ""
            tgtS := str.replace(tagTxt, " · ", " " + fmtPx(bestLv) + " ", 0) + (bestDone ? " ✓" : "")
        string planB = holdDir != 0 ? holdTxt : invTxt != "" ? invTxt : holdTxt
        string planTxt = setS
        if planB != ""
            planTxt := planTxt + (planTxt != "" ? " · " : "") + planB
        if tgtS != ""
            planTxt := planTxt + (planTxt != "" ? " · " : "") + "target " + tgtS
        if not tblFull and planTxt != ""
            cellT(r, nyT < 585 ? "Plan" : nyT >= 600 ? "Plan 10:00" : "Plan 9:45", planTxt, setupTxt != "" ? setupC : txtMain, holdTip != "" ? holdTip : "Best tested target and stop for the morning. Full table mode shows each part on its own row.")
            r += 1
        if tblFull and (or15Txt != "" or or30Txt != "")
            cellT(r, "Opening range", (or15Txt != "" ? "15m " + or15Txt : "") + (or30Txt != "" ? (or15Txt != "" ? "  |  " : "") + "30m " + or30Txt : ""), color.new(orCol, 0), or15Tip + (or30Tip != "" ? "\n\n" + or30Tip : ""))
            r += 1
        if tblFull and holdTxt != ""
            cellT(r, nyT >= 600 ? "10:00 plan" : "9:45 plan", holdTxt, holdDir == 0 ? txtDim : txtMain, holdTip)
            r += 1
        if tblFull and setupTxt != ""
            cellT(r, "Setup", setupTxt, setupC, "Audited as displayed (one setup per day): NQ A 60-63%, NQ B 56-57%, ES B 57-63%, gold 56-58%, 9:45 -> 11:30, both 2010-18 and 2019-26, 100+ cases each. A = opened above yesterday's high + green 9:30-9:45; B = heavy 9:30-9:45 volume + overnight 4H majority agree with the opening candle. If price trades back through the 9:30 open before 10:30, the setup direction wins only 50-56% from there (2010-26, NQ/ES/YM): the edge is used up, but it is not a reversal signal.")
            r += 1
        if tblFull and volTxt != ""
            cellT(r, "Open volume", volTxt, color.new(#e3b341, 0), "9:30-9:45 volume versus its 20-day average (>1.25x = heavy). Feeds setup B. On YM the opening candle direction held into 11:30 56.3% in both eras.")
            r += 1
        if tblFull and invTxt != ""
            cellT(r, "OR break", invTxt, txtMain, "Invalidation from 16 years of 1-minute data: once price comes back 0.5 OR width inside after the first break, the break rarely works.")
            r += 1
        if tblFull and tagTxt != ""
            cellT(r, "Target", tagTxt, color.new(tagCol, 0), "The overnight / prior-day high or low most likely to be tagged by 11:30, given its distance from the 9:30 open.")
            r += 1
        if zoneTxt != ""
            cellT(r, "Zone", zoneTxt, txtMain, "9:30 opened beyond a day-zone line. Since 2010 (both 2010-18 and 2019-26, all four markets) the next line out was usually reached by 11:30 and a full return to the 18:00 open was rare, so fading back to the open is a low-odds idea on these mornings.")
            r += 1
        if profTxt != ""
            cellT(r, "Profile", profTxt, txtMain, "Daily profile from the midnight (0:00) open. Since 2010, when 9:30 opened above it, the 9:30-11:30 window set the futures-day low on only 6-16% of days (NQ 13-16, ES 9-11, YM 9-15, gold 6-8%) and the high on 24-36%; below it, the day high only 5-14% and the low 26-36%. Stable in 2010-18, 2019-22 and 2023-26. Week: when 9:30 is above the Sunday 18:00 week open, this morning set the week low on 0-6% of days; below it, the week high on 0-5%.")
            r += 1
        if sessTxt != ""
            cellT(r, "Session", sessTxt, sessC, "Bias of the current session box. Rule and hit rate come from this chart's own history.")
            r += 1
        if newsTxt != ""
            cellT(r, "News", newsTxt, newsIsToday ? color.new(#e3b341, 0) : txtMain, "Scheduled FOMC (2:00 ET), CPI and NFP (8:30 ET). Update the calendar yearly.")
            r += 1
        string monTxt = monDir == 1 ? "Mon up: Mon low " + fmtPx(monL) + " = week low 41-45%" : monDir == -1 ? "Mon down: Mon high " + fmtPx(monH) + " = week high 32-42%" : ""
        if monTxt != ""
            cellT(r, "Week", monTxt, txtMain, "26 years of daily data, all four markets, both halves: when Monday closes up, Monday's low ends up being the week's low 41-45% of the time (about 20% by chance) and its high is the week's high only 9-17%. When Monday closes down, Monday's high is the week's high 32-42% of the time. Rechecked on Databento 2010-26, all four markets, both eras.")
            r += 1
        if S == 3 and nyHour >= 13 and nyHour < 17
            cellT(r, "Gold", "day mostly done", txtDim, "After 13:00 NY the gold day high was already in on 82-84% of days and the low on 84-85% (Databento 2010-26, both eras; COMEX settles 13:30).")
    true
if showTable and barstate.islast
    f_drawTable()

// ─────────────────────────────────────────────────────────── clean-up: drop levels once they no longer matter
f_dropAll(array<line> ls, array<label> lbs) =>
    while array.size(ls) > 0
        line.delete(array.pop(ls))
    while array.size(lbs) > 0
        label.delete(array.pop(lbs))
    true
// a level is gone once price trades through it after it was drawn: delete its line and the label at its price
f_dropHit(array<line> ls, array<label> lbs, int off) =>
    int n = array.size(ls)
    if n > 0
        for i = n - 1 to 0
            line l = array.get(ls, i)
            float y = line.get_y1(l)
            int x2 = line.get_x2(l)
            if line.get_x1(l) - off < time and x2 > time and high >= y and low <= y
                int m = array.size(lbs)
                if m > 0
                    for j = m - 1 to 0
                        label b = array.get(lbs, j)
                        if label.get_x(b) == x2 and math.abs(label.get_y(b) - y) < syminfo.mintick
                            label.delete(b)
                            array.remove(lbs, j)
                line.delete(l)
                array.remove(ls, i)
    true
if intraday
    // setup / signal that hit its stop or its time stop: the whole plan no longer matters
    if pbHit or setStall
        f_dropAll(stQ, stLQ)
    if clearHit and nyT >= 570 and nyT < 690
        f_dropHit(tLQ, tLbQ, tOff)
        f_dropHit(iLQ, iLbQ, 0)
        f_dropHit(stQ, stLQ, 0)
        f_dropHit(oLQ, oLbQ, 0)
    if clear1130 and startOf(690)
        f_dropAll(tLQ, tLbQ)
        f_dropAll(iLQ, iLbQ)
        f_dropAll(stQ, stLQ)
        f_dropAll(oLQ, oLbQ)
        while array.size(oQ) > 0
            box.delete(array.pop(oQ))
        invTxt := ""
        setupTxt := ""
        tagTxt := ""

// ─────────────────────────────────────────────────────────── de-clutter: labels at ~the same time and price
// stack instead of overlapping (the higher one's text goes just above its price, the lower one's just below)
f_collect(array<label> q, array<label> all) =>
    int nq = array.size(q)
    if nq > 0
        for i = 0 to nq - 1
            label lb = array.get(q, i)
            if not na(lb) and label.get_x(lb) > time and label.get_text(lb) != ""
                array.push(all, lb)
    true
f_declutter() =>
    array<label> all = array.new<label>()
    f_collect(tLbQ, all)
    f_collect(iLbQ, all)
    f_collect(amzLb, all)
    f_collect(oLbQ, all)
    f_collect(stLQ, all)
    int n = array.size(all)
    if n > 1 and not na(dHar)
        for i = 0 to n - 2
            for j = i + 1 to n - 1
                label a = array.get(all, i)
                label b = array.get(all, j)
                if math.abs(label.get_x(a) - label.get_x(b)) <= 3 * barMs and math.abs(label.get_y(a) - label.get_y(b)) < 0.03 * dHar
                    bool aHi = label.get_y(a) >= label.get_y(b)
                    label.set_style(aHi ? a : b, label.style_label_lower_left)
                    label.set_style(aHi ? b : a, label.style_label_upper_left)
    true
if barstate.islast
    f_declutter()

// minimal mode: keep only the two highest-odds live target labels/lines; hide invalid / far-side / setup drawings
f_hideL(array<line> ls, array<label> lbs) =>
    if array.size(ls) > 0
        for i = 0 to array.size(ls) - 1
            line.set_color(array.get(ls, i), color.new(color.black, 100))
    if array.size(lbs) > 0
        for i = 0 to array.size(lbs) - 1
            label.set_textcolor(array.get(lbs, i), color.new(color.black, 100))
    true
f_oddsOf(string tx) =>
    float p = na
    for w in str.split(tx, " ")
        if na(p) and str.endswith(w, "%")
            p := str.tonumber(str.replace(w, "%", ""))
    p
f_minimal() =>
    f_hideL(iLQ, iLbQ)
    f_hideL(stQ, stLQ)
    int n = array.size(tLbQ)
    if n > 0
        array<float> ps = array.new<float>()
        for i = 0 to n - 1
            label lb = array.get(tLbQ, i)
            float p = label.get_x(lb) > time ? f_oddsOf(label.get_text(lb)) : na
            array.push(ps, na(p) ? -1.0 : p)
        array<float> srt = array.copy(ps)
        array.sort(srt, order.descending)
        float cut = array.size(srt) >= 2 ? array.get(srt, 1) : array.get(srt, 0)
        for i = 0 to n - 1
            label lb = array.get(tLbQ, i)
            bool keep = array.get(ps, i) >= cut and array.get(ps, i) >= 0
            label.set_textcolor(lb, keep ? lblCol : color.new(color.black, 100))
            int nl = array.size(tLQ)
            if nl > 0
                for j = 0 to nl - 1
                    line l = array.get(tLQ, j)
                    if line.get_x2(l) == label.get_x(lb) and math.abs(line.get_y1(l) - label.get_y(lb)) < syminfo.mintick
                        line.set_color(l, keep ? color.new(tagCol, 15) : color.new(color.black, 100))
    true
if barstate.islast and minimal
    f_minimal()


// ─────────────────────────────────────────────────────────── Asia stat boxes (drawn at the Asia open)
// Where the Asia high / low land from the session open (x daily HAR): darker = middle 50% of sessions, lighter = out to
// the 90% edge; Monday (Sunday 18:00 open) runs bigger. research/asia_zones.py: 2010-18 bands covered 44-61% of 2019-26
// sessions (target 50), the 90% edge held 82-95%.
var array<box> asQ = array.new<box>()
var array<line> asL = array.new<line>()
var array<label> asLb = array.new<label>()
f_asiaBoxes(int aStart) =>
    bool mon = dayofweek(time_tradingday, "UTC") == dayofweek.monday
    int k = (aStart == 1080 ? 0 : 8) + (mon ? 4 : 0)
    array<float> z = f_asiaZ(S)
    int aEnd = time + (1440 - aStart) * 60000
    float o = open
    for sd = 0 to 1
        float sg = sd == 0 ? 1.0 : -1.0
        float q25 = o + sg * array.get(z, k) * dHar
        float q50 = o + sg * array.get(z, k + 1) * dHar
        float q75 = o + sg * array.get(z, k + 2) * dHar
        float q90 = o + sg * array.get(z, k + 3) * dHar
        array.push(asQ, box.new(time, math.max(q25, q75), aEnd, math.min(q25, q75), xloc = xloc.bar_time, border_color = color.new(asiaCol, 35), border_width = 1, bgcolor = color.new(asiaCol, 93)))
        array.push(asL, line.new(time, q90, aEnd, q90, xloc = xloc.bar_time, color = color.new(asiaCol, 40), style = line.style_dotted))
        array.push(asL, line.new(time, q50, aEnd, q50, xloc = xloc.bar_time, color = color.new(asiaCol, 0), width = 2))
    while array.size(asQ) > sessDays * 2
        box.delete(array.shift(asQ))
    while array.size(asL) > sessDays * 4
        line.delete(array.shift(asL))
    while array.size(asLb) > daysBack * 2
        label.delete(array.shift(asLb))
    true
// ─────────────────────────────────────────────────────────── 17:00 close: clear the day's session / OR / AM-zone drawings
f_clearDay() =>
    for b in sQ
        box.delete(b)
    for b in oQ
        box.delete(b)
    for b in amzQ
        box.delete(b)
    for b in asQ
        box.delete(b)
    for l in oLQ
        line.delete(l)
    for l in amzL
        line.delete(l)
    for l in asL
        line.delete(l)
    for l in sL
        label.delete(l)
    for l in oLbQ
        label.delete(l)
    array.clear(sQ)
    array.clear(oQ)
    array.clear(amzQ)
    array.clear(asQ)
    array.clear(oLQ)
    array.clear(amzL)
    array.clear(asL)
    array.clear(sL)
    array.clear(oLbQ)
    true
if startOf(1020)
    f_clearDay()

int asStart = asiaStat == "18:00-0:00" ? 1080 : 1200
if asiaStat != "Off" and showTags and intraday and timeframe.in_seconds() <= 3600 and not na(dHar) and startOf(asStart)
    f_asiaBoxes(asStart)

Overview

Upper and lower day zones use selectable inner and outer percentiles, with optional price labels and an open line. AM, midday, and PM session boxes and 15- and 30-minute opening ranges can be shown or hidden. Minimal mode hides most overlays and leaves morning zones plus two targets. Levels stay fixed after their session times print. The code is marked as using lookahead.

Details

Published by

samuelharting on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Support & resistance, Sessions & time

Markets

Indices, Gold

Timeframes

M15-H1

Risk Notes

  • Reads higher-timeframe data with lookahead on, so the history can show signals that were not there in real time

Settings

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

SettingTypeDefaultVariable
Minimal mode (AM zones, next-move odds, top 2 targets)boolfalseminimal
Show day zonesbooltrueshowDaily
Zone fromstring70innerPct
to percentilestring90outerPct
Price labelsbooltrueshowDLbl
18:00 open linebooltrueshowOpen
Days to keepint2daysBack
Fit zones in viewstringInner edgesfitMode
Uppercolor#e05252upBase
Lowercolor#2aa889dnBase
Zone edge widthint1zoneW
18:00 open linecolor#8b949e73openCol
Show all 40 settings
SettingTypeDefaultVariable
openStylestringDottedopenStyle
AM 9:30-11:30booltrueshowW1
Midday 11:30-13:30booltrueshowW2
PM 13:30-16:00booltrueshowW3
Box colorcolor#7a8ca8sessCol
Days of session / OR boxes to keepint1sessDays
OR 9:30-9:45booltrueshowOR15
OR 9:30-10:00booltrueshowOR30
OR colorcolor#4fa3e0orCol
Likely targets by 11:30 (overnight / prior-day highs & lows)booltrueshowTags
Target colorcolor#b07cd8tagCol
Target lines: start bars to the rightint55tgtOffB
length (bars)int8tgtLenB
Likely AM high / low zonescolor#7a8ca8amzCol
Target linestringDashedtagStyle
widthint1tagW
Opening-range line widthint1orW
Stop / invalidationcolor#9aa0a6invCol
invStylestringDottedinvStyle
Failed-break far sidecolor#e8a33dfarCol
Remove a level once price trades through itbooltrueclearHit
Min target odds %int40minOdds
Clear morning levels after 11:30booltrueclear1130
TablebooltrueshowTable
Table detailstringCompacttblFull
Chart label textcolor#000000lblCol
Show push-target line when new high / low odds ≥ %int60pushMin
biggerbooltruelblBig
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