// @ts-nocheck
/**
 * Focused combined candidate validation for L16 variants.
 *
 * 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
  isSl: boolean
  isHardSl: boolean
}

type SimCfg = {
  label: string
  longT: number
  shortT: number
  hardCapBars?: number
  longCooldownBars?: number
  shortCooldownBars?: 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 = 20, MIN_H = 60, MAX_H = 240, CAP = cfg.hardCapBars ?? 360, 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
    const hardCap = i + 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
    trades.push({ gross, net: gross, isSl, isHardSl, month: new Date(bars[i].ts).toISOString().slice(0, 7), year: new Date(bars[i].ts).getUTCFullYear(), side, bar: i, softSlBps, sizingBps })
    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)))}`) }

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 configs: SimCfg[] = []
  for (const shortT of [8, 9]) {
    for (const cap of [360, 420]) {
      for (const longCd of [30, 60]) {
        for (const shortCd of [15, 30]) {
          configs.push({
            label: `S${shortT} cap${cap} cdL${longCd}/S${shortCd}`,
            longT: 16,
            shortT,
            hardCapBars: cap,
            longCooldownBars: longCd,
            shortCooldownBars: shortCd,
          })
        }
      }
    }
  }
  const current: SimCfg = { label: 'CURRENT L16/S8 cap360 cd30/30', longT: 16, shortT: 8, hardCapBars: 360, longCooldownBars: 30, shortCooldownBars: 30 }
  const oldProd: SimCfg = { label: 'OLD L10/S8 cap360 cd30/30', longT: 10, shortT: 8, hardCapBars: 360, longCooldownBars: 30, shortCooldownBars: 30 }
  const allCfg = [oldProd, current, ...configs.filter(c => c.label !== 'S8 cap360 cdL30/S30')]

  console.log(`Simulating ${allCfg.length} configs...`)
  const sims = allCfg.map(cfg => ({ cfg, trades: simulate(bars, cfg) }))
  const curSim = sims.find(s => s.cfg.label === current.label)!
  const oldSim = sims.find(s => s.cfg.label === oldProd.label)!

  function sideCounts(tr: Trade[]) {
    const l=tr.filter(t=>t.side==='long'), sh=tr.filter(t=>t.side==='short')
    return {l:l.length,s:sh.length,lm:stats(l.map(t=>t.net)).mean,sm:stats(sh.map(t=>t.net)).mean}
  }
  function pLine(name:string, ref:Trade[], tr:Trade[]) {
    const pf=pairedMaps(monthly(ref), monthly(tr)); const pa=pairedMaps(actualMonthly(ref), actualMonthly(tr))
    return `${name} 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 actualDelta(ref:Trade[], tr:Trade[]) { return pairedMaps(actualMonthly(ref), actualMonthly(tr)).mean }
  function row(sim:any, fee:number, refCur:Trade[], refOld:Trade[]) {
    const tr=withFee(sim.trades, fee), m=metric(sim.cfg.label,tr), side=sideCounts(tr), [pre,post]=halfMetrics(tr,bars)
    return {sim,tr,m,side,pre,post,minLoo:minLeaveOneOutMoIR(tr), vsCur:pLine('vsCurrent',refCur,tr), vsOld:pLine('vsOld',refOld,tr), actualDeltaCur:actualDelta(refCur,tr)}
  }

  for (const fee of [0,4]) {
    console.log(`\n================ COMBINED GRID feeRT=${fee} ================`)
    const refCur=withFee(curSim.trades,fee), refOld=withFee(oldSim.trades,fee)
    const rows=sims.map(s=>row(s,fee,refCur,refOld))

    console.log('\nFULL GRID')
    for (const r of rows) {
      console.log(`${fmt(r.m)} L=${r.side.l} S=${r.side.s} minLOO=${r.minLoo.toFixed(2)} pre/post=${r.pre.moIR.toFixed(2)}/${r.post.moIR.toFixed(2)} ${r.vsCur} ${r.vsOld}`)
    }

    function printRank(title:string, sortFn:(a:any,b:any)=>number) {
      console.log('\n'+title)
      rows.slice().sort(sortFn).slice(0,10).forEach(r => {
        console.log(`${r.sim.cfg.label}: moIR=${r.m.moIR.toFixed(2)} actIR=${r.m.actIR.toFixed(2)} maxDD=${(r.m.maxDD*100).toFixed(1)}% m+=${r.m.mp}/${r.m.mt} minLOO=${r.minLoo.toFixed(2)} ${r.vsCur}`)
      })
    }
    printRank('Top by moIR', (a,b)=>b.m.moIR-a.m.moIR)
    printRank('Top by actualIR', (a,b)=>b.m.actIR-a.m.actIR)
    printRank('Lowest maxDD', (a,b)=>a.m.maxDD-b.m.maxDD)
    printRank('Best actual monthly delta vs current', (a,b)=>b.actualDeltaCur-a.actualDeltaCur)

    console.log('\nYEARLY TOP 5 BY MOIR')
    for (const r of rows.slice().sort((a,b)=>b.m.moIR-a.m.moIR).slice(0,5)) {
      yearlyRows(r.sim.cfg.label, r.tr)
    }
  }
}

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