// @ts-nocheck
/**
 * Formal short-threshold validation with LONG_ENTRY_THRESH=16 fixed.
 *
 * 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
}

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 shorts = Array.from({ length: 21 }, (_, i) => 4 + i)
  const prodBase = { label: 'L10/S8', longT: 10, shortT: 8 }
  const sims = [
    { cfg: prodBase, trades: simulate(bars, prodBase) },
    ...shorts.map(s => ({ cfg: { label: `L16/S${s}`, longT: 16, shortT: s }, trades: simulate(bars, { label: `L16/S${s}`, longT: 16, shortT: s }) }))
  ]
  const l16s8 = sims.find(s => s.cfg.label === 'L16/S8')!
  const prod = sims[0]

  function sideCounts(tr: Trade[]) {
    const long = tr.filter(t => t.side === 'long')
    const short = tr.filter(t => t.side === 'short')
    return { long: long.length, short: short.length, longMean: stats(long.map(t=>t.net)).mean, shortMean: stats(short.map(t=>t.net)).mean }
  }
  function pLine(label:string, ref:Trade[], tr:Trade[]) {
    const pf=pairedMaps(monthly(ref), monthly(tr)); const pa=pairedMaps(actualMonthly(ref), actualMonthly(tr))
    return `${label} 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 row(s:any, fee:number, refProd:Trade[], refL16:Trade[]) {
    const tr=withFee(s.trades, fee), m=metric(s.cfg.label,tr), side=sideCounts(tr), [pre,post]=halfMetrics(tr,bars)
    return { s, tr, m, side, pre, post, minLoo:minLeaveOneOutMoIR(tr), vsProd:pLine('vsProd',refProd,tr), vsL16:pLine('vsL16S8',refL16,tr) }
  }

  for (const fee of [0,4]) {
    console.log(`\n================ SHORT THRESHOLD SWEEP with L16 fixed feeRT=${fee} ================`)
    const refProd=withFee(prod.trades,fee), refL16=withFee(l16s8.trades,fee)
    const rows=sims.map(s=>row(s,fee,refProd,refL16))
    console.log('FULL METRICS')
    for (const r of rows) {
      console.log(`${fmt(r.m)} L=${r.side.long} S=${r.side.short} shortMean=${r.side.shortMean.toFixed(2)} minLOO=${r.minLoo.toFixed(2)} pre/post=${r.pre.moIR.toFixed(2)}/${r.post.moIR.toFixed(2)} ${r.vsProd} ${r.vsL16}`)
    }
    console.log('\nTop by moIR')
    rows.slice().sort((a,b)=>b.m.moIR-a.m.moIR).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)} S=${r.side.short} ${r.vsL16}`))
    console.log('\nTop by actualIR')
    rows.slice().sort((a,b)=>b.m.actIR-a.m.actIR).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)} S=${r.side.short} ${r.vsL16}`))
    console.log('\nLowest maxDD')
    rows.slice().sort((a,b)=>a.m.maxDD-b.m.maxDD).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)} S=${r.side.short} ${r.vsL16}`))

    console.log('\nYEARLY DETAILS key short thresholds')
    for (const key of ['L16/S4','L16/S6','L16/S8','L16/S10','L16/S12','L16/S16','L16/S20','L16/S24']) {
      const sim=sims.find(x=>x.cfg.label===key); if(!sim) continue
      yearlyRows(key, withFee(sim.trades,fee))
    }
  }
}

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