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.
//@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.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Minimal mode (AM zones, next-move odds, top 2 targets)
bool
false
minimal
Show day zones
bool
true
showDaily
Zone from
string
70
innerPct
to percentile
string
90
outerPct
Price labels
bool
true
showDLbl
18:00 open line
bool
true
showOpen
Days to keep
int
2
daysBack
Fit zones in view
string
Inner edges
fitMode
Upper
color
#e05252
upBase
Lower
color
#2aa889
dnBase
Zone edge width
int
1
zoneW
18:00 open line
color
#8b949e73
openCol
Show all 40 settings
Setting
Type
Default
Variable
openStyle
string
Dotted
openStyle
AM 9:30-11:30
bool
true
showW1
Midday 11:30-13:30
bool
true
showW2
PM 13:30-16:00
bool
true
showW3
Box color
color
#7a8ca8
sessCol
Days of session / OR boxes to keep
int
1
sessDays
OR 9:30-9:45
bool
true
showOR15
OR 9:30-10:00
bool
true
showOR30
OR color
color
#4fa3e0
orCol
Likely targets by 11:30 (overnight / prior-day highs & lows)
bool
true
showTags
Target color
color
#b07cd8
tagCol
Target lines: start bars to the right
int
55
tgtOffB
length (bars)
int
8
tgtLenB
Likely AM high / low zones
color
#7a8ca8
amzCol
Target line
string
Dashed
tagStyle
width
int
1
tagW
Opening-range line width
int
1
orW
Stop / invalidation
color
#9aa0a6
invCol
invStyle
string
Dotted
invStyle
Failed-break far side
color
#e8a33d
farCol
Remove a level once price trades through it
bool
true
clearHit
Min target odds %
int
40
minOdds
Clear morning levels after 11:30
bool
true
clear1130
Table
bool
true
showTable
Table detail
string
Compact
tblFull
Chart label text
color
#000000
lblCol
Show push-target line when new high / low odds ≥ %
int
60
pushMin
bigger
bool
true
lblBig
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.
This 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-
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
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
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
Indicatorv6SMC / ICT / price actionSessions & time
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
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