// Does the desk's exit geometry survive its own session windows? // Replays production mechanics (lib/oppTapeGrade.ts) on /fp-data 1m bars: // entry = first 5m CLOSE through the trigger, skipped if >0.5x planned risk beyond it ("no chase") // fill = next 1m open; risk = |fill - stop|; brackets re-projected FROM THE FILL // exit = stop or target, stop wins ties; anything open at the session bell is marked there // Signal proxy for "a level the desk would publish": break of the session's opening 30m range, // stop at the opposite side of that range (the desk's structural "invalidation reclaims X"). import fs from "node:fs"; const DATA = "/fp-data/glbx"; const SYMS = ["CL", "NQ", "ES", "GC", "ZN", "RTY", "YM", "6E"]; const SESSIONS = { ny: ["09:22", "16:00", false], london: ["02:45", "09:30", false], asia: ["18:40", "03:00", true] }; const OR_MIN = 30, CHASE_MAX = 0.5, YEAR = "2026"; const hhmm = (s) => { const [h, m] = s.split(":").map(Number); return h * 60 + m; }; const etFmt = new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }); const offsetCache = new Map(); function etOf(ms) { // ET date + minute-of-day, offset cached per UTC day const day = Math.floor(ms / 86400_000); let off = offsetCache.get(day); if (off === undefined) { const p = Object.fromEntries(etFmt.formatToParts(ms).map((x) => [x.type, x.value])); const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour % 24, +p.minute); off = Math.round((asUTC - Math.floor(ms / 60000) * 60000) / 60000); offsetCache.set(day, off); } const t = ms + off * 60000, d = new Date(t); return { date: d.toISOString().slice(0, 10), min: d.getUTCHours() * 60 + d.getUTCMinutes() }; } function loadBars(sym) { const f = `${DATA}/${sym}/1m/${YEAR}.csv`; if (!fs.existsSync(f)) return []; const out = []; const lines = fs.readFileSync(f, "utf8").split("\n"); for (let i = 1; i < lines.length; i++) { const L = lines[i]; if (!L) continue; const c = L.split(","); const t = Date.parse(c[0].slice(0, 19) + "Z"); const o = +c[4], h = +c[5], l = +c[6], cl = +c[7]; if (!Number.isFinite(t) || !Number.isFinite(o)) continue; out.push({ t, o, h, l, c: cl }); } return out; } const to5m = (bars) => { const out = []; let cur = null; for (const b of bars) { const k = Math.floor(b.t / 300_000) * 300_000; if (!cur || cur.t !== k) { if (cur) out.push(cur); cur = { t: k, o: b.o, h: b.h, l: b.l, c: b.c }; } else { cur.h = Math.max(cur.h, b.h); cur.l = Math.min(cur.l, b.l); cur.c = b.c; } } if (cur) out.push(cur); return out; }; // One attempt = one direction on one session. Returns null when the range is unusable. function attempt(bars, long, orHi, orLo) { const trigger = long ? orHi : orLo, stop = long ? orLo : orHi; const planned = Math.abs(trigger - stop); if (!(planned > 0)) return null; const m5 = to5m(bars); let confirmIdx = -1; for (let i = 0; i < m5.length; i++) { const over = long ? m5[i].c - trigger : trigger - m5[i].c; if (!(over > 0)) continue; if (over > CHASE_MAX * planned) continue; // no chase confirmIdx = i; break; } if (confirmIdx < 0) return { triggered: false }; const closeMs = m5[confirmIdx].t + 300_000; const fillBar = bars.find((b) => b.t >= closeMs); if (!fillBar) return { triggered: false }; const fill = fillBar.o, risk = Math.abs(fill - stop); if (!(risk > 0)) return null; const walk = bars.filter((b) => b.t >= fillBar.t); let mfe = 0, mae = 0, hitStopAt = -1; const mfeAt = []; // running MFE per bar, to evaluate any target without re-walking for (let i = 0; i < walk.length; i++) { const b = walk[i]; mfe = Math.max(mfe, (long ? b.h - fill : fill - b.l) / risk); mae = Math.max(mae, (long ? fill - b.l : b.h - fill) / risk); mfeAt.push(mfe); const stopped = long ? b.l <= stop : b.h >= stop; if (stopped) { hitStopAt = i; break; } } const markR = (long ? walk[Math.min(hitStopAt < 0 ? walk.length - 1 : hitStopAt, walk.length - 1)].c - fill : fill - walk[Math.min(hitStopAt < 0 ? walk.length - 1 : hitStopAt, walk.length - 1)].c) / risk; return { triggered: true, mfe, mae, hitStopAt, walk, long, fill, risk, stop, markR, bars: walk.length }; } // Outcome for a given target, with production's "mark at the bell" for unresolved trades. function outcomeFor(a, targetR, beAt = null) { if (!a.triggered) return 0; const { walk, long, fill, risk, stop } = a; const target = long ? fill + targetR * risk : fill - targetR * risk; let be = false; for (const b of walk) { const up = (long ? b.h - fill : fill - b.l) / risk; const stopPx = be ? fill : stop; const stopped = long ? b.l <= stopPx : b.h >= stopPx; const hitTgt = long ? b.h >= target : b.l <= target; if (stopped) return be ? 0 : -1; // stop wins ties if (hitTgt) return targetR; if (beAt != null && up >= beAt) be = true; // move to breakeven once this far ahead } const last = walk[walk.length - 1].c; return +(((long ? last - fill : fill - last) / risk).toFixed(3)); } const rows = []; for (const sym of SYMS) { const bars = loadBars(sym); if (!bars.length) { console.error(`no data for ${sym}`); continue; } // group bars by ET date + session const bySess = new Map(); for (const b of bars) { const { date, min } = etOf(b.t); for (const [name, [s, e, nextDay]] of Object.entries(SESSIONS)) { const start = hhmm(s), end = hhmm(e); let key = null; if (!nextDay) { if (min >= start && min <= end) key = `${date}|${name}`; } else { if (min >= start) key = `${date}|${name}`; else if (min <= end) { const d = new Date(Date.parse(date + "T12:00:00Z") - 86400_000).toISOString().slice(0, 10); key = `${d}|${name}`; } } if (key) { const arr = bySess.get(key) ?? []; arr.push(b); bySess.set(key, arr); } } } for (const [key, sb] of bySess) { if (sb.length < 120) continue; const [date, sess] = key.split("|"); const orEnd = sb[0].t + OR_MIN * 60_000; const or = sb.filter((b) => b.t < orEnd); if (or.length < OR_MIN * 0.6) continue; const orHi = Math.max(...or.map((b) => b.h)), orLo = Math.min(...or.map((b) => b.l)); const after = sb.filter((b) => b.t >= orEnd); if (after.length < 60) continue; for (const long of [true, false]) { const a = attempt(after, long, orHi, orLo); if (a) rows.push({ sym, date, sess, long, ...a }); } } } fs.writeFileSync("/study-rows.json", JSON.stringify(rows.map(({ walk, ...r }) => r))); const trig = rows.filter((r) => r.triggered); const pct = (x, n) => `${((100 * x) / (n || 1)).toFixed(1)}%`; console.log(`attempts ${rows.length} · triggered ${trig.length} (${pct(trig.length, rows.length)}) · symbols ${SYMS.join(",")} · 2026 YTD`); const mfes = trig.map((r) => r.mfe).sort((a, b) => a - b); const q = (p) => mfes[Math.floor(p * (mfes.length - 1))]?.toFixed(2); console.log(`MFE of triggered trades (R): median ${q(0.5)} · p75 ${q(0.75)} · p90 ${q(0.9)}`); for (const t of [0.5, 0.75, 1.0, 1.5, 2.0]) console.log(` P(MFE >= ${t}R) = ${pct(trig.filter((r) => r.mfe >= t).length, trig.length)}`); console.log("\nexpectancy by target (R per ATTEMPT, and per TRIGGERED trade) — production mechanics, mark at the bell:"); for (const t of [0.5, 0.75, 1.0, 1.5, 2.0]) { const rs = rows.map((r) => outcomeFor(r, t)); const tot = rs.reduce((a, b) => a + b, 0), tr = rows.filter((r) => r.triggered).map((r) => outcomeFor(r, t)); const wins = tr.filter((x) => x >= t - 1e-9).length, losses = tr.filter((x) => x <= -0.999).length; console.log(` target ${t}R: ${(tot / rows.length).toFixed(3)}R/attempt · ${(tr.reduce((a, b) => a + b, 0) / (tr.length || 1)).toFixed(3)}R/trade · hit ${pct(wins, tr.length)} · full stops ${pct(losses, tr.length)}`); } console.log("\nwith a move to breakeven once +0.5R ahead:"); for (const t of [1.0, 1.5]) { const tr = rows.filter((r) => r.triggered).map((r) => outcomeFor(r, t, 0.5)); console.log(` target ${t}R + BE@0.5R: ${(tr.reduce((a, b) => a + b, 0) / (tr.length || 1)).toFixed(3)}R/trade · full stops ${pct(tr.filter((x) => x <= -0.999).length, tr.length)} · scratched ${pct(tr.filter((x) => x === 0).length, tr.length)}`); } console.log("\nby session (target 1.5R, production):"); for (const s of Object.keys(SESSIONS)) { const rs = rows.filter((r) => r.sess === s); const tr = rs.filter((r) => r.triggered); console.log(` ${s.padEnd(7)} attempts ${String(rs.length).padStart(4)} · triggered ${pct(tr.length, rs.length)} · ${(tr.map((r) => outcomeFor(r, 1.5)).reduce((a, b) => a + b, 0) / (tr.length || 1)).toFixed(3)}R/trade · median MFE ${(tr.map((r) => r.mfe).sort((a, b) => a - b)[Math.floor(tr.length / 2)] ?? 0).toFixed(2)}R`); }