// Round 3 — the variants the literature says should matter: // opening-range length 5 / 15 / 30 min (Zarattini: Sharpe 2.81 -> 1.43 -> 0.21) // exit: fixed 1.5R (production) vs run-to-session-close capped at 10R (every documented winner) // relative-volume selection (the filter that took SR 0.48 -> 2.81 in the 7,000-stock study) // Costs fixed: dollar risk = ticks x $/tick; cost = $4 round turn + 1 tick slippage each way. import fs from "node:fs"; const DATA = "/fp-data/glbx"; const SYMS = ["CL", "NQ", "ES", "GC", "ZN", "RTY", "YM", "6E"]; const SPEC = { CL: [0.01, 10], NQ: [0.25, 5], ES: [0.25, 12.5], GC: [0.1, 10], ZN: [0.015625, 15.625], RTY: [0.1, 5], YM: [1, 5], "6E": [0.00005, 6.25] }; const COMMISSION = 4; const SESSIONS = { ny: ["09:22", "16:00", false], london: ["02:45", "09:30", false], asia: ["18:40", "03:00", true] }; const 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 offCache = new Map(); function etOf(ms) { const day = Math.floor(ms / 86400_000); let off = offCache.get(day); if (off === undefined) { const p = Object.fromEntries(etFmt.formatToParts(ms).map((x) => [x.type, x.value])); off = Math.round((Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour % 24, +p.minute) - Math.floor(ms / 60000) * 60000) / 60000); offCache.set(day, off); } const d = new Date(ms + off * 60000); 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"), o = +c[4], h = +c[5], l = +c[6], cl = +c[7], v = +c[8]; if (Number.isFinite(t) && Number.isFinite(o)) out.push({ t, o, h, l, c: cl, v: v || 0 }); } return out; } const to5m = (b0) => { const out = []; let cur = null; for (const b of b0) { 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, v: b.v }; } else { cur.h = Math.max(cur.h, b.h); cur.l = Math.min(cur.l, b.l); cur.c = b.c; cur.v += b.v; } } if (cur) out.push(cur); return out; }; const sessions = []; for (const sym of SYMS) { const bars = loadBars(sym); if (!bars.length) { console.error("no data", sym); continue; } const bySess = new Map(); for (const b of bars) { const { date, min } = etOf(b.t); for (const [name, [s, e, nx]] of Object.entries(SESSIONS)) { const st = hhmm(s), en = hhmm(e); let key = null; if (!nx) { if (min >= st && min <= en) key = `${date}|${name}`; } else if (min >= st) key = `${date}|${name}`; else if (min <= en) key = `${new Date(Date.parse(date + "T12:00:00Z") - 86400_000).toISOString().slice(0, 10)}|${name}`; if (key) { const a = bySess.get(key) ?? []; a.push(b); bySess.set(key, a); } } } for (const [key, sb] of bySess) { if (sb.length >= 120) { const [date, sess] = key.split("|"); sessions.push({ sym, date, sess, bars: sb }); } } } console.log(`sessions loaded: ${sessions.length} (${SYMS.length} symbols, 2026 YTD)\n`); function run(orMin, exitMode, minVol) { const volHist = new Map(); const out = []; for (const s of sessions.sort((a, b) => (a.date < b.date ? -1 : 1))) { const { sym, sess, bars } = s; const [tick, perTick] = SPEC[sym]; const orEnd = bars[0].t + orMin * 60_000; const or = bars.filter((b) => b.t < orEnd); if (or.length < orMin * 0.6) continue; const after = bars.filter((b) => b.t >= orEnd); if (after.length < 60) continue; const orHi = Math.max(...or.map((b) => b.h)), orLo = Math.min(...or.map((b) => b.l)); const orVol = or.reduce((a, b) => a + b.v, 0); const hk = `${sym}|${sess}`; const hist = volHist.get(hk) ?? []; const med = hist.length >= 5 ? [...hist].sort((x, y) => x - y)[Math.floor(hist.length / 2)] : null; const rvol = med ? orVol / med : null; hist.push(orVol); if (hist.length > 20) hist.shift(); volHist.set(hk, hist); if (minVol != null && !(rvol != null && rvol >= minVol)) continue; const m5 = to5m(after); for (const long of [true, false]) { const trigger = long ? orHi : orLo, stop = long ? orLo : orHi, planned = Math.abs(trigger - stop); if (!(planned > 0)) continue; let ci = -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; ci = i; break; } if (ci < 0) { out.push({ triggered: false, r: 0, costR: 0 }); continue; } const fillBar = after.find((b) => b.t >= m5[ci].t + 300_000); if (!fillBar) { out.push({ triggered: false, r: 0, costR: 0 }); continue; } const fill = fillBar.o, risk = Math.abs(fill - stop); if (!(risk > 0)) continue; const riskUsd = (risk / tick) * perTick; const costR = (COMMISSION + 2 * perTick) / riskUsd; // $4 RT + 1 tick slippage each way const targetR = exitMode === "fixed" ? 1.5 : 10; const target = long ? fill + targetR * risk : fill - targetR * risk; let r = null; for (const b of after.filter((x) => x.t >= fillBar.t)) { if (long ? b.l <= stop : b.h >= stop) { r = -1; break; } // stop wins ties if (long ? b.h >= target : b.l <= target) { r = targetR; break; } } if (r == null) { const last = after[after.length - 1].c; r = (long ? last - fill : fill - last) / risk; } out.push({ triggered: true, r, costR }); } } const tr = out.filter((x) => x.triggered); const mean = (xs) => xs.reduce((a, b) => a + b, 0) / (xs.length || 1); const gross = mean(tr.map((x) => x.r)), net = mean(tr.map((x) => x.r - x.costR)); const wins = tr.filter((x) => x.r > 0).length; return { n: tr.length, attempts: out.length, gross, net, hit: (100 * wins) / (tr.length || 1), medCost: [...tr.map((x) => x.costR)].sort((a, b) => a - b)[Math.floor(tr.length / 2)] }; } console.log("OR length x exit rule (all sessions, both directions, net = after $4 RT + 1 tick each way):"); for (const orMin of [5, 15, 30]) for (const exitMode of ["fixed", "runner"]) { const x = run(orMin, exitMode, null); console.log(` OR ${String(orMin).padStart(2)}m · ${exitMode === "fixed" ? "target 1.5R " : "run to bell/10R"} · n ${String(x.n).padStart(4)} · gross ${x.gross.toFixed(3)}R · net ${x.net.toFixed(3)}R · win ${x.hit.toFixed(1)}% · median cost ${(x.medCost * 100).toFixed(1)}% of 1R`); } console.log("\nwith relative-volume selection on the opening range (literature's key filter):"); for (const orMin of [5, 15]) for (const mv of [1.2, 1.5, 2]) { const x = run(orMin, "runner", mv); console.log(` OR ${String(orMin).padStart(2)}m · run to bell · RVOL >= ${mv} · n ${String(x.n).padStart(4)} · gross ${x.gross.toFixed(3)}R · net ${x.net.toFixed(3)}R · win ${x.hit.toFixed(1)}%`); }