// Backtest of the claimed "profitable prop trader" strategies — rules frozen in PREREG-2026-09-16.md. // node --max-old-space-size=12288 engine.mjs [NQ|ES ...] → out/trades-.json + out/tape-summary.json import fs from "node:fs"; import path from "node:path"; const HERE = path.dirname(new URL(import.meta.url).pathname); const OUT = path.join(HERE, "out"); fs.mkdirSync(OUT, { recursive: true }); const GLBX = "/fp-data/glbx"; const EVENTS = "/fp-context/events"; const YEARS = ["2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025", "2026"]; // 2018 only warms 20-day means const START = "2019-01-01", OOS = "2024-01-01"; // micro contract economics (PREREG): tick, $ per tick, commission+fees per round turn const SPEC = { NQ: { tick: 0.25, perTick: 0.5, comm: 1.5, straddleD: 40, vwapStopTicks: 12 }, ES: { tick: 0.25, perTick: 1.25, comm: 1.5, straddleD: 10, vwapStopTicks: 4 }, }; const NEWS_SLIP_TICKS = 4; // ── time: ET wall clock per bar, offset cached per UTC hour (DST-safe) ───────── 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 etOffsetMin(ms) { const h = Math.floor(ms / 3600_000); let off = offCache.get(h); 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(h, off); } return off; } const pad = (n) => String(n).padStart(2, "0"); function loadRoot(root) { const t = [], o = [], hi = [], lo = [], c = [], v = [], day = [], mod = []; for (const y of YEARS) { const f = `${GLBX}/${root}/1m/${y}.csv`; if (!fs.existsSync(f)) continue; const text = fs.readFileSync(f, "utf8"); let pos = text.indexOf("\n") + 1; while (pos < text.length) { const nl = text.indexOf("\n", pos); const end = nl < 0 ? text.length : nl; const L = text.slice(pos, end); pos = end + 1; if (!L) continue; const cols = L.split(","); const ms = Date.parse(cols[0].slice(0, 19) + "Z"); const op = +cols[4], hp = +cols[5], lp = +cols[6], cp = +cols[7]; if (!Number.isFinite(ms) || !Number.isFinite(op)) continue; const et = new Date(ms + etOffsetMin(ms) * 60000); t.push(ms); o.push(op); hi.push(hp); lo.push(lp); c.push(cp); v.push(+cols[8] || 0); day.push(`${et.getUTCFullYear()}-${pad(et.getUTCMonth() + 1)}-${pad(et.getUTCDate())}`); mod.push(et.getUTCHours() * 60 + et.getUTCMinutes()); } } // de-duplicate / order (year files can overlap at boundaries) const idx = t.map((_, i) => i).sort((a, b) => t[a] - t[b]); const pick = (arr) => idx.map((i) => arr[i]); const B = { t: pick(t), o: pick(o), h: pick(hi), l: pick(lo), c: pick(c), v: pick(v), day: pick(day), mod: pick(mod) }; const keep = [0]; for (let i = 1; i < B.t.length; i++) if (B.t[i] !== B.t[i - 1]) keep.push(i); for (const k of Object.keys(B)) B[k] = keep.map((i) => B[k][i]); B.n = B.t.length; // index each ET trading date's RTH span (09:30..15:59) and first bar at-or-after any ET minute B.rth = new Map(); for (let i = 0; i < B.n; i++) { if (B.mod[i] < 570 || B.mod[i] > 959) continue; const r = B.rth.get(B.day[i]); if (!r) B.rth.set(B.day[i], { s: i, e: i }); else r.e = i; } return B; } // first bar index >= ms (binary search) function at(B, ms) { let lo = 0, hi = B.n; while (lo < hi) { const m = (lo + hi) >> 1; if (B.t[m] < ms) lo = m + 1; else hi = m; } return lo; } // epoch ms of an ET wall-clock time on an ET date (uses offset at noon that day; DST-safe for 00:00–23:59) function etMs(date, minuteOfDay) { const base = Date.parse(`${date}T12:00:00Z`); const off = etOffsetMin(base); return Date.parse(`${date}T00:00:00Z`) + (minuteOfDay - off) * 60000; } const prevDate = (d) => new Date(Date.parse(d + "T12:00:00Z") - 86400_000).toISOString().slice(0, 10); // ── events: CPI, NFP 08:30; FOMC statement 14:00 ───────────────────────────── function loadEvents() { const ev = new Map(); // date -> [{kind, mod}] const add = (d, kind, mod) => { const a = ev.get(d) ?? []; if (!a.some((x) => x.kind === kind)) a.push({ kind, mod }); ev.set(d, a); }; for (const dir of ["macro-release", "central-bank"]) { for (const f of fs.readdirSync(`${EVENTS}/${dir}`).filter((x) => x.endsWith(".jsonl"))) { for (const L of fs.readFileSync(`${EVENTS}/${dir}/${f}`, "utf8").split("\n")) { if (!L) continue; let j; try { j = JSON.parse(L); } catch { continue; } const sub = j.subclass; let kind = null; if (dir === "macro-release" && j.region === "US" && (sub === "cpi" || sub === "nfp")) kind = sub; if (dir === "central-bank" && (sub === "fomc-statement" || sub === "fomc-decision")) kind = "fomc"; if (!kind) continue; const ms = Date.parse(j.ts_event); if (!Number.isFinite(ms)) continue; const et = new Date(ms + etOffsetMin(ms) * 60000); const d = `${et.getUTCFullYear()}-${pad(et.getUTCMonth() + 1)}-${pad(et.getUTCDate())}`; const m = et.getUTCHours() * 60 + et.getUTCMinutes(); if (kind !== "fomc" && m !== 510) continue; // CPI/NFP only at 08:30 ET if (kind === "fomc" && m !== 840) continue; // FOMC statement at 14:00 ET add(d, kind, m); } } } return ev; } // ── generic executor: spec → trade result for ONE micro ────────────────────── // spec: { date, fillIdx, fillPx (already includes entry slippage), dir (+1/-1), stopPx, targetPx|null, exitMs, entryType } function execute(B, S, spec) { const { fillIdx, fillPx, dir, stopPx, targetPx, exitMs } = spec; const tk = S.tick; let mfe = 0, mae = 0, tMfe = fillIdx, tMae = fillIdx; let exitPx = null, reason = "time"; let i = fillIdx; for (; i < B.n && B.t[i] < exitMs; i++) { const up = dir > 0 ? B.h[i] - fillPx : fillPx - B.l[i]; const dn = dir > 0 ? fillPx - B.l[i] : B.h[i] - fillPx; if (up > mfe) { mfe = up; tMfe = i; } if (dn > mae) { mae = dn; tMae = i; } const stopped = dir > 0 ? B.l[i] <= stopPx : B.h[i] >= stopPx; if (stopped) { exitPx = stopPx - dir * tk; reason = "stop"; break; } // stop order: 1 tick adverse if (targetPx != null && (dir > 0 ? B.h[i] >= targetPx + tk : B.l[i] <= targetPx - tk)) { exitPx = targetPx; reason = "target"; break; } // limit: 1 tick through } if (exitPx == null) { const j = Math.min(Math.max(i - 1, fillIdx), B.n - 1); exitPx = B.c[j] - dir * tk; reason = "time"; } // market exit at the time stop const pts = dir * (exitPx - fillPx); const gross = (pts / tk) * S.perTick; const net = gross - S.comm; const stopRiskUsd = (Math.abs(fillPx - stopPx) / tk) * S.perTick + S.comm; return { date: spec.date, arm: spec.arm, dir, gross: +gross.toFixed(2), net: +net.toFixed(2), reason, mfeUsd: +((mfe / tk) * S.perTick).toFixed(2), maeUsd: +((mae / tk) * S.perTick).toFixed(2), mfeFirst: tMfe <= tMae, stopRiskUsd: +stopRiskUsd.toFixed(2), entryMs: B.t[fillIdx], exitMs: B.t[Math.min(i, B.n - 1)] }; } const marketFill = (B, S, idx, dir, slipTicks = 1) => (idx < B.n ? B.o[idx] + dir * slipTicks * S.tick : null); // ── strategy arms (signal → spec) ──────────────────────────────────────────── function rangeOf(B, s, e) { let h = -Infinity, l = Infinity; for (let i = s; i <= e; i++) { if (B.h[i] > h) h = B.h[i]; if (B.l[i] < l) l = B.l[i]; } return { h, l }; } function to5m(B, s, e) { // 5m bars aligned to the ET clock, returns [{s,e,o,h,l,c,endMod}] const out = []; let cur = null; for (let i = s; i <= e; i++) { const bucket = Math.floor(B.mod[i] / 5) * 5; if (!cur || cur.bucket !== bucket) { if (cur) out.push(cur); cur = { bucket, s: i, e: i, o: B.o[i], h: B.h[i], l: B.l[i], c: B.c[i] }; } else { cur.e = i; cur.h = Math.max(cur.h, B.h[i]); cur.l = Math.min(cur.l, B.l[i]); cur.c = B.c[i]; } } if (cur) out.push(cur); return out; } const idxAtMod = (B, date, mod) => at(B, etMs(date, mod)); const flatMs = (date) => etMs(date, 955); // 15:55 ET const ARMS = { "ORB15-2X": (B, S, date) => orb15(B, S, date, "2x"), "ORB15-RUN": (B, S, date) => orb15(B, S, date, "run"), "IB-EXT": (B, S, date) => ibExt(B, S, date), "IB-FADE": (B, S, date) => ibFade(B, S, date), "RANGE-FADE": (B, S, date) => rangeFade(B, S, date), "ICT-SB-AM": (B, S, date, ctx) => ictSB(B, S, date, ctx, "am", 2), "ICT-SB-AM-3R": (B, S, date, ctx) => ictSB(B, S, date, ctx, "am", 3), "ICT-SB-LDN": (B, S, date, ctx) => ictSB(B, S, date, ctx, "ldn", 2), "VWAP-FADE": (B, S, date, ctx) => vwapFade(B, S, date, ctx), "VWAP-TREND": (B, S, date) => vwapTrend(B, S, date), "EMA-9-21": (B, S, date, ctx) => ema921(B, S, date, ctx), "NEWS-STRADDLE": (B, S, date, ctx) => newsStraddle(B, S, date, ctx), "NEWS-FADE": (B, S, date, ctx) => newsFade(B, S, date, ctx), }; function orb15(B, S, date, mode) { const r = B.rth.get(date); if (!r) return []; const s = idxAtMod(B, date, 570), e = idxAtMod(B, date, 585) - 1; if (e - s < 10) return []; const { h, l } = rangeOf(B, s, e); const R = h - l; if (!(R > 0)) return []; for (let i = e + 1; i <= r.e && B.mod[i] < 955; i++) { const up = B.c[i] > h, dn = B.c[i] < l; if (!up && !dn) continue; const dir = up ? 1 : -1; const past = up ? B.c[i] - h : l - B.c[i]; if (past > 0.25 * R) return []; // no chase → first break skipped, day done const fi = i + 1; if (fi > r.e) return []; const fill = marketFill(B, S, fi, dir); const stop = mode === "2x" ? (dir > 0 ? l : h) : (h + l) / 2; const target = mode === "2x" ? fill + dir * 2 * R : null; return [{ date, fillIdx: fi, fillPx: fill, dir, stopPx: stop, targetPx: target, exitMs: flatMs(date) }]; } return []; } function ibExt(B, S, date) { const r = B.rth.get(date); if (!r) return []; const s = idxAtMod(B, date, 570), e = idxAtMod(B, date, 630) - 1; if (e - s < 40) return []; const { h, l } = rangeOf(B, s, e); if (!(h > l)) return []; for (const b of to5m(B, e + 1, r.e)) { if (B.mod[b.e] >= 955) break; const dir = b.c > h ? 1 : b.c < l ? -1 : 0; if (!dir) continue; const fi = b.e + 1; if (fi > r.e) return []; return [{ date, fillIdx: fi, fillPx: marketFill(B, S, fi, dir), dir, stopPx: (h + l) / 2, targetPx: null, exitMs: flatMs(date) }]; } return []; } function ibFade(B, S, date) { const r = B.rth.get(date); if (!r) return []; const s = idxAtMod(B, date, 570), e = idxAtMod(B, date, 630) - 1; if (e - s < 40) return []; const { h, l } = rangeOf(B, s, e); const IB = h - l; if (!(IB > 0)) return []; const bars = to5m(B, e + 1, r.e); for (let k = 0; k < bars.length; k++) { const b = bars[k]; if (B.mod[b.e] >= 955) break; const extDir = b.c >= h + 0.1 * IB ? 1 : b.c <= l - 0.1 * IB ? -1 : 0; if (!extDir) continue; let extreme = extDir > 0 ? b.h : b.l; for (let m = k + 1; m < bars.length && m <= k + 6; m++) { const x = bars[m]; extreme = extDir > 0 ? Math.max(extreme, x.h) : Math.min(extreme, x.l); const backInside = extDir > 0 ? x.c <= h : x.c >= l; if (!backInside) continue; const dir = -extDir; const fi = x.e + 1; if (fi > r.e || B.mod[fi] >= 955) return []; return [{ date, fillIdx: fi, fillPx: marketFill(B, S, fi, dir), dir, stopPx: extreme, targetPx: (h + l) / 2, exitMs: flatMs(date) }]; } return []; // first extension only } return []; } function rangeFade(B, S, date) { const r = B.rth.get(date); if (!r) return []; const s = idxAtMod(B, date, 570), e = idxAtMod(B, date, 600) - 1; if (e - s < 20) return []; const { h, l } = rangeOf(B, s, e); const R = h - l; if (!(R > 0)) return []; for (let i = e + 1; i <= r.e && B.mod[i] < 955; i++) { const touchH = B.h[i] >= h + S.tick, touchL = B.l[i] <= l - S.tick; if (!touchH && !touchL) continue; if (touchH && touchL) return []; // both in one bar: ambiguous, skip const dir = touchH ? -1 : 1; const fill = touchH ? h : l; return [{ date, fillIdx: i, fillPx: fill, dir, stopPx: touchH ? h + 0.5 * R : l - 0.5 * R, targetPx: (h + l) / 2, exitMs: flatMs(date), entryType: "limit" }]; } return []; } function fractalSwings(B, s, e) { // 5-bar fractals, confirmed 2 bars later const highs = [], lows = []; for (let i = s + 2; i <= e - 2; i++) { if (B.h[i] > B.h[i - 1] && B.h[i] > B.h[i - 2] && B.h[i] > B.h[i + 1] && B.h[i] > B.h[i + 2]) highs.push(i); if (B.l[i] < B.l[i - 1] && B.l[i] < B.l[i - 2] && B.l[i] < B.l[i + 1] && B.l[i] < B.l[i + 2]) lows.push(i); } return { highs, lows }; } function ictSB(B, S, date, ctx, win, rr) { if (ctx.events.has(date)) return []; const tk = S.tick; let wS, wE, levels, entryUntil, flat; if (win === "am") { if (!B.rth.get(date)) return []; const a0 = idxAtMod(B, date, 570), a1 = idxAtMod(B, date, 600) - 1; const onS = at(B, etMs(prevDate(date), 1080)), onE = idxAtMod(B, date, 570) - 1; if (a1 - a0 < 20 || onE - onS < 60) return []; const A = rangeOf(B, a0, a1), O = rangeOf(B, onS, onE); levels = { highs: [A.h, O.h], lows: [A.l, O.l] }; wS = idxAtMod(B, date, 600); wE = idxAtMod(B, date, 660) - 1; entryUntil = etMs(date, 690); flat = flatMs(date); } else { const asS = at(B, etMs(prevDate(date), 1080)), asE = idxAtMod(B, date, 180) - 1; if (asE - asS < 60) return []; const A = rangeOf(B, asS, asE); levels = { highs: [A.h], lows: [A.l] }; wS = idxAtMod(B, date, 180); wE = idxAtMod(B, date, 240) - 1; entryUntil = etMs(date, 270); flat = etMs(date, 570); } if (wE - wS < 30) return []; for (let i = wS; i <= wE; i++) { for (const side of [1, -1]) { // +1 = swept a high → bearish setup const swept = side > 0 ? levels.highs.some((L) => B.h[i] >= L + tk) : levels.lows.some((L) => B.l[i] <= L - tk); if (!swept) continue; let ext = side > 0 ? B.h[i] : B.l[i], extIdx = i; const lookS = Math.max(wS - 30, i - 30); for (let m = i + 1; m <= Math.min(i + 20, wE + 20, B.n - 1); m++) { if (side > 0 ? B.h[m] > ext : B.l[m] < ext) { ext = side > 0 ? B.h[m] : B.l[m]; extIdx = m; } const { highs, lows } = fractalSwings(B, lookS, m - 1); const swing = side > 0 ? lows.filter((x) => x <= m - 3).pop() : highs.filter((x) => x <= m - 3).pop(); if (swing == null) continue; const mss = side > 0 ? B.c[m] < B.l[swing] : B.c[m] > B.h[swing]; if (!mss) continue; let fvg = null; // most recent 3-bar imbalance inside the displacement for (let k = Math.max(extIdx, lookS) + 2; k <= m; k++) { if (side > 0 && B.h[k] < B.l[k - 2]) fvg = { top: B.l[k - 2], bot: B.h[k] }; if (side < 0 && B.l[k] > B.h[k - 2]) fvg = { top: B.l[k], bot: B.h[k - 2] }; } if (!fvg) return []; // first MSS without FVG ends the window's setup const dir = -side; const entry = (fvg.top + fvg.bot) / 2; const stop = side > 0 ? ext + tk : ext - tk; const R = Math.abs(stop - entry); if (!(R > 0)) return []; for (let f = m + 1; f < B.n && B.t[f] < entryUntil; f++) { if (dir < 0 ? B.h[f] >= stop : B.l[f] <= stop) return []; // invalidated before fill const filled = dir < 0 ? B.h[f] >= entry + tk : B.l[f] <= entry - tk; if (filled) return [{ date, fillIdx: f, fillPx: entry, dir, stopPx: stop, targetPx: entry + dir * rr * R, exitMs: flat, entryType: "limit" }]; } return []; } } } return []; } function vwapSeries(B, s, e) { let pv = 0, vv = 0, pv2 = 0; const vw = [], sd = []; for (let i = s; i <= e; i++) { const tp = (B.h[i] + B.l[i] + B.c[i]) / 3, vol = B.v[i] || 1; pv += tp * vol; vv += vol; pv2 += tp * tp * vol; const m = pv / vv; vw.push(m); sd.push(Math.sqrt(Math.max(0, pv2 / vv - m * m))); } return { vw, sd }; } function vwapFade(B, S, date, ctx) { if (ctx.events.has(date)) return []; const r = B.rth.get(date); if (!r) return []; const s = r.s; const { vw, sd } = vwapSeries(B, s, r.e); const fhMean = ctx.firstHourMean.get(date); if (!fhMean) return []; const out = []; let busyUntil = 0; let hi = -Infinity, lo = Infinity; for (let i = s; i <= r.e && B.mod[i] < 955; i++) { hi = Math.max(hi, B.h[i]); lo = Math.min(lo, B.l[i]); if (B.mod[i] < 600 || B.t[i] < busyUntil || out.length >= 2) continue; if (hi - lo > 1.5 * fhMean && B.mod[i] <= 630) continue; // running first-hour range as trend filter (no look-ahead) if (B.mod[i] > 630 && ctx.firstHour.get(date) > 1.5 * fhMean) continue; const k = i - s, band = 2 * sd[k]; const tagUp = B.h[i] >= vw[k] + band && B.c[i] < vw[k] + band; const tagDn = B.l[i] <= vw[k] - band && B.c[i] > vw[k] - band; if (!tagUp && !tagDn) continue; const dir = tagUp ? -1 : 1; const fi = i + 1; if (fi > r.e) break; const stop = tagUp ? B.h[i] + S.vwapStopTicks * S.tick : B.l[i] - S.vwapStopTicks * S.tick; const spec = { date, fillIdx: fi, fillPx: marketFill(B, S, fi, dir), dir, stopPx: stop, targetPx: vw[k], exitMs: flatMs(date) }; out.push(spec); busyUntil = execute(B, S, spec).exitMs + 60_000; } return out; } function vwapTrend(B, S, date) { const r = B.rth.get(date); if (!r) return []; const s = r.s; const { vw, sd } = vwapSeries(B, s, r.e); const bars = to5m(B, s, idxAtMod(B, date, 630) - 1); if (bars.length < 10) return []; const above = bars.every((b) => b.c > vw[b.e - s]), below = bars.every((b) => b.c < vw[b.e - s]); if (!above && !below) return []; const dir = above ? 1 : -1; for (let i = idxAtMod(B, date, 630); i <= r.e && B.mod[i] < 955; i++) { const lim = vw[i - 1 - s], sig = sd[i - 1 - s]; const touched = dir > 0 ? B.l[i] <= lim - S.tick : B.h[i] >= lim + S.tick; if (!touched) continue; return [{ date, fillIdx: i, fillPx: lim, dir, stopPx: lim - dir * sig, targetPx: null, exitMs: flatMs(date), entryType: "limit" }]; } return []; } function ema921(B, S, date, ctx) { const r = B.rth.get(date); if (!r) return []; const bars = to5m(B, r.s, r.e); const st = ctx.ema; const out = []; let busyUntil = 0; for (let k = 0; k < bars.length; k++) { const b = bars[k]; const prev9 = st.e9, prev21 = st.e21; st.e9 = st.e9 == null ? b.c : st.e9 + (2 / 10) * (b.c - st.e9); st.e21 = st.e21 == null ? b.c : st.e21 + (2 / 22) * (b.c - st.e21); if (prev9 == null || out.length >= 3 || k < 5) continue; if (B.mod[b.e] < 595 || B.mod[b.e] > 924 || B.t[b.e] < busyUntil) continue; const up = prev9 <= prev21 && st.e9 > st.e21, dn = prev9 >= prev21 && st.e9 < st.e21; if (!up && !dn) continue; const dir = up ? 1 : -1; const fi = b.e + 1; if (fi > r.e) continue; const swing = dir > 0 ? Math.min(...bars.slice(k - 5, k).map((x) => x.l)) : Math.max(...bars.slice(k - 5, k).map((x) => x.h)); const fill = marketFill(B, S, fi, dir); const R = Math.abs(fill - swing); if (!(R > 0) || (dir > 0 ? swing >= fill : swing <= fill)) continue; const spec = { date, fillIdx: fi, fillPx: fill, dir, stopPx: swing, targetPx: fill + dir * 2 * R, exitMs: flatMs(date) }; out.push(spec); busyUntil = execute(B, S, spec).exitMs + 60_000; } return out; } function newsStraddle(B, S, date, ctx) { const evs = ctx.events.get(date); if (!evs) return []; const out = []; const D = S.straddleD; const tk = S.tick; for (const e of evs) { const ref = at(B, etMs(date, e.mod - 3)); if (ref >= B.n || B.day[ref] !== date) continue; const base = B.c[ref], buy = base + D, sell = base - D; const startMs = etMs(date, e.mod - 2), endMs = etMs(date, e.mod + 60); for (let i = at(B, startMs); i < B.n && B.t[i] < endMs; i++) { const up = B.h[i] >= buy, dn = B.l[i] <= sell; if (!up && !dn) continue; if (up && dn) { // both sides inside one minute: book the worst case (filled, then stopped) out.push({ date, fillIdx: i, fillPx: buy + NEWS_SLIP_TICKS * tk, dir: 1, stopPx: buy - D, targetPx: null, exitMs: B.t[i] + 1, forcedWorst: true, arm: "NEWS-STRADDLE" }); break; } const dir = up ? 1 : -1; const nominal = up ? buy : sell; out.push({ date, fillIdx: i, fillPx: nominal + dir * NEWS_SLIP_TICKS * tk, dir, stopPx: nominal - dir * D, targetPx: nominal + dir * 2 * D, exitMs: endMs }); break; } } return out; } function newsFade(B, S, date, ctx) { const evs = ctx.events.get(date); if (!evs) return []; const rngMean = ctx.dayRangeMean.get(date); if (!rngMean) return []; const out = []; for (const e of evs) { const pre = at(B, etMs(date, e.mod - 1)), post = at(B, etMs(date, e.mod + 15)); if (pre >= B.n || post >= B.n || B.day[pre] !== date || B.day[post] !== date) continue; const move = B.c[post] - B.c[pre]; if (Math.abs(move) < 0.25 * rngMean) continue; const dir = move > 0 ? -1 : 1; let ext = move > 0 ? -Infinity : Infinity; for (let i = at(B, etMs(date, e.mod)); i <= post; i++) ext = move > 0 ? Math.max(ext, B.h[i]) : Math.min(ext, B.l[i]); const fi = post + 1; if (fi >= B.n) continue; out.push({ date, fillIdx: fi, fillPx: marketFill(B, S, fi, dir), dir, stopPx: ext, targetPx: B.c[pre], exitMs: etMs(date, e.mod + 120) }); } return out; } // ── run ────────────────────────────────────────────────────────────────────── function mulberry32(a) { return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const mean = (x) => x.reduce((a, b) => a + b, 0) / (x.length || 1); const tstat = (x) => { if (x.length < 2) return 0; const m = mean(x); const s = Math.sqrt(x.reduce((a, b) => a + (b - m) ** 2, 0) / (x.length - 1)); return s ? m / (s / Math.sqrt(x.length)) : 0; }; const roots = process.argv.slice(2).length ? process.argv.slice(2) : ["NQ", "ES"]; const events = loadEvents(); console.log(`events: ${[...events.values()].flat().length} releases on ${events.size} dates`); const summary = fs.existsSync(`${OUT}/tape-summary.json`) ? JSON.parse(fs.readFileSync(`${OUT}/tape-summary.json`, "utf8")) : {}; for (const root of roots) { const t0 = Date.now(); const B = loadRoot(root); const S = SPEC[root]; const dates = [...B.rth.keys()].sort(); console.log(`${root}: ${B.n} bars, ${dates.length} RTH dates, loaded in ${((Date.now() - t0) / 1000).toFixed(0)}s`); // rolling 20-day means (prior days only) for the filters that need them const firstHour = new Map(), dayRange = new Map(), firstHourMean = new Map(), dayRangeMean = new Map(); for (const d of dates) { const r = B.rth.get(d); const fe = idxAtMod(B, d, 630) - 1; if (fe > r.s) { const x = rangeOf(B, r.s, fe); firstHour.set(d, x.h - x.l); } const y = rangeOf(B, r.s, r.e); dayRange.set(d, y.h - y.l); } for (let k = 20; k < dates.length; k++) { const prev = dates.slice(k - 20, k); firstHourMean.set(dates[k], mean(prev.map((d) => firstHour.get(d)).filter(Number.isFinite))); dayRangeMean.set(dates[k], mean(prev.map((d) => dayRange.get(d)).filter(Number.isFinite))); } const trades = []; for (const [arm, fn] of Object.entries(ARMS)) { const ctx = { events, firstHour, firstHourMean, dayRangeMean, ema: { e9: null, e21: null } }; const rng = mulberry32(20260916); let nArm = 0; for (const d of dates) { const specs = fn(B, S, d, ctx); if (d < START) continue; for (const sp of specs) { if (sp.fillPx == null || sp.fillIdx >= B.n) continue; const res = sp.forcedWorst ? { date: d, dir: 1, gross: -(((S.straddleD / S.tick) + NEWS_SLIP_TICKS + 1) * S.perTick), net: -(((S.straddleD / S.tick) + NEWS_SLIP_TICKS + 1) * S.perTick) - S.comm, reason: "both-sides-one-bar", mfeUsd: 0, maeUsd: ((S.straddleD / S.tick) + NEWS_SLIP_TICKS + 1) * S.perTick, mfeFirst: false, stopRiskUsd: ((S.straddleD / S.tick) + NEWS_SLIP_TICKS + 1) * S.perTick + S.comm, entryMs: B.t[sp.fillIdx], exitMs: B.t[sp.fillIdx] } : execute(B, S, sp); trades.push({ ...res, arm, root, half: d < OOS ? "IS" : "OOS" }); // zero-edge control: same time, entry and geometry, direction by seeded coin flip const flip = rng() < 0.5 ? 1 : -1; const dir = sp.dir * flip; const stopDist = Math.abs(sp.fillPx - sp.stopPx), tgtDist = sp.targetPx == null ? null : Math.abs(sp.targetPx - sp.fillPx); const ctrl = execute(B, S, { ...sp, dir, stopPx: sp.fillPx - dir * stopDist, targetPx: tgtDist == null ? null : sp.fillPx + dir * tgtDist }); trades.push({ ...ctrl, arm: `${arm}~CTRL`, root, half: d < OOS ? "IS" : "OOS" }); nArm++; } } console.log(` ${arm.padEnd(14)} ${nArm} trades`); } fs.writeFileSync(`${OUT}/trades-${root}.json`, JSON.stringify(trades)); // tape verdicts (PREREG rule) const byArm = new Map(); for (const tr of trades) { const a = byArm.get(tr.arm) ?? []; a.push(tr); byArm.set(tr.arm, a); } summary[root] = {}; for (const [arm, ts] of byArm) { const half = (h) => ts.filter((x) => x.half === h); const row = (xs) => ({ n: xs.length, winPct: +(100 * xs.filter((x) => x.net > 0).length / (xs.length || 1)).toFixed(1), grossMean: +mean(xs.map((x) => x.gross)).toFixed(2), netMean: +mean(xs.map((x) => x.net)).toFixed(2), netT: +tstat(xs.map((x) => x.net)).toFixed(2), grossT: +tstat(xs.map((x) => x.gross)).toFixed(2), netSum: +xs.reduce((a, x) => a + x.net, 0).toFixed(0) }); const IS = row(half("IS")), OOSr = row(half("OOS")), ALL = row(ts); let verdict; if (IS.n < 100 || OOSr.n < 100) verdict = "INSUFFICIENT"; else if (IS.netMean > 0 && OOSr.netMean > 0 && ALL.netT >= 3 && ALL.grossMean > 0) verdict = "VALIDATED"; else if (IS.netMean > 0 && OOSr.netMean > 0) verdict = "INCONCLUSIVE"; else verdict = "REFUTED"; summary[root][arm] = { verdict, IS, OOS: OOSr, ALL }; } fs.writeFileSync(`${OUT}/tape-summary.json`, JSON.stringify(summary, null, 1)); console.log(`${root} done in ${((Date.now() - t0) / 1000).toFixed(0)}s`); }