// @ts-nocheck
/**
 * Focused OOS/walk-forward validation for LEVEL_WAIT_BARS under L16/S8 cap420.
 *
 * Validates the candidate from exploratory research:
 *   LONG_ENTRY_THRESH = 16, SHORT_ENTRY_THRESH = 8
 *
 * This script keeps the current canonical mechanics and sweeps only the long
 * threshold, then conditionally sweeps hard-cap bars around the best candidate.
 * It reports full-period, yearly, half-split, leave-one-year-out, paired monthly
 * fixed-notional and actual L3-sized comparisons, and simple walk-forward
 * threshold selection.
 */

import fs from 'node:fs'
import readline from 'node:readline'

type Bar = { ts: number; o: number; h: number; l: number; c: number; v?: number }
type Trade = {
  gross: number
  net: number
  month: string
  year: number
  side: 'long' | 'short'
  bar: number
  sizingBps: number
  softSlBps: number
  isStructL2: boolean
  isFallback: boolean
  isSl: boolean
  isHardSl: boolean
  hour: number
  dow: number
}

type SimCfg = {
  label: string
  longT: number
  shortT: number
  hardCapBars?: number
  longCapBars?: number
  shortCapBars?: number
  levelWaitBars?: number
  immediateDistBps?: number
}

async function loadBars(files: string[]): Promise<Bar[]> {
  const seen = new Set<number>()
  const all: Bar[] = []
  for (const f of files) {
    if (!fs.existsSync(f)) continue
    const rl = readline.createInterface({ input: fs.createReadStream(f), crlfDelay: Infinity })
    for await (const line of rl) {
      if (!line.trim()) continue
      const b = JSON.parse(line)
      if (!seen.has(b.ts)) { seen.add(b.ts); all.push(b) }
    }
  }
  all.sort((a, b) => a.ts - b.ts)
  return all
}

function lbRet(bars: Bar[], i: number, n: number) { return i >= n && bars[i - n].c > 0 ? (bars[i].c - bars[i - n].c) / bars[i - n].c * 10000 : 0 }
function closePos(b: Bar) { return b.h > b.l ? (b.c - b.l) / (b.h - b.l) : 0.5 }
function avgCP(bars: Bar[], i: number, n: number) { let s = 0; for (let j = Math.max(0, i - n + 1); j <= i; j++) s += closePos(bars[j]); return s / Math.min(n, i + 1) }
function rsi(bars: Bar[], i: number, n: number) { if (i < n) return 50; let u = 0, d = 0; for (let j = i - n + 1; j <= i; j++) { const x = bars[j].c - bars[j - 1].c; if (x > 0) u += x; else d -= x } return d === 0 ? 100 : 100 - 100 / (1 + u / d) }

function computeScore(bars: Bar[], i: number) {
  const d = new Date(bars[i].ts), h = d.getUTCHours(), m = d.getUTCMinutes(), dow = d.getUTCDay()
  let w = 0, hW = 0, tW = 0
  const add = (dir: number, wt: number, hold: number) => { w += dir * wt; hW += hold * wt; tW += wt }
  if (dow === 4) add(-1, 20, 240)
  if (dow === 3) add(+1, 19.05, 240)
  if (dow === 0) add(+1, 15.05, 240)
  if (dow === 1) add(+1, 10.61, 240)
  if (dow === 5) add(-1, 6, 240)
  if (h === 22) add(+1, 2.86, 120)
  if (h === 21) add(+1, 17.9, 120)
  if (h === 20) add(+1, 9.49, 60)
  if (h === 23) add(-1, 8.94, 30)
  if (i >= 60) { const bp = avgCP(bars, i, 60); if (bp > 0.58) add(+1, 15.49, 240); if (bp < 0.42) add(-1, 8, 240) }
  if (i >= 60 && (h === 13 || (h === 14 && m <= 15)) && m <= 15) { const gap = lbRet(bars, i, 60); if (Math.abs(gap) > 5) add(gap > 0 ? -1 : +1, 15, 120) }
  if (i >= 60) { const rv = rsi(bars, i, 60); if (rv < 30) add(+1, 4.63, 120) }
  if (i >= 1440) { const dy = lbRet(bars, i, 1440); if (Math.abs(dy) > 30) add(dy > 0 ? -1 : +1, 5, 240) }
  return { score: w, hold: tW > 0 ? Math.max(60, Math.min(240, Math.round(hW / tW))) : 120 }
}

