// Does the best-evidenced intraday futures rule hold on OUR tape in 2026? // Baltussen, Da, Lammers & Martens (JFE 2021): trade the last half hour in the direction of // the day so far (prior close -> 15:30 ET). Reported SR 0.87-1.73 by asset class, success 0.53-0.61. // Also Gao et al (JFE 2018): first half hour (09:30-10:00) predicts the last half hour. import fs from "node:fs"; const DATA = "/fp-data/glbx"; const SYMS = ["ES", "NQ", "YM", "RTY", "CL", "GC", "ZN"]; 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] }; const COMMISSION = 4, YEAR = "2026"; 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() }; } const mean = (x) => x.reduce((a, b) => a + b, 0) / (x.length || 1); const sd = (x) => { const m = mean(x); return Math.sqrt(mean(x.map((v) => (v - m) ** 2))); }; for (const sym of SYMS) { const f = `${DATA}/${sym}/1m/${YEAR}.csv`; if (!fs.existsSync(f)) continue; const [tick, perTick] = SPEC[sym]; const day = new Map(); // date -> { at: Map(min->close) } 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"), cl = +c[7]; if (!Number.isFinite(t) || !Number.isFinite(cl)) continue; const { date, min } = etOf(t); if (min < 9 * 60 + 30 || min > 16 * 60) continue; // RTH only let d = day.get(date); if (!d) { d = new Map(); day.set(date, d); } d.set(min, cl); } const dates = [...day.keys()].sort(); const rows = []; for (let i = 1; i < dates.length; i++) { const prev = day.get(dates[i - 1]), cur = day.get(dates[i]); const prevClose = prev.get(16 * 60) ?? prev.get(15 * 60 + 59); const open30 = cur.get(10 * 60) ?? cur.get(10 * 60 - 1); // 10:00 ET const p1530 = cur.get(15 * 60 + 30), close = cur.get(16 * 60) ?? cur.get(15 * 60 + 59); const openPx = cur.get(9 * 60 + 30); if (![prevClose, open30, p1530, close, openPx].every(Number.isFinite)) continue; rows.push({ date: dates[i], rROD: p1530 / prevClose - 1, r1: open30 / openPx - 1, move: close - p1530 }); } if (rows.length < 60) { console.log(`${sym}: only ${rows.length} days`); continue; } const costUsd = COMMISSION + 2 * perTick; const evalRule = (label, sideOf) => { const pnl = rows.map((r) => { const side = sideOf(r); return side === 0 ? 0 : (side * r.move / tick) * perTick - costUsd; }).filter((_, i) => sideOf(rows[i]) !== 0); const gross = rows.map((r) => { const side = sideOf(r); return side === 0 ? null : (side * r.move / tick) * perTick; }).filter((x) => x != null); const t = mean(pnl) / (sd(pnl) / Math.sqrt(pnl.length || 1)); const sr = (mean(pnl) / (sd(pnl) || 1)) * Math.sqrt(252); console.log(` ${label.padEnd(30)} n ${String(pnl.length).padStart(3)} · gross $${mean(gross).toFixed(2)} · net $${mean(pnl).toFixed(2)}/contract · win ${(100 * pnl.filter((x) => x > 0).length / (pnl.length || 1)).toFixed(1)}% · t ${t.toFixed(2)} · SR ${sr.toFixed(2)}`); }; console.log(`\n${sym} — last half hour (15:30->16:00 ET), ${rows.length} days, cost $${costUsd}/contract round turn:`); evalRule("direction of day so far", (r) => Math.sign(r.rROD)); evalRule("direction of first 30 min", (r) => Math.sign(r.r1)); evalRule("both agree", (r) => (Math.sign(r.rROD) === Math.sign(r.r1) ? Math.sign(r.rROD) : 0)); evalRule("always long (benchmark)", () => 1); }