/**
 * 4-year backtest: SMOOTH config on BTCUSDT-1m, Apr 2022 → Apr 2026.
 *
 * Loads all kline files in order, deduplicates by timestamp, runs the
 * SMOOTH parameter set (matched exactly to combined-demo.ts):
 *
 *   longEntry  = 10
 *   shortEntry = 8
 *   extThresh  = 10
 *   hold       = 60-240 min
 *   stop       = 100 bps (soft SL trigger — hard SL not modelled here)
 *   stopCap    = 2 same-direction stops/day
 *   fee        = 0 bps (MEXC maker)     ← optimistic baseline
 *   fee        = 4 bps (Bybit maker)    ← also shown
 *
 * Outputs:
 *   - Full-period aggregate stats
 *   - Monthly breakdown (all ~48 months)
 *   - Annual breakdown (4 years)
 *   - Walk-forward: 4 one-year folds, train on prior years, test on next
 *   - Half-split robustness (H1 vs H2)
 *   - Side stats (long vs short)
 *   - Stop cap impact (with vs without)
 */
import fs   from 'node:fs'
import path from 'node:path'
import rl   from 'node:readline'

interface Bar { ts: number; o: number; h: number; l: number; c: number; v: number }
interface Trade {
  entryBar: number; month: string; year: string
  net: number; stopped: boolean; side: 'long'|'short'; entryScore: number
}

// ── Load & merge all kline files ─────────────────────────────────────────

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

// ── Signal helpers (identical to combined-demo.ts) ───────────────────────

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

function getScore(b: Bar[], i: number): { score: number; holdBars: number } {
  let w = 0, hW = 0, tW = 0
  const d = new Date(b[i].ts), h = d.getUTCHours(), m = d.getUTCMinutes(), dow = d.getUTCDay()
  const add = (dir: number, wt: number, hold: number) => { w+=dir*wt; hW+=hold*wt; tW+=wt }
  if (dow===4) add(-1,34.41,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,10.84,240)
  if (h===21) add(1,17.90,60); if (h===20) add(1,9.49,60); if (h===23) add(-1,8.94,30)
  const bp = avgCP(b,i,60)
  if (bp>0.58) add(1,15.49,240); if (bp<0.42) add(-1,15.49,240)
  if (i>=60 && (h===13||(h===14&&m<=15)) && m<=15) {
    const g = lbRet(b,i,60); if (Math.abs(g)>5) add(g>0?-1:1,6.87,120)
  }
  if (i>=60) { const r=rsiVal(b,i,60); if (r<30) add(1,4.63,120); if (r>70) add(-1,4.63,120) }
  if (i>=1440) { const dy=lbRet(b,i,1440); if (Math.abs(dy)>30) add(dy>0?-1:1,5.0,240) }
  return { score: w, holdBars: tW>0 ? Math.max(60, Math.min(240, Math.round(hW/tW))) : 120 }
}

// ── Simulation ────────────────────────────────────────────────────────────

function simulate(
  bars: Bar[],
  fee: number,
  longEntry = 10, shortEntry = 8,
  extThresh = 10, stopBps = 100, stopCapDay = 2
): Trade[] {
  const trades: Trade[] = []
  const stopCounts = new Map<string, number>()   // 'YYYY-MM-DD_long' → count
  let cd = 0

  for (let i = 1440; i < bars.length; i++) {
    if (i < cd) continue
    const { score, holdBars } = getScore(bars, i)
    const dir  = score > 0 ? 1 : -1
    const side: 'long'|'short' = dir > 0 ? 'long' : 'short'
    const thresh = side === 'long' ? longEntry : shortEntry
    if (Math.abs(score) < thresh) continue

    // Daily stop cap
    const day = new Date(bars[i].ts).toISOString().slice(0,10)
    const capKey = `${day}_${side}`
    if ((stopCounts.get(capKey) ?? 0) >= stopCapDay) continue

    const entry = bars[i].c
    let deadline = i + holdBars, exitBar = deadline, stopped = false, cur = i

    outer: while (true) {
      if (deadline >= bars.length) { exitBar = bars.length - 2; break }
      for (let j = cur+1; j <= deadline && j < bars.length; j++) {
        if (dir * (bars[j].c - entry) / entry * 10000 <= -stopBps) {
          exitBar = j; stopped = true; break outer
        }
      }
      const { score: es, holdBars: eh } = getScore(bars, deadline)
      if ((es * dir) > 0 && Math.abs(es) >= extThresh) {
        cur = deadline; deadline = Math.min(bars.length-2, deadline + Math.max(60, Math.min(240,eh)))
        continue
      }
      exitBar = deadline; break
    }

    if (exitBar >= bars.length) exitBar = bars.length - 2
    const gross = stopped ? -stopBps : dir * (bars[exitBar].c - entry) / entry * 10000
    const net   = gross - fee

    trades.push({
      entryBar: i, net, stopped, side,
      month: new Date(bars[i].ts).toISOString().slice(0,7),
      year:  new Date(bars[i].ts).toISOString().slice(0,4),
      entryScore: Math.abs(score)
    })

    if (stopped) stopCounts.set(capKey, (stopCounts.get(capKey) ?? 0) + 1)
    cd = exitBar + 5
  }
  return trades
}

