// @ts-nocheck
/**
 * Next-candidate validation: side-specific caps, entry execution, and risk overlays.
 *
 * 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)
}

function simulate(bars: Bar[], cfg: SimCfg): Trade[] {
  const trades: Trade[] = [], stopCounts = new Map<string, number>()
  let cooldown = 0
  const EXT = 8, WAIT = cfg.levelWaitBars ?? 20, MIN_H = 60, MAX_H = 240, CAP = cfg.hardCapBars ?? 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 <= (cfg.immediateDistBps ?? 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
    const hardCap = i + (side === 'long' ? (cfg.longCapBars ?? CAP) : (cfg.shortCapBars ?? CAP))
    let deadline = i + Math.max(MIN_H, Math.min(MAX_H, hold)), exitBar = -1, isSl = false, isHardSl = false
    for (let j = entryBar + 1; j < Math.min(N, hardCap + 1); j++) {
      if (dir > 0 ? bars[j].c <= slPx : bars[j].c >= slPx) { exitBar = j; isSl = true; isHardSl = dir > 0 ? bars[j].l <= hardSlPx : bars[j].h >= hardSlPx; 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; break }
    }
    if (exitBar < 0) exitBar = Math.min(N - 2, hardCap)
    const gross = isSl ? (isHardSl ? -hardSlBps : -softSlBps) : dir * (bars[exitBar].c - entryPx) / entryPx * 10000
    const dt = new Date(bars[i].ts)
    trades.push({ gross, net: gross, 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 ? 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 metric(label: string, tr: Trade[]) { const s = stats(tr.map(t => t.net)); const 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 } }
function fmt(m: ReturnType<typeof metric>) { return `${m.label.padEnd(10)} 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}` }
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 subset(tr: Trade[], pred: (t: Trade) => boolean) { return tr.filter(pred) }
function yearsOf(tr: Trade[]) { return [...new Set(tr.map(t => t.year))].sort() }
function minLeaveOneOutMoIR(tr: Trade[]) { let mn = Infinity; for (const y of yearsOf(tr)) mn = Math.min(mn, moIR(tr.filter(t => t.year !== y))); return mn }
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 yearlyRows(label: string, tr: Trade[]) { console.log(`\n${label}`); for (const y of yearsOf(tr)) console.log(`  ${y} ${fmt(metric('', tr.filter(t => t.year === y)))}`) }

type RiskFn = (t: Trade, ctx: { eq: number; peak: number; dd: number; monthPnl: number; tradeIndex: number }) => number

function riskFixed(pct: number): RiskFn { return () => pct }
function riskDdThrottle(): RiskFn {
  return (_t, ctx) => {
    const dd = ctx.dd
    if (dd >= 0.30) return 0.005
    if (dd >= 0.20) return 0.010
    if (dd >= 0.10) return 0.020
    return 0.030
  }
}
function riskSmoothDd(): RiskFn { return (_t, ctx) => Math.max(0.010, 0.03 * (1 - ctx.dd / 0.35)) }
function riskStopWidthScale(): RiskFn {
  return (t) => {
    if (t.sizingBps < 50) return 0.015
    if (t.sizingBps < 75) return 0.020
    if (t.sizingBps <= 150) return 0.030
    return 0.020
  }
}
function riskFallbackReduced(): RiskFn { return (t) => t.isFallback ? 0.015 : 0.030 }
function riskFallbackAndWidth(): RiskFn {
  const w = riskStopWidthScale()
  return (t, ctx) => Math.min(t.isFallback ? 0.020 : 0.030, w(t, ctx))
}
function riskWeekendReduced(): RiskFn { return (t) => t.dow === 6 ? 0.015 : 0.030 }
function riskBadHourReduced(): RiskFn { const bad = new Set([4,9,16,17,22]); return (t) => bad.has(t.hour) ? 0.015 : 0.030 }
function riskComboConservative(): RiskFn {
  const dd = riskDdThrottle(), fw = riskFallbackAndWidth(), wk = riskWeekendReduced(), bh = riskBadHourReduced()
  return (t, ctx) => Math.min(dd(t,ctx), fw(t,ctx), wk(t,ctx), bh(t,ctx))
}

function compoundEval(label: string, trades: Trade[], riskFn: RiskFn, startEq = 10000) {
  let eq = startEq, peak = startEq, maxDd = 0
  let month = '', monthStart = startEq, monthPnl = 0
  const monthReturns: Record<string, number> = {}
  const riskVals: number[] = []
  for (let idx=0; idx<trades.length; idx++) {
    const t = trades[idx]
    if (t.month !== month) {
      if (month) monthReturns[month] = (eq - monthStart) / monthStart
      month = t.month; monthStart = eq; monthPnl = 0
    }
    const dd = peak > 0 ? (peak - eq) / peak : 0
    const risk = Math.max(0, riskFn(t, { eq, peak, dd, monthPnl, tradeIndex: idx }))
    riskVals.push(risk)
    const notional = eq * risk / (t.sizingBps / 10000)
    const pnl = notional * t.net / 10000
    eq += pnl
    monthPnl += pnl
    if (eq > peak) peak = eq
    maxDd = Math.max(maxDd, (peak - eq) / peak)
  }
  if (month) monthReturns[month] = (eq - monthStart) / monthStart
  const vals = Object.values(monthReturns)
  const m = vals.length ? vals.reduce((a,b)=>a+b,0)/vals.length : 0
  const sd = vals.length > 2 ? Math.sqrt(vals.reduce((a,b)=>a+(b-m)**2,0)/(vals.length-1)) : 0
  const s = stats(trades.map(t=>t.net))
  return { label, n: trades.length, mean:s.mean, t:s.t, wr:s.wr, endEq:eq, ret:eq/startEq-1, maxDD:maxDd, moIR:sd>0?m/sd:0, mpos:vals.filter(x=>x>0).length, mt:vals.length, avgRisk:riskVals.reduce((a,b)=>a+b,0)/Math.max(1,riskVals.length) }
}
function fmtComp(x: ReturnType<typeof compoundEval>) {
  return `${x.label.padEnd(22)} n=${String(x.n).padStart(4)} mean=${x.mean.toFixed(2).padStart(6)} t=${x.t.toFixed(2)}${sig(x.t).padEnd(4)} end=$${x.endEq.toFixed(0)} ret=${(x.ret*100).toFixed(0)}% moIR=${x.moIR.toFixed(2)} maxDD=${(x.maxDD*100).toFixed(1)}% m+=${x.mpos}/${x.mt} avgRisk=${(x.avgRisk*100).toFixed(2)}%`
}
function pairedActual(ref: Trade[], tr: Trade[]) { return pairedMaps(actualMonthly(ref), actualMonthly(tr)) }
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}`
}

async function main() {
  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[bars.length-1].ts).toISOString().slice(0,10)})`)

  const strategyCfgs: SimCfg[] = [
    { label: 'current L16/S8 cap420 wait20 imm10', longT:16, shortT:8, hardCapBars:420, levelWaitBars:20, immediateDistBps:10 },
    { label: 'sideCap L360/S420', longT:16, shortT:8, longCapBars:360, shortCapBars:420, levelWaitBars:20, immediateDistBps:10 },
    { label: 'sideCap L420/S480', longT:16, shortT:8, longCapBars:420, shortCapBars:480, levelWaitBars:20, immediateDistBps:10 },
    { label: 'sideCap L360/S480', longT:16, shortT:8, longCapBars:360, shortCapBars:480, levelWaitBars:20, immediateDistBps:10 },
    { label: 'S9 cap420', longT:16, shortT:9, hardCapBars:420, levelWaitBars:20, immediateDistBps:10 },
    { label: 'wait10 imm10', longT:16, shortT:8, hardCapBars:420, levelWaitBars:10, immediateDistBps:10 },
    { label: 'wait15 imm10', longT:16, shortT:8, hardCapBars:420, levelWaitBars:15, immediateDistBps:10 },
    { label: 'wait30 imm10', longT:16, shortT:8, hardCapBars:420, levelWaitBars:30, immediateDistBps:10 },
    { label: 'wait20 imm0', longT:16, shortT:8, hardCapBars:420, levelWaitBars:20, immediateDistBps:0 },
    { label: 'wait20 imm5', longT:16, shortT:8, hardCapBars:420, levelWaitBars:20, immediateDistBps:5 },
    { label: 'wait20 imm15', longT:16, shortT:8, hardCapBars:420, levelWaitBars:20, immediateDistBps:15 },
    { label: 'wait20 imm20', longT:16, shortT:8, hardCapBars:420, levelWaitBars:20, immediateDistBps:20 },
  ]
  const sims = strategyCfgs.map(cfg => ({ cfg, trades: simulate(bars, cfg) }))
  const cur = sims[0]

  for (const fee of [0,4]) {
    console.log(`\n================ NEXT STRATEGY CANDIDATES feeRT=${fee} ================`)
    const ref = withFee(cur.trades, fee)
    const rows = sims.map(s => { const tr=withFee(s.trades,fee), m=metric(s.cfg.label,tr), pa=pairedActual(ref,tr), [pre,post]=halfMetrics(tr,bars); return {s,tr,m,pa,pre,post,minLoo:minLeaveOneOutMoIR(tr)} })
    console.log('\nFULL STRATEGY/EXECUTION GRID')
    for (const r of rows) console.log(`${fmt(r.m)} minLOO=${r.minLoo.toFixed(2)} pre/post=${r.pre.moIR.toFixed(2)}/${r.post.moIR.toFixed(2)} actualΔ=$${r.pa.mean.toFixed(0)}(${r.pa.imp}/${r.pa.n},t=${r.pa.t.toFixed(2)}) ${sideStats(r.tr)}`)
    const rank = (name:string, arr:any[], sort:any) => { console.log('\n'+name); arr.slice().sort(sort).slice(0,8).forEach(r=>console.log(`${r.s.cfg.label}: moIR=${r.m.moIR.toFixed(2)} actIR=${r.m.actIR.toFixed(2)} maxDD=${(r.m.maxDD*100).toFixed(1)}% minLOO=${r.minLoo.toFixed(2)} actualΔ=$${r.pa.mean.toFixed(0)} t=${r.pa.t.toFixed(2)}`)) }
    rank('Top by moIR', rows, (a:any,b:any)=>b.m.moIR-a.m.moIR)
    rank('Top by actualIR', rows, (a:any,b:any)=>b.m.actIR-a.m.actIR)
    rank('Lowest maxDD', rows, (a:any,b:any)=>a.m.maxDD-b.m.maxDD)
    rank('Best actual monthly delta vs current', rows, (a:any,b:any)=>b.pa.mean-a.pa.mean)

    console.log('\nRISK OVERLAYS ON CURRENT TRADE SET')
    const riskProfiles: [string,RiskFn][] = [
      ['fixed 3%', riskFixed(0.03)],
      ['DD throttle', riskDdThrottle()],
      ['smooth DD', riskSmoothDd()],
      ['stop width scale', riskStopWidthScale()],
      ['fallback 1.5%', riskFallbackReduced()],
      ['fallback+width', riskFallbackAndWidth()],
      ['Saturday 1.5%', riskWeekendReduced()],
      ['bad-hour 1.5%', riskBadHourReduced()],
      ['combo conservative', riskComboConservative()],
    ]
    for (const [name,fn] of riskProfiles) console.log(fmtComp(compoundEval(name, ref, fn)))

    console.log('\nRISK OVERLAYS ON BEST SIDE-CAP/EXEC CANDIDATES')
    for (const candidate of ['sideCap L360/S420','sideCap L420/S480','S9 cap420','wait10 imm10']) {
      const sim = rows.find(r=>r.s.cfg.label===candidate)
      if (!sim) continue
      console.log(`\n${candidate}`)
      for (const [name,fn] of riskProfiles.slice(0,6)) console.log(fmtComp(compoundEval(name, sim.tr, fn)))
    }
  }
}

main().catch(e => { console.error(e); process.exit(1) })