const SWING_N = 5, SWING_LOOKBACK = 480
function compositeLiquidityLevel(bars: Bar[], i: number, dir: 1 | -1, refPrice?: number) {
  const N = i + 1, price = refPrice ?? bars[i].c, candidates: number[] = []
  candidates.push(dir > 0 ? Math.floor(price / 1000) * 1000 : Math.ceil(price / 1000) * 1000)
  candidates.push(dir > 0 ? Math.floor(price / 500) * 500 : Math.ceil(price / 500) * 500)
  candidates.push(dir > 0 ? Math.floor(price / 250) * 250 : Math.ceil(price / 250) * 250)
  let bestSwing: number | null = null, bestDist = Infinity
  const scanEnd = N - SWING_N - 1, scanStart = Math.max(SWING_N, N - SWING_LOOKBACK - SWING_N)
  for (let j = scanEnd; j >= scanStart; j--) {
    let isSwing = true
    for (let k = 1; k <= SWING_N && isSwing; k++) {
      if (dir > 0) { if ((bars[j - k]?.l ?? Infinity) <= bars[j].l) isSwing = false; if ((bars[j + k]?.l ?? Infinity) <= bars[j].l) isSwing = false }
      else { if ((bars[j - k]?.h ?? 0) >= bars[j].h) isSwing = false; if ((bars[j + k]?.h ?? 0) >= bars[j].h) isSwing = false }
    }
    if (!isSwing) continue
    const lvl = dir > 0 ? bars[j].l : bars[j].h
    if (dir > 0 ? lvl >= price : lvl <= price) continue
    const dist = Math.abs(price - lvl)
    if (dist < bestDist) { bestDist = dist; bestSwing = lvl }
  }
  if (bestSwing !== null) candidates.push(bestSwing)
  const d = new Date(bars[N - 1].ts); d.setUTCHours(0, 0, 0, 0); const today = d.getTime()
  let pdH = 0, pdL = Infinity
  for (let j = N - 1; j >= 0; j--) {
    if (bars[j].ts >= today) continue
    if (bars[j].ts < today - 86400000) break
    if (bars[j].h > pdH) pdH = bars[j].h
    if (bars[j].l < pdL) pdL = bars[j].l
  }
  if (dir > 0 && pdL < price && pdL > 0) candidates.push(pdL)
  if (dir < 0 && pdH > price && pdH > 0) candidates.push(pdH)
  const volMap = new Map<number, number>()
  for (let j = N - 1; j >= 0 && bars[j].ts >= today; j--) { const bucket = Math.round(bars[j].c / 50) * 50; volMap.set(bucket, (volMap.get(bucket) ?? 0) + (bars[j].v ?? 0)) }
  let maxV = 0, poc = 0
  volMap.forEach((v, p) => { if (v > maxV) { maxV = v; poc = p } })
  if (poc > 0 && (dir > 0 ? poc < price : poc > price)) candidates.push(poc)
  const valid = candidates.filter(l => dir > 0 ? l <= price : l >= price)
  if (valid.length === 0) return dir > 0 ? Math.floor(price / 250) * 250 : Math.ceil(price / 250) * 250
  return valid.reduce((b, c) => Math.abs(price - c) < Math.abs(price - b) ? c : b)
}

