// Round 2: does ANY conditioner turn the breakout into an edge, and what do costs cost? // Same production mechanics as study.mjs; adds volume, ATR-normalised stop size, trend // alignment, confirmation-bar quality, time of day — and the commission/slippage hurdle in R. import fs from "node:fs"; const DATA = "/fp-data/glbx"; const SYMS = ["CL", "NQ", "ES", "GC", "ZN", "RTY", "YM", "6E"]; // tick size, $ per tick (full-size contract), round-turn commission $ (retail futures ~$4 RT) const SPEC = { CL: [0.01, 10, 4], NQ: [0.25, 5, 4], ES: [0.25, 12.5, 4], GC: [0.1, 10, 4], ZN: [0.015625, 15.625, 4], RTY: [0.1, 5, 4], YM: [1, 5, 4], "6E": [0.00005, 6.25, 4] }; 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 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 atr = (bars, n = 14) => { if (bars.length < 2) return 0; let s = 0, k = 0; for (let i = Math.max(1, bars.length - n); i < bars.length; i++) { const p = bars[i - 1], b = bars[i]; s += Math.max(b.h - b.l, Math.abs(b.h - p.c), Math.abs(b.l - p.c)); k++; } return k ? s / k : 0; }; const rows = []; for (const sym of SYMS) { const bars = loadBars(sym); if (!bars.length) continue; const [tick, perTick, commission] = SPEC[sym]; const bySess = new Map(); const dayClose = new Map(); for (const b of bars) { const { date, min } = etOf(b.t); dayClose.set(date, b.c); 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); } } } const dates = [...new Set([...bySess.keys()].map((k) => k.split("|")[0]))].sort(); const prevClose = new Map(); dates.forEach((d, i) => { if (i) prevClose.set(d, dayClose.get(dates[i - 1])); }); const orVolHist = []; 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 orVol = or.reduce((a, b) => a + b.v, 0); const after = sb.filter((b) => b.t >= orEnd); if (after.length < 60) continue; const a5 = to5m(sb.filter((b) => b.t < orEnd)); const atr5 = atr(a5); orVolHist.push(orVol); const volMed = orVolHist.slice(-20).sort((x, y) => x - y)[Math.floor(Math.min(orVolHist.length, 20) / 2)] || orVol; const pc = prevClose.get(date); for (const long of [true, false]) { const trigger = long ? orHi : orLo, stop = long ? orLo : orHi, planned = Math.abs(trigger - stop); if (!(planned > 0)) continue; const m5 = to5m(after); 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) { rows.push({ sym, date, sess, long, triggered: false }); continue; } const cb = m5[ci], closeMs = cb.t + 300_000; const fillBar = after.find((b) => b.t >= closeMs); if (!fillBar) { rows.push({ sym, date, sess, long, triggered: false }); continue; } const fill = fillBar.o, risk = Math.abs(fill - stop); if (!(risk > 0)) continue; const walk = after.filter((b) => b.t >= fillBar.t); const { min: emin } = etOf(fillBar.t); // confirmation-bar quality: close in the top (long) / bottom (short) quarter of its own range, volume vs session median const barRange = cb.h - cb.l || 1; const closePos = long ? (cb.c - cb.l) / barRange : (cb.h - cb.c) / barRange; rows.push({ sym, date, sess, long, triggered: true, fill, risk, stop, walk: walk.map((b) => [b.h, b.l, b.c]), riskTicks: risk / tick, costR: (commission + 2 * tick * perTick) / (risk * perTick), // commission + 1 tick slippage each way atrRatio: atr5 ? risk / atr5 : null, volRatio: volMed ? orVol / volMed : null, closePos, trendAligned: pc == null ? null : (long ? fill > pc : fill < pc), entryMin: emin }); } } } const outcome = (r, targetR) => { if (!r.triggered) return 0; const { walk, long, fill, risk, stop } = r; const target = long ? fill + targetR * risk : fill - targetR * risk; for (const [h, l, c] of walk) { if (long ? l <= stop : h >= stop) return -1; if (long ? h >= target : l <= target) return targetR; } const last = walk[walk.length - 1][2]; return (long ? last - fill : fill - last) / risk; }; const trig = rows.filter((r) => r.triggered); const mean = (xs) => xs.reduce((a, b) => a + b, 0) / (xs.length || 1); const line = (label, rs, t = 1.5) => { if (rs.length < 40) return; const g = rs.map((r) => outcome(r, t)); const net = g.map((x, i) => x - rs[i].costR); console.log(` ${label.padEnd(34)} n ${String(rs.length).padStart(4)} · gross ${mean(g).toFixed(3)}R · net of costs ${mean(net).toFixed(3)}R`); }; console.log(`triggered trades ${trig.length} · 2026 YTD · target 1.5R unless noted\n`); console.log("COST HURDLE (commission $4 RT + 1 tick slippage each way, as a fraction of the risk):"); const costs = trig.map((r) => r.costR).sort((a, b) => a - b); console.log(` median ${(costs[Math.floor(costs.length / 2)] * 100).toFixed(1)}% of 1R · p25 ${(costs[Math.floor(costs.length * 0.25)] * 100).toFixed(1)}% · p75 ${(costs[Math.floor(costs.length * 0.75)] * 100).toFixed(1)}%`); console.log(` median stop size ${Math.round(trig.map((r) => r.riskTicks).sort((a, b) => a - b)[Math.floor(trig.length / 2)])} ticks\n`); console.log("CONDITIONERS (each row = the subset that passes the filter):"); line("all triggered", trig); for (const [lo, hi] of [[0, 0.5], [0.5, 1], [1, 2], [2, 99]]) line(`stop ${lo}-${hi}x ATR14(5m)`, trig.filter((r) => r.atrRatio != null && r.atrRatio >= lo && r.atrRatio < hi)); for (const [lo, hi] of [[0, 0.8], [0.8, 1.2], [1.2, 2], [2, 99]]) line(`opening-range volume ${lo}-${hi}x median`, trig.filter((r) => r.volRatio != null && r.volRatio >= lo && r.volRatio < hi)); line("aligned with prior-day close", trig.filter((r) => r.trendAligned === true)); line("against prior-day close", trig.filter((r) => r.trendAligned === false)); line("confirm bar closes in top/bottom 25%", trig.filter((r) => r.closePos >= 0.75)); line("confirm bar closes mid-range", trig.filter((r) => r.closePos < 0.5)); for (const s of Object.keys(SESSIONS)) line(`session ${s}`, trig.filter((r) => r.sess === s)); for (const s of SYMS) line(`symbol ${s}`, trig.filter((r) => r.sym === s)); console.log("\nBEST-CASE STACK (aligned + strong close + volume expansion + stop >= 0.8 ATR):"); const stack = trig.filter((r) => r.trendAligned === true && r.closePos >= 0.75 && (r.volRatio ?? 0) >= 1.2 && (r.atrRatio ?? 0) >= 0.8); for (const t of [0.75, 1, 1.5]) { const g = stack.map((r) => outcome(r, t)); console.log(` target ${t}R: n ${stack.length} · gross ${mean(g).toFixed(3)}R · net ${mean(g.map((x, i) => x - stack[i].costR)).toFixed(3)}R · hit ${(100 * g.filter((x) => x >= t - 1e-9).length / (g.length || 1)).toFixed(1)}%`); }