// ── Statistics ────────────────────────────────────────────────────────────

function tStat(v: number[]): { n: number; mean: number; t: number; wr: number; sd: number } {
  const n = v.length
  if (n < 3) return { n, mean: 0, t: 0, wr: 0, sd: 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))
  const se   = sd / Math.sqrt(n)
  return { n, mean, t: se > 0 ? mean/se : 0, wr: v.filter(x=>x>0).length/n, sd }
}

function sig(t: number): string {
  const a = Math.abs(t)
  return a > 3.89 ? '****' : a > 3.29 ? '***' : a > 2.58 ? '**' : a > 1.96 ? '*' : ''
}

function monthlyPnl(trades: Trade[], notional = 1500): Map<string, number> {
  const m = new Map<string, number>()
  for (const t of trades) m.set(t.month, (m.get(t.month) ?? 0) + notional * t.net / 10000)
  return m
}

function monthlyIR(trades: Trade[]): { mean: number; sd: number; ir: number; worst: number; pos: number; total: number } {
  const mp = monthlyPnl(trades)
  const vals = [...mp.values()]
  const s = tStat(vals)
  return { mean: s.mean, sd: s.sd, ir: s.sd > 0 ? s.mean/s.sd : 0, worst: Math.min(...vals), pos: vals.filter(v=>v>0).length, total: vals.length }
}

function equityStats(trades: Trade[], startEq = 500, riskPct = 0.03, stopBps = 100): { endEq: number; maxDd: number } {
  let eq = startEq, peak = startEq, maxDd = 0
  for (const t of trades) {
    const notional = eq * riskPct / (stopBps / 10000)
    eq += notional * t.net / 10000
    if (eq > peak) peak = eq
    maxDd = Math.max(maxDd, (peak - eq) / peak)
  }
  return { endEq: eq, maxDd }
}

// ── Main ──────────────────────────────────────────────────────────────────