function stopLiquidityLevel(bars: Bar[], i: number, dir: 1 | -1, refPrice: number) {
  const N = i + 1, price = refPrice
  const c = [dir > 0 ? Math.floor(price / 1000) * 1000 : Math.ceil(price / 1000) * 1000, dir > 0 ? Math.floor(price / 500) * 500 : Math.ceil(price / 500) * 500, dir > 0 ? Math.floor(price / 250) * 250 : Math.ceil(price / 250) * 250]
  const d = new Date(bars[N - 1].ts); d.setUTCHours(0, 0, 0, 0); const today = d.getTime()
  let pdH = 0, pdL = Infinity
  for (let j = N - 1; j >= 0; j--) { if (bars[j].ts >= today) continue; if (bars[j].ts < today - 86400000) break; if (bars[j].h > pdH) pdH = bars[j].h; if (bars[j].l < pdL) pdL = bars[j].l }
  if (dir > 0 && pdL < price && pdL > 0) c.push(pdL)
  if (dir < 0 && pdH > price && pdH > 0) c.push(pdH)
  const v = c.filter(l => dir > 0 ? l <= price : l >= price)
  if (v.length === 0) return dir > 0 ? Math.floor(price / 250) * 250 : Math.ceil(price / 250) * 250
  return v.reduce((b, x) => Math.abs(price - x) < Math.abs(price - b) ? x : b)
}

type SlCfg = {
  label: string
  mode: 'current' | 'hardOnly' | 'hardAny' | 'confirmSoft' | 'confirmClose' | 'graceClose' | 'scoreGraceClose'
  confirmBars?: number
  graceBars?: number
  bufferBps?: number
  /** Conservative sizing/risk adjustment for buffered L2 trigger. */
  riskBufferBps?: number
  /** Optional conservative P&L assumption: soft fill happens at buffered trigger, not L2 reclaim. */
  softFillBufferBps?: number
  softCooldownBars?: number
}

