// The overnight hold: 16:00 ET close -> next 09:30 ET open. Friction is a far smaller share of the // move than an intraday scalp, and the overnight/intraday return split is one of the most replicated // results in the equity literature. Tested unconditionally and gated on prior-day dealer gamma. import fs from "node:fs"; import zlib from "node:zlib"; const GLBX = "/fp-data/glbx", GEXDIR = "/fp-data/gex/history"; const SPEC = { NQ: [0.25, 5, 14], ES: [0.25, 12.5, 29], CL: [0.01, 10, 24], GC: [0.1, 10, 24] }; const YEARS = ["2020", "2021", "2022", "2023", "2024", "2025", "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))); }; const nge = new Map(); for (const f of fs.readdirSync(GEXDIR).filter((x) => x.endsWith(".json.gz")).sort()) { try { const j = JSON.parse(zlib.gunzipSync(fs.readFileSync(`${GEXDIR}/${f}`))); const rows = Array.isArray(j.qqq) ? j.qqq : []; if (!rows.length) continue; let g = 0; for (const r of rows) g += (+r.call_gex || 0) + (+r.put_gex || 0); nge.set(j.date ?? f.slice(0, 10), g); } catch {} } const gexDates = [...nge.keys()].sort(); const prevGamma = (date) => { let lo = 0, hi = gexDates.length - 1, best = null; while (lo <= hi) { const m = (lo + hi) >> 1; if (gexDates[m] < date) { best = gexDates[m]; lo = m + 1; } else hi = m - 1; } return best ? nge.get(best) : null; }; for (const [sym, [tick, perTick, cost]] of Object.entries(SPEC)) { const day = new Map(); for (const y of YEARS) { const f = `${GLBX}/${sym}/1m/${y}.csv`; if (!fs.existsSync(f)) continue; 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], cl = +c[7]; if (!Number.isFinite(t)) continue; const { date, min } = etOf(t); if (min !== 9 * 60 + 30 && min !== 16 * 60 && min !== 15 * 60 + 59) continue; let d = day.get(date); if (!d) { d = {}; day.set(date, d); } if (min === 9 * 60 + 30) d.open = o; else d.close = 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]); if (!Number.isFinite(prev?.close) || !Number.isFinite(cur?.open)) continue; const gapUsd = ((cur.open - prev.close) / tick) * perTick; const dayUsd = Number.isFinite(cur.close) ? ((cur.close - cur.open) / tick) * perTick : null; rows.push({ date: dates[i], gapUsd, dayUsd, gamma: prevGamma(dates[i]) }); } const rep = (label, rs, pick) => { if (rs.length < 30) return; const pnl = rs.map((r) => pick(r) - cost); const t = mean(pnl) / (sd(pnl) / Math.sqrt(pnl.length)); console.log(` ${label.padEnd(34)} n ${String(rs.length).padStart(4)} · gross $${mean(rs.map(pick)).toFixed(2)} · net $${mean(pnl).toFixed(2)} · win ${(100 * pnl.filter((x) => x > 0).length / pnl.length).toFixed(1)}% · t ${t.toFixed(2)} · ann.SR ${((mean(pnl) / (sd(pnl) || 1)) * Math.sqrt(252)).toFixed(2)}`); }; console.log(`${sym} — overnight: buy 16:00 ET close, sell 09:30 ET open (${rows.length} nights, ${YEARS[0]}-2026, cost $${cost}):`); rep("long overnight, every night", rows, (r) => r.gapUsd); rep(" ...prior-day gamma NEGATIVE", rows.filter((r) => r.gamma != null && r.gamma < 0), (r) => r.gapUsd); rep(" ...prior-day gamma POSITIVE", rows.filter((r) => r.gamma != null && r.gamma >= 0), (r) => r.gapUsd); rep("long the RTH day instead (control)", rows.filter((r) => r.dayUsd != null), (r) => r.dayUsd); console.log(""); }