async function main() {
  const DATA_DIR = 'data/klines'
  const files = [
    `${DATA_DIR}/BTCUSDT-1m-2022-2025.jsonl`,
    `${DATA_DIR}/BTCUSDT-1m-2022-2025b.jsonl`,
    `${DATA_DIR}/BTCUSDT-1m.jsonl`,
  ]

  console.log('Loading bars from:')
  for (const f of files) console.log(`  ${f} (${fs.existsSync(f) ? 'found' : 'MISSING'})`)

  const bars = await loadKlines(files)
  const first = new Date(bars[0].ts).toISOString().slice(0,10)
  const last  = new Date(bars[bars.length-1].ts).toISOString().slice(0,10)
  const years = (bars.length / 525600).toFixed(2)
  console.log(`\nLoaded ${bars.length} bars  (${first} → ${last}, ~${years} years)\n`)

  // Check for large gaps (>60 min) — flag data quality
  let gaps = 0, maxGap = 0
  for (let i = 1; i < bars.length; i++) {
    const gap = (bars[i].ts - bars[i-1].ts) / 60000
    if (gap > 60) { gaps++; if (gap > maxGap) maxGap = gap }
  }
  if (gaps > 0) console.log(`Data gaps: ${gaps} gaps > 60min, largest = ${maxGap.toFixed(0)}min\n`)

  for (const fee of [0, 4]) {
    const feeLabel = fee === 0 ? 'MEXC 0bps' : 'Bybit 4bps'
    const trades   = simulate(bars, fee)
    const all      = tStat(trades.map(t => t.net))
    const mir      = monthlyIR(trades)
    const eq       = equityStats(trades)
    const longs    = trades.filter(t => t.side==='long')
    const shorts   = trades.filter(t => t.side==='short')
    const stops    = trades.filter(t => t.stopped)

    console.log(`${'═'.repeat(70)}`)
    console.log(`SMOOTH — ${feeLabel}   (${first} → ${last})`)
    console.log(`${'═'.repeat(70)}`)
    console.log(`Trades:    ${all.n}  (${(all.n / parseFloat(years)).toFixed(0)}/yr  ${(all.n / bars.length * 1440).toFixed(1)}/day)`)
    console.log(`Mean/trade: ${all.mean >= 0 ? '+' : ''}${all.mean.toFixed(2)} bps`)
    console.log(`t-stat:    ${all.t.toFixed(2)}${sig(all.t)}`)
    console.log(`WR:        ${(all.wr*100).toFixed(1)}%`)
    console.log(`Stops:     ${stops.length} (${(stops.length/all.n*100).toFixed(1)}%)`)
    console.log(`Monthly IR: ${mir.ir.toFixed(2)}  mean=$${mir.mean.toFixed(0)}  SD=$${mir.sd.toFixed(0)}  worst=$${mir.worst.toFixed(0)}`)
    console.log(`Months+:   ${mir.pos}/${mir.total}`)
    console.log(`Equity:    $500 → $${eq.endEq.toFixed(0)} (+${((eq.endEq/500-1)*100).toFixed(0)}%)  maxDD=${(eq.maxDd*100).toFixed(1)}%`)

    // Side stats
    const ls = tStat(longs.map(t=>t.net)), ss = tStat(shorts.map(t=>t.net))
    console.log(`\nLong:  n=${ls.n}  mean=${ls.mean.toFixed(2)}bps  t=${ls.t.toFixed(2)}${sig(ls.t)}  WR=${(ls.wr*100).toFixed(1)}%`)
    console.log(`Short: n=${ss.n}  mean=${ss.mean.toFixed(2)}bps  t=${ss.t.toFixed(2)}${sig(ss.t)}  WR=${(ss.wr*100).toFixed(1)}%`)

    // Half-split
    const mid = Math.floor(trades.length / 2)
    const h1  = tStat(trades.slice(0, mid).map(t=>t.net))
    const h2  = tStat(trades.slice(mid).map(t=>t.net))
    console.log(`\nHalf-split: H1 t=${h1.t.toFixed(2)}${sig(h1.t)} mean=${h1.mean.toFixed(2)}  |  H2 t=${h2.t.toFixed(2)}${sig(h2.t)} mean=${h2.mean.toFixed(2)}`)

    // Annual breakdown
    console.log(`\nAnnual breakdown:`)
    const years_list = [...new Set(trades.map(t => t.year))].sort()
    for (const yr of years_list) {
      const yt  = trades.filter(t => t.year === yr)
      const ys  = tStat(yt.map(t => t.net))
      const yir = monthlyIR(yt)
      console.log(`  ${yr}  n=${String(ys.n).padStart(4)}  mean=${ys.mean >= 0 ? '+' : ''}${ys.mean.toFixed(2).padStart(6)}bps  t=${ys.t.toFixed(2).padStart(5)}${sig(ys.t).padEnd(4)}  WR=${(ys.wr*100).toFixed(1).padStart(5)}%  months+=${yir.pos}/${yir.total}`)
    }

    // Monthly breakdown
    console.log(`\nMonthly breakdown (fixed $1500 notional):`)
    const mp = monthlyPnl(trades)
    for (const [mo, pnl] of [...mp.entries()].sort()) {
      const mt = trades.filter(t => t.month === mo)
      const ms = tStat(mt.map(t => t.net))
      const flag = pnl >= 0 ? '✓' : '✗'
      console.log(`  ${mo}  n=${String(ms.n).padStart(3)}  mean=${ms.mean >= 0 ? '+' : ''}${ms.mean.toFixed(1).padStart(6)}bps  t=${ms.t.toFixed(2).padStart(5)}  $${pnl >= 0 ? '+' : ''}${pnl.toFixed(0).padStart(5)}  ${flag}`)
    }

    // Walk-forward: train on each year, test on next
    console.log(`\nWalk-forward (train 1yr → test next yr):`)
    const yearBars: Record<string, number> = {}
    for (let i = 0; i < bars.length; i++) {
      const yr = new Date(bars[i].ts).toISOString().slice(0,4)
      if (!yearBars[yr]) yearBars[yr] = i
    }
    const yrs = Object.keys(yearBars).sort()
    for (let yi = 0; yi < yrs.length - 1; yi++) {
      const trainStart = yearBars[yrs[yi]]
      const testStart  = yearBars[yrs[yi+1]]
      const testEnd    = yrs[yi+2] ? yearBars[yrs[yi+2]] : bars.length
      const trainBars  = bars.slice(trainStart, testStart + 1440)
      const testBars   = bars.slice(testStart)
      const trainT     = simulate(trainBars, fee)
      const testT      = simulate(testBars.slice(0, testEnd - testStart), fee)
      const tr = tStat(trainT.map(t=>t.net)), te = tStat(testT.map(t=>t.net))
      console.log(`  ${yrs[yi]}→${yrs[yi+1]}  TRAIN n=${tr.n} mean=${tr.mean.toFixed(2)}bps t=${tr.t.toFixed(2)}${sig(tr.t)}  |  TEST n=${te.n} mean=${te.mean.toFixed(2)}bps t=${te.t.toFixed(2)}${sig(te.t)}`)
    }

    console.log()
  }
}

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