function simulate(bars: Bar[], slCfg: SlCfg): Trade[] {
  const cfg: SimCfg = { label: slCfg.label, longT: 16, shortT: 8, hardCapBars: 420, levelWaitBars: 30, immediateDistBps: 10 }
  const trades: Trade[] = [], stopCounts = new Map<string, number>()
  let cooldown = 0
  const EXT = 8, WAIT = 30, MIN_H = 60, MAX_H = 240, CAP = 420, CONFIRM = 4, SL_CD = 30, N = bars.length
  for (let i = 20160; i < N - WAIT; i++) {
    if (i < cooldown) continue
    const { score, hold } = computeScore(bars, i)
    if (score === 0) continue
    const dir: 1 | -1 = score > 0 ? 1 : -1
    const side = dir > 0 ? 'long' : 'short'
    const thresh = dir > 0 ? cfg.longT : cfg.shortT
    if (Math.abs(score) < thresh) continue
    let confirmed = true
    for (let k = 1; k < CONFIRM; k++) { const sk = computeScore(bars, i - k).score; if (sk * dir < thresh) { confirmed = false; break } }
    if (!confirmed) continue
    if (dir * lbRet(bars, i, 4320) < -600) continue
    if (dir * lbRet(bars, i, 10080) < -700) continue
    if (dir * lbRet(bars, i, 20160) < -800) continue
    const day = new Date(bars[i].ts).toISOString().slice(0, 10), ck = `${day}_${side}`
    if ((stopCounts.get(ck) ?? 0) >= 2) continue

    const target = compositeLiquidityLevel(bars, i, dir)
    const distBps = Math.abs(bars[i].c - target) / bars[i].c * 10000
    let entryPx = bars[i].c, entryBar = i, filled = distBps <= 10
    if (!filled) {
      for (let j = i + 1; j <= Math.min(N - 1, i + WAIT); j++) {
        const hit = dir > 0 ? bars[j].l <= target : bars[j].h >= target
        if (hit) { filled = true; entryBar = j; entryPx = target; break }
      }
    }
    if (!filled) continue

    const slRef = dir > 0 ? target - 0.01 : target + 0.01
    const slL2 = stopLiquidityLevel(bars, i, dir, slRef)
    const slBps = Math.abs(target - slL2) / target * 10000
    const isStruct = slBps >= 15 && slBps <= 200
    const softSlBps = isStruct ? slBps : 100
    const slPx = isStruct ? slL2 : (dir > 0 ? target * 0.99 : target * 1.01)
    const l3ref = dir > 0 ? slPx - 0.01 : slPx + 0.01
    const slL3 = stopLiquidityLevel(bars, i, dir, l3ref)
    const l3Gap = Math.max(10, Math.min(100, Math.abs(slPx - slL3) / slPx * 10000))
    const hardSlBps = isStruct ? softSlBps + l3Gap + 8 : 133
    const hardSlPx = isStruct ? (dir > 0 ? slPx * (1 - l3Gap / 10000) : slPx * (1 + l3Gap / 10000)) : (dir > 0 ? target * (1 - 1.25 / 100) : target * (1 + 1.25 / 100))
    const sizingBps = (isStruct ? softSlBps + l3Gap : 125) + (slCfg.riskBufferBps ?? 0)
    const softExitBps = softSlBps + (slCfg.softFillBufferBps ?? 0)

    const hardCap = i + CAP
    let deadline = i + Math.max(MIN_H, Math.min(MAX_H, hold))
    let exitBar = -1, isSl = false, isHardSl = false, gross = 0, reason = 'time'
    let breachCount = 0, armedAt = -1

    for (let j = entryBar + 1; j < Math.min(N, hardCap + 1); j++) {
      const hardTouched = dir > 0 ? bars[j].l <= hardSlPx : bars[j].h >= hardSlPx
      const bufferPx = slPx * ((slCfg.bufferBps ?? 0) / 10000)
      const softClosed = dir > 0 ? bars[j].c <= slPx - bufferPx : bars[j].c >= slPx + bufferPx
      const reclaimed = dir > 0 ? bars[j].c > slPx : bars[j].c < slPx

      if (slCfg.mode === 'current') {
        if (softClosed) {
          exitBar = j; isSl = true; isHardSl = hardTouched; reason = isHardSl ? 'hard_sl' : 'soft_sl'; gross = isHardSl ? -hardSlBps : -softExitBps; break
        }
      } else {
        // Real exchange L3 is live from entry. Any intrabar L3 touch exits.
        if (hardTouched) { exitBar = j; isSl = true; isHardSl = true; reason = 'hard_sl'; gross = -hardSlBps; break }

        if (slCfg.mode === 'hardOnly') {
          // No L2 soft stop at all. Let the trade live until L3 hard stop,
          // normal time/extension exit, or hard cap.
        } else if (slCfg.mode === 'hardAny') {
          if (softClosed) { exitBar = j; isSl = true; reason = 'soft_sl'; gross = -softExitBps; break }
        } else if (slCfg.mode === 'confirmSoft' || slCfg.mode === 'confirmClose') {
          if (softClosed) breachCount++; else breachCount = 0
          if (breachCount >= (slCfg.confirmBars ?? 2)) {
            exitBar = j; isSl = true; reason = slCfg.mode === 'confirmSoft' ? 'confirm_soft' : 'confirm_close'
            gross = slCfg.mode === 'confirmSoft' ? -softExitBps : dir * (bars[j].c - entryPx) / entryPx * 10000
            break
          }
        } else if (slCfg.mode === 'graceClose' || slCfg.mode === 'scoreGraceClose') {
          const scoreInDir = computeScore(bars, j).score * dir
          const allowGrace = slCfg.mode === 'graceClose' || scoreInDir >= EXT
          if (softClosed && allowGrace && armedAt < 0) armedAt = j
          if (softClosed && !allowGrace) {
            exitBar = j; isSl = true; reason = 'soft_sl'; gross = -softExitBps; break
          }
          if (armedAt >= 0) {
            if (reclaimed) { armedAt = -1 }
            else if (j - armedAt + 1 >= (slCfg.graceBars ?? 3)) {
              exitBar = j; isSl = true; reason = slCfg.mode === 'scoreGraceClose' ? 'score_grace_close' : 'grace_close'
              gross = dir * (bars[j].c - entryPx) / entryPx * 10000
              break
            }
          }
        }
      }

      if (j >= i + MIN_H) {
        const es = computeScore(bars, j)
        if (es.score * dir >= EXT) { const proposed = Math.min(j + Math.max(MIN_H, Math.min(MAX_H, es.hold)), hardCap); if (proposed > deadline) deadline = proposed }
      }
      if (j >= deadline) { exitBar = j; reason = 'time'; gross = dir * (bars[j].c - entryPx) / entryPx * 10000; break }
    }
    if (exitBar < 0) { exitBar = Math.min(N - 2, hardCap); reason = 'cap'; gross = dir * (bars[exitBar].c - entryPx) / entryPx * 10000 }

    const dt = new Date(bars[i].ts)
    trades.push({ gross, net: gross, reason, isSl, isHardSl, month: dt.toISOString().slice(0, 7), year: dt.getUTCFullYear(), side, bar: i, softSlBps, sizingBps, isStructL2: isStruct, isFallback: !isStruct, hour: dt.getUTCHours(), dow: dt.getUTCDay() })
    if (isSl) stopCounts.set(ck, (stopCounts.get(ck) ?? 0) + 1)
    cooldown = exitBar + (isSl ? (isHardSl ? SL_CD : (slCfg.softCooldownBars ?? SL_CD)) : 5)
  }
  return trades
}

function withFee(tr: Trade[], fee: number): Trade[] { return tr.map(t => ({ ...t, net: t.gross - fee })) }
function stats(v: number[]) { const n = v.length; if (n < 3) return { n, mean: 0, sd: 0, t: 0, wr: 0 }; const mean = v.reduce((a, b) => a + b, 0) / n; const sd = Math.sqrt(v.reduce((s, x) => s + (x - mean) ** 2, 0) / (n - 1)); return { n, mean, sd, t: sd > 0 ? mean / (sd / Math.sqrt(n)) : 0, wr: v.filter(x => x > 0).length / n } }
function sig(t: number) { const a = Math.abs(t); return a > 3.89 ? '****' : a > 3.29 ? '***' : a > 2.58 ? '**' : a > 1.96 ? '*' : '' }
function monthly(tr: Trade[], fixed = 1500) { const m: Record<string, number> = {}; for (const t of tr) m[t.month] = (m[t.month] ?? 0) + fixed * t.net / 10000; return m }
function actualMonthly(tr: Trade[], acct = 10000, risk = 0.03) { const m: Record<string, number> = {}; for (const t of tr) { const notional = acct * risk / (t.sizingBps / 10000); m[t.month] = (m[t.month] ?? 0) + notional * t.net / 10000 } return m }
function ir(vals: number[]) { if (vals.length < 3) return 0; const s = stats(vals); return s.sd > 0 ? s.mean / s.sd : 0 }
function moIR(tr: Trade[]) { return ir(Object.values(monthly(tr))) }
function actualIR(tr: Trade[]) { return ir(Object.values(actualMonthly(tr))) }
function maxDD(tr: Trade[], risk = 0.03) { let eq = 500, pk = 500, dd = 0; for (const t of tr) { eq += eq * risk / (t.sizingBps / 10000) * t.net / 10000; if (eq > pk) pk = eq; dd = Math.max(dd, (pk - eq) / pk) } return dd }
function pairedMaps(a: Record<string, number>, b: Record<string, number>) { const months = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort(); const diffs = months.map(m => (b[m] ?? 0) - (a[m] ?? 0)); const s = stats(diffs); return { n: diffs.length, imp: diffs.filter(x => x > 0).length, mean: s.mean, t: s.t, total: diffs.reduce((x,y)=>x+y,0) } }
function minLeaveOneOutMoIR(tr: Trade[]) { const years = [...new Set(tr.map(t=>t.year))]; let mn=Infinity; for (const y of years) mn = Math.min(mn, moIR(tr.filter(t=>t.year!==y))); return mn }
function metric(label: string, tr: Trade[]) {
  const s = stats(tr.map(t => t.net)), mp = Object.values(monthly(tr)); const sl=tr.filter(t=>t.isSl), hard=tr.filter(t=>t.isHardSl)
  return { label, n:s.n, mean:s.mean, t:s.t, wr:s.wr, moIR:moIR(tr), actIR:actualIR(tr), maxDD:maxDD(tr), mp:mp.filter(x=>x>0).length, mt:mp.length, slRate: tr.length?sl.length/tr.length:0, hardRate: sl.length?hard.length/sl.length:0, minLOO:minLeaveOneOutMoIR(tr) }
}
function fmt(m: ReturnType<typeof metric>, ref?: Trade[], tr?: Trade[]) {
  let p = ''
  if (ref && tr) { const pa=pairedMaps(actualMonthly(ref), actualMonthly(tr)); p=` actualΔ=$${pa.mean.toFixed(0)}(${pa.imp}/${pa.n},t=${pa.t.toFixed(2)})` }
  return `${m.label.padEnd(24)} n=${String(m.n).padStart(4)} mean=${m.mean.toFixed(2).padStart(6)} t=${m.t.toFixed(2)}${sig(m.t).padEnd(4)} WR=${(m.wr*100).toFixed(1)}% moIR=${m.moIR.toFixed(2)} actIR=${m.actIR.toFixed(2)} maxDD=${(m.maxDD*100).toFixed(1)}% m+=${m.mp}/${m.mt} SL=${(m.slRate*100).toFixed(1)}% hard=${(m.hardRate*100).toFixed(1)}% minLOO=${m.minLOO.toFixed(2)}${p}`
}
function reasonCounts(tr: Trade[]) { const m=new Map<string,number>(); for (const t of tr) m.set(t.reason,(m.get(t.reason)??0)+1); return [...m.entries()].sort((a,b)=>b[1]-a[1]).map(([k,v])=>`${k}:${v}`).join(' ') }

function addMonths(ms: number, n: number) { const d = new Date(ms); d.setUTCMonth(d.getUTCMonth() + n); return d.getTime() }
function monthStr(ms: number) { return new Date(ms).toISOString().slice(0,7) }
function inWindow(bars: Bar[], t: Trade, start: number, end: number) { const ts = bars[t.bar].ts; return ts >= start && ts < end }
function mergeTrades(parts: Trade[][]) { return parts.flat().sort((a,b)=>a.bar-b.bar) }
function actualTotal(tr: Trade[]) { return Object.values(actualMonthly(tr)).reduce((a,b)=>a+b,0) }
function scoreMetric(m: ReturnType<typeof metric>) { return m.moIR + 0.35*m.actIR - 0.60*m.maxDD }
function halfMetrics(tr: Trade[], bars: Bar[]) {
  const cut = Date.parse('2024-04-01T00:00:00Z')
  return [metric('pre', tr.filter(t => bars[t.bar].ts < cut)), metric('post', tr.filter(t => bars[t.bar].ts >= cut))]
}
function cfgKey(c: SlCfg) { return c.label }
function select(rows: {idx:number; cfg:SlCfg; tr:Trade[]; m:ReturnType<typeof metric>}[], objective:'moIR'|'actIR'|'score') {
  const viable = rows.filter(r => r.m.n >= 120 && r.m.mt >= 6)
  const pool = viable.length ? viable : rows
  return pool.slice().sort((a,b)=>{
    const av = objective === 'score' ? scoreMetric(a.m) : a.m[objective]
    const bv = objective === 'score' ? scoreMetric(b.m) : b.m[objective]
    if (bv !== av) return bv - av
    if (a.m.maxDD !== b.m.maxDD) return a.m.maxDD - b.m.maxDD
    return a.idx - b.idx
  })[0]
}
function pairedLine(ref:Trade[], tr:Trade[]) {
  const pf = pairedMaps(monthly(ref), monthly(tr))
  const pa = pairedMaps(actualMonthly(ref), actualMonthly(tr))
  return `fixedΔ=$${pf.mean.toFixed(1)}(${pf.imp}/${pf.n},t=${pf.t.toFixed(2)}) actualΔ=$${pa.mean.toFixed(0)}(${pa.imp}/${pa.n},t=${pa.t.toFixed(2)})`
}
function compactLine(label:string, tr:Trade[], ref?:Trade[]) {
  const m = metric(label, tr)
  const p = ref ? ' ' + pairedLine(ref, tr) + ` actualTotal=$${actualTotal(tr).toFixed(0)}` : ''
  return `${label.padEnd(18)} n=${String(m.n).padStart(4)} mean=${m.mean.toFixed(2).padStart(6)} t=${m.t.toFixed(2)}${sig(m.t).padEnd(4)} WR=${(m.wr*100).toFixed(1)}% moIR=${m.moIR.toFixed(2)} actIR=${m.actIR.toFixed(2)} maxDD=${(m.maxDD*100).toFixed(1)}% m+=${m.mp}/${m.mt} SL=${(m.slRate*100).toFixed(1)}% hard=${(m.hardRate*100).toFixed(1)}% minLOO=${m.minLOO.toFixed(2)}${p}`
}
function wfRun(args:{bars:Bar[]; sims:{cfg:SlCfg; trades:Trade[]}[]; fee:number; lookbackMonths:number|'expanding'; testMonths:number; objective:'moIR'|'actIR'|'score'; startMonth:string; endMonth:string}) {
  const {bars,sims,fee,lookbackMonths,testMonths,objective}=args
  const start = Date.parse(args.startMonth+'-01T00:00:00Z')
  const end = addMonths(Date.parse(args.endMonth+'-01T00:00:00Z'),1)
  let testStart = start
  const selectedParts: Trade[][] = []
  const fixedParts: Record<string,Trade[][]> = Object.fromEntries(sims.map(s=>[cfgKey(s.cfg), []]))
  const folds:any[] = []
  while (addMonths(testStart,testMonths) <= end) {
    const testEnd = addMonths(testStart,testMonths)
    const trainEnd = testStart
    const trainStart = lookbackMonths === 'expanding' ? Date.parse('2022-04-01T00:00:00Z') : addMonths(trainEnd, -lookbackMonths)
    if (trainStart < Date.parse('2022-04-01T00:00:00Z')) { testStart = addMonths(testStart,testMonths); continue }
    const rows = sims.map((s,idx)=>{ const tr=withFee(s.trades.filter(t=>inWindow(bars,t,trainStart,trainEnd)),fee); return {idx,cfg:s.cfg,tr,m:metric('',tr)} })
    const chosen = select(rows,objective)
    const selTest = withFee(sims[chosen.idx].trades.filter(t=>inWindow(bars,t,testStart,testEnd)),fee)
    selectedParts.push(selTest)
    for (const s of sims) fixedParts[cfgKey(s.cfg)].push(withFee(s.trades.filter(t=>inWindow(bars,t,testStart,testEnd)),fee))
    folds.push({test:monthStr(testStart)+'..'+monthStr(addMonths(testEnd,-1)), train:monthStr(trainStart)+'..'+monthStr(addMonths(trainEnd,-1)), chosen:cfgKey(chosen.cfg), chosenM:chosen.m, selTest, selM:metric('',selTest)})
    testStart = addMonths(testStart,testMonths)
  }
  const fixed = Object.fromEntries(Object.entries(fixedParts).map(([k,v])=>[k,mergeTrades(v)]))
  return {folds, selected:mergeTrades(selectedParts), fixed}
}
function printSummary(label:string, res:ReturnType<typeof wfRun>, currentKey:string) {
  const ref = res.fixed[currentKey]
  const counts:Record<string,number> = {}
  for (const f of res.folds) counts[f.chosen] = (counts[f.chosen] ?? 0) + 1
  console.log('\n'+label)
  console.log('folds='+res.folds.length+' chosen='+JSON.stringify(counts))
  console.log(compactLine('WF-select', res.selected, ref))
  for (const [k,tr] of Object.entries(res.fixed)) console.log(compactLine(k, tr as Trade[], ref))
  const foldWins:Record<string,number> = {}
  for (const [k] of Object.entries(res.fixed)) foldWins[k] = 0
  let wfWins = 0
  for (const f of res.folds) {
    const s = f.test.slice(0,7), e = f.test.slice(-7)
    const cur = ref.filter(t=>t.month>=s&&t.month<=e)
    const curTot = actualTotal(cur)
    if (actualTotal(f.selTest) > curTot) wfWins++
    for (const [k,tr] of Object.entries(res.fixed)) {
      if (actualTotal((tr as Trade[]).filter(t=>t.month>=s&&t.month<=e)) > curTot) foldWins[k]++
    }
  }
  console.log('fold actual wins vs current: WF='+wfWins+'/'+res.folds.length+' fixed='+JSON.stringify(foldWins))
}
function sideStats(tr: Trade[]) {
  const l=tr.filter(t=>t.side==='long'), s=tr.filter(t=>t.side==='short')
  return `L=${l.length}/${stats(l.map(t=>t.net)).mean.toFixed(1)} S=${s.length}/${stats(s.map(t=>t.net)).mean.toFixed(1)} fallback=${tr.filter(t=>t.isFallback).length}`
}
function yearlyLines(label:string, tr:Trade[]) {
  console.log('\nYEARLY '+label)
  for (const y of [...new Set(tr.map(t=>t.year))].sort()) console.log('  '+y+' '+compactLine('', tr.filter(t=>t.year===y)))
}

;(async () => {
  console.log('Loading bars...')
  const bars = await loadBars(['data/klines/BTCUSDT-1m-2022-2025.jsonl','data/klines/BTCUSDT-1m-2022-2025b.jsonl','data/klines/BTCUSDT-1m.jsonl'])
  console.log(`${bars.length.toLocaleString()} bars (${new Date(bars[0].ts).toISOString().slice(0,10)} → ${new Date(bars.at(-1)!.ts).toISOString().slice(0,10)})`)

  const cfgs: SlCfg[] = [
    { label:'current', mode:'current' },
    { label:'buf8 risk+8', mode:'hardAny', bufferBps:8, riskBufferBps:8 },
    { label:'buf10 risk+10', mode:'hardAny', bufferBps:10, riskBufferBps:10 },
    { label:'buf12 risk+12', mode:'hardAny', bufferBps:12, riskBufferBps:12 },
    { label:'buf15 risk+15', mode:'hardAny', bufferBps:15, riskBufferBps:15 },
    { label:'buf10 risk+fill', mode:'hardAny', bufferBps:10, riskBufferBps:10, softFillBufferBps:10 },
  ]
  const currentKey = 'current'
  const sims = cfgs.map(cfg => ({ cfg, trades: simulate(bars, cfg) }))
  console.log('Simulated configs: '+cfgs.map(c=>c.label).join(' | '))

  for (const fee of [0,4]) {
    console.log(`\n================ SOFT-SL BUFFER WALK-FORWARD feeRT=${fee} ================`)
    const ref = withFee(sims[0].trades, fee)
    console.log('\nFULL FIXED GRID')
    for (const sim of sims) {
      const tr = withFee(sim.trades, fee)
      const [pre,post] = halfMetrics(tr,bars)
      console.log(`${compactLine(sim.cfg.label, tr, sim.cfg.label===currentKey?undefined:ref)} pre/post=${pre.moIR.toFixed(2)}/${post.moIR.toFixed(2)} ${sideStats(tr)} reasons=${reasonCounts(tr)}`)
    }
    for (const key of ['current','buf10 risk+10','buf12 risk+12','buf10 risk+fill']) {
      const sim = sims.find(s=>s.cfg.label===key)!
      yearlyLines(`${key} feeRT=${fee}`, withFee(sim.trades, fee))
    }
    for (const objective of ['moIR','actIR','score'] as const) {
      for (const lookbackMonths of [12,18,24,'expanding'] as const) {
        for (const testMonths of [1,3]) {
          const startMonth = lookbackMonths === 'expanding' ? '2023-01' : monthStr(addMonths(Date.parse('2022-04-01T00:00:00Z'), lookbackMonths))
          const res = wfRun({bars,sims,fee,lookbackMonths,testMonths,objective,startMonth,endMonth:'2026-03'})
          printSummary(`objective=${objective} train=${lookbackMonths} test=${testMonths}m`,res,currentKey)
        }
      }
    }
  }
})().catch(e=>{console.error(e);process.exit(1)})
