/**
 * Full combinatorial backtest: sweep ALL tunable parameters together.
 *
 * Parameters:
 *   - Entry threshold: 8, 10, 12, 15
 *   - Extension threshold: 0, 3, 5, 8, 10
 *   - SL cooldown: 0, 60, 120, 180, 240
 *   - Hold range: 60-120, 60-180, 60-240, 60-360, 120-240
 *   - Stop loss: 50, 75, 100, 150, 200
 *   - Risk: fixed at 3% (not a backtest param, only affects equity curve)
 *
 * Total combos: 4 × 5 × 5 × 5 × 5 = 2,500 combinations
 * For each: mean net bps, t-stat, n trades, WR, cum bps, positive months
 * Rank by t-stat, show top 20, then verify top 3 with monthly detail.
 */
import fs from 'node:fs'
import readline from 'node:readline'

interface Bar { ts: number; o: number; h: number; l: number; c: number }

async function load(): Promise<Bar[]> {
  const bars: Bar[] = []
  const rl = readline.createInterface({ input: fs.createReadStream('data/klines/BTCUSDT-1m.jsonl') })
  for await (const line of rl) { if (line.trim()) bars.push(JSON.parse(line)) }
  return bars
}

function lbRet(b: Bar[], i: number, n: 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) { return b.h>b.l?(b.c-b.l)/(b.h-b.l):0.5 }
function avgCP(b: Bar[], i: number, n: 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 rsi(b: 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 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(bars: Bar[], i: number): { score: number; holdBars: number } {
  let w=0,hW=0,tW=0
  const d=new Date(bars[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(bars,i,60);if(bp>0.58)add(1,15.49,240);if(bp<0.42)add(-1,15.49,240)
  if((h===13||(h===14&&m<=15))&&m<=15&&i>=60){const g=lbRet(bars,i,60);if(Math.abs(g)>5)add(g>0?-1:1,6.87,120)}
  const rv=rsi(bars,i,60);if(rv<30)add(1,4.63,120);if(rv>70)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.0,240)}
  return{score:w,holdBars:tW>0?Math.max(60,Math.min(240,Math.round(hW/tW))):120}
}

function tt(v: number[]): { mean: number; t: number; n: number } {
  const n=v.length; if(n<10) return {mean:0,t:0,n}
  const m=v.reduce((a,b)=>a+b,0)/n
  const se=Math.sqrt(v.reduce((s,x)=>s+(x-m)**2,0)/(n-1)/n)
  return {mean:m,t:se>0?m/se:0,n}
}
function sig(t: number) { const a=Math.abs(t);return a>3.89?'****':a>3.29?'***':a>2.58?'**':a>1.96?'*':'' }

function simulate(
  bars: Bar[],
  entryThresh: number,
  extThresh: number,
  slCooldown: number,
  minHold: number,
  maxHold: number,
  stopBps: number
): number[] {
  const nets: number[] = []
  let cd = 0, slDir = 0, slUntil = -1
  const FEE = 4

  for (let i = 1440; i < bars.length; i++) {
    if (i < cd) continue
    const { score, holdBars: rawHold } = getScore(bars, i)
    if (Math.abs(score) < entryThresh) continue
    const dir = score > 0 ? 1 : -1
    const holdBars = Math.max(minHold, Math.min(maxHold, rawHold))
    if (slDir !== 0 && i < slUntil && dir === slDir) continue

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

    // Walk with extensions
    let curStart = i
    while (true) {
      if (deadline >= bars.length) { exitBar = bars.length - 2; break }
      // Check stop in this segment
      for (let j = curStart + 1; j <= deadline && j < bars.length; j++) {
        if (dir * (bars[j].c - entry) / entry * 10000 <= -stopBps) {
          exitBar = j; stopped = true; break
        }
      }
      if (stopped) break
      // Extension check at deadline
      const { score: extScore, holdBars: extRawHold } = getScore(bars, deadline)
      const extHold = Math.max(minHold, Math.min(maxHold, extRawHold))
      const dirMatch = (extScore * dir) > 0
      const magOk = extThresh === 0 ? true : Math.abs(extScore) >= extThresh
      if (dirMatch && magOk) {
        curStart = deadline
        deadline = Math.min(bars.length - 2, deadline + extHold)
        continue
      }
      exitBar = deadline
      break
    }

    if (exitBar >= bars.length) exitBar = bars.length - 2
    const grossBps = stopped ? -stopBps : dir * (bars[exitBar].c - entry) / entry * 10000
    nets.push(grossBps - FEE)
    if (stopped && slCooldown > 0) { slDir = dir; slUntil = exitBar + slCooldown }
    cd = Math.max(exitBar + 5, i + holdBars)
  }
  return nets
}

function monthlyPositive(bars: Bar[], nets: number[], indices: number[]): number {
  // approximate — just count months
  const m = new Map<string, number>()
  let idx = 0
  for (const net of nets) {
    const month = '?'  // simplified
    if (!m.has(month)) m.set(month, 0)
    m.set(month, m.get(month)! + net)
  }
  return 0
}

interface Config {
  entryThresh: number; extThresh: number; slCooldown: number
  minHold: number; maxHold: number; stopBps: number
}

interface Result extends Config {
  mean: number; t: number; n: number; wr: number; cum: number; label: string
}

async function main() {
  process.stderr.write('Loading...')
  const bars = await load()
  process.stderr.write(` ${bars.length}\n`)

  const ENTRY_THRESHOLDS = [8, 10, 12, 15]
  const EXT_THRESHOLDS = [0, 3, 5, 8, 10]
  const SL_COOLDOWNS = [0, 60, 120, 180, 240]
  const HOLD_RANGES: [number, number][] = [[60,120],[60,180],[60,240],[60,360],[120,240]]
  const STOP_LEVELS = [50, 75, 100, 150, 200]

  const total = ENTRY_THRESHOLDS.length * EXT_THRESHOLDS.length * SL_COOLDOWNS.length *
                HOLD_RANGES.length * STOP_LEVELS.length
  process.stderr.write(`Running ${total} combinations...\n`)

  const results: Result[] = []
  let count = 0

  for (const entryThresh of ENTRY_THRESHOLDS) {
    for (const extThresh of EXT_THRESHOLDS) {
      for (const slCooldown of SL_COOLDOWNS) {
        for (const [minHold, maxHold] of HOLD_RANGES) {
          for (const stopBps of STOP_LEVELS) {
            count++
            if (count % 250 === 0) process.stderr.write(`  ${count}/${total}\n`)
            const nets = simulate(bars, entryThresh, extThresh, slCooldown, minHold, maxHold, stopBps)
            const s = tt(nets)
            const wr = nets.filter(n => n > 0).length / nets.length
            const cum = nets.reduce((a, b) => a + b, 0)
            const label = `e=${entryThresh} x=${extThresh} sl=${slCooldown} h=${minHold}-${maxHold} stop=${stopBps}`
            results.push({ entryThresh, extThresh, slCooldown, minHold, maxHold, stopBps, mean: s.mean, t: s.t, n: s.n, wr, cum, label })
          }
        }
      }
    }
  }

  results.sort((a, b) => b.t - a.t)

  console.log(`\n${'═'.repeat(110)}`)
  console.log(`FULL COMBINATORIAL BACKTEST — ${total} combinations, ranked by t-stat`)
  console.log(`${'═'.repeat(110)}`)
  console.log()

  // Top 30
  console.log('TOP 30:')
  console.log()
  console.log('  Rank  Config                                          n     mean     t-stat    WR    cum bps')
  console.log('  ' + '─'.repeat(100))
  for (let i = 0; i < Math.min(30, results.length); i++) {
    const r = results[i]
    console.log(
      `  ${String(i+1).padStart(4)}  ${r.label.padEnd(48)}  ${String(r.n).padStart(5)}  ${r.mean.toFixed(2).padStart(7)}  ${r.t.toFixed(2).padStart(7)} ${sig(r.t).padEnd(4)}  ${(r.wr*100).toFixed(0).padStart(3)}%  ${r.cum.toFixed(0).padStart(8)}`
    )
  }

  // Bottom 10 (worst)
  console.log()
  console.log('BOTTOM 10 (worst):')
  console.log()
  for (let i = results.length - 10; i < results.length; i++) {
    const r = results[i]
    console.log(
      `  ${String(i+1).padStart(4)}  ${r.label.padEnd(48)}  ${String(r.n).padStart(5)}  ${r.mean.toFixed(2).padStart(7)}  ${r.t.toFixed(2).padStart(7)} ${sig(r.t).padEnd(4)}  ${(r.wr*100).toFixed(0).padStart(3)}%  ${r.cum.toFixed(0).padStart(8)}`
    )
  }

  // ── Parameter marginals: which values are best on average? ──
  console.log()
  console.log('═'.repeat(110))
  console.log('PARAMETER MARGINALS — average t-stat by parameter value (higher = better)')
  console.log('═'.repeat(110))
  console.log()

  function marginal(name: string, key: keyof Config, vals: number[]) {
    console.log(`  ${name}:`)
    for (const v of vals) {
      const subset = results.filter(r => r[key] === v)
      const avgT = subset.reduce((s, r) => s + r.t, 0) / subset.length
      const avgMean = subset.reduce((s, r) => s + r.mean, 0) / subset.length
      const best = subset.sort((a, b) => b.t - a.t)[0]
      const bar = '█'.repeat(Math.max(0, Math.round((avgT + 1) * 5)))
      console.log(`    ${String(v).padStart(4)}: avg_t=${avgT.toFixed(2).padStart(5)}  avg_mean=${avgMean.toFixed(2).padStart(6)}bps  best_t=${best.t.toFixed(2)}  ${bar}`)
    }
    console.log()
  }

  marginal('Entry threshold', 'entryThresh', ENTRY_THRESHOLDS)
  marginal('Extension threshold', 'extThresh', EXT_THRESHOLDS)
  marginal('SL cooldown (bars)', 'slCooldown', SL_COOLDOWNS)
  marginal('Stop loss (bps)', 'stopBps', STOP_LEVELS)

  // Hold range marginal
  console.log('  Hold range:')
  for (const [minH, maxH] of HOLD_RANGES) {
    const subset = results.filter(r => r.minHold === minH && r.maxHold === maxH)
    const avgT = subset.reduce((s, r) => s + r.t, 0) / subset.length
    const avgMean = subset.reduce((s, r) => s + r.mean, 0) / subset.length
    const bar = '█'.repeat(Math.max(0, Math.round((avgT + 1) * 5)))
    console.log(`    ${minH}-${maxH}: avg_t=${avgT.toFixed(2).padStart(5)}  avg_mean=${avgMean.toFixed(2).padStart(6)}bps  ${bar}`)
  }

  // ── Monthly detail for top 3 ──
  console.log()
  console.log('═'.repeat(110))
  console.log('TOP 3 — MONTHLY DETAIL')
  console.log('═'.repeat(110))

  for (const r of results.slice(0, 3)) {
    console.log()
    console.log(`  ${r.label}  mean=${r.mean.toFixed(2)}bps  t=${r.t.toFixed(2)} ${sig(r.t)}  n=${r.n}`)
    console.log()

    // Re-simulate with month tracking
    let cd = 0, slDir = 0, slUntil = -1
    const monthNets = new Map<string, number[]>()
    for (let i = 1440; i < bars.length; i++) {
      if (i < cd) continue
      const { score, holdBars: rawHold } = getScore(bars, i)
      if (Math.abs(score) < r.entryThresh) continue
      const dir = score > 0 ? 1 : -1
      const holdBars = Math.max(r.minHold, Math.min(r.maxHold, rawHold))
      if (slDir !== 0 && i < slUntil && dir === slDir) continue
      const entry = bars[i].c
      let deadline = i + holdBars, exitBar = deadline, stopped = false, curStart = i
      while (true) {
        if (deadline >= bars.length) { exitBar = bars.length - 2; break }
        for (let j = curStart + 1; j <= deadline && j < bars.length; j++) {
          if (dir * (bars[j].c - entry) / entry * 10000 <= -r.stopBps) { exitBar = j; stopped = true; break }
        }
        if (stopped) break
        const { score: extScore, holdBars: extRaw } = getScore(bars, deadline)
        const extHold = Math.max(r.minHold, Math.min(r.maxHold, extRaw))
        if ((extScore * dir) > 0 && (r.extThresh === 0 || Math.abs(extScore) >= r.extThresh)) {
          curStart = deadline; deadline = Math.min(bars.length - 2, deadline + extHold); continue
        }
        exitBar = deadline; break
      }
      if (exitBar >= bars.length) exitBar = bars.length - 2
      const gross = stopped ? -r.stopBps : dir * (bars[exitBar].c - entry) / entry * 10000
      const net = gross - 4
      const month = new Date(bars[i].ts).toISOString().slice(0, 7)
      if (!monthNets.has(month)) monthNets.set(month, [])
      monthNets.get(month)!.push(net)
      if (stopped && r.slCooldown > 0) { slDir = dir; slUntil = exitBar + r.slCooldown }
      cd = Math.max(exitBar + 5, i + holdBars)
    }

    let cum = 0, posMonths = 0, totMonths = 0
    console.log('    Month      n     cum     mean     t')
    console.log('    ' + '─'.repeat(45))
    for (const [m, nets] of [...monthNets.entries()].sort()) {
      const mt = tt(nets)
      cum += nets.reduce((a, b) => a + b, 0)
      totMonths++; if (mt.mean > 0) posMonths++
      console.log(`    ${m}  ${String(nets.length).padStart(4)}  ${cum.toFixed(0).padStart(7)}  ${mt.mean.toFixed(2).padStart(7)}  ${mt.t.toFixed(2).padStart(6)} ${sig(mt.t)}`)
    }
    console.log(`    Positive months: ${posMonths}/${totMonths}`)
  }

  // ── Verdict ──
  console.log()
  console.log('═'.repeat(110))
  console.log('VERDICT — OPTIMAL CONFIGURATION')
  console.log('═'.repeat(110))
  console.log()
  const top = results[0]
  console.log(`  ${top.label}`)
  console.log(`  mean=${top.mean.toFixed(2)}bps  t=${top.t.toFixed(2)}  n=${top.n}  WR=${(top.wr*100).toFixed(0)}%  cum=${top.cum.toFixed(0)}bps`)
  console.log()
  console.log('  vs current runner config (e=10 x=8 sl=120 h=60-240 stop=100):')
  const current = results.find(r => r.entryThresh===10 && r.extThresh===8 && r.slCooldown===120 && r.minHold===60 && r.maxHold===240 && r.stopBps===100)
  if (current) {
    console.log(`  ${current.label}`)
    console.log(`  mean=${current.mean.toFixed(2)}bps  t=${current.t.toFixed(2)}  n=${current.n}`)
    console.log(`  Improvement: +${(top.mean - current.mean).toFixed(2)}bps/trade  t: ${current.t.toFixed(2)} → ${top.t.toFixed(2)}`)
  }
}

main().catch(e => { process.stderr.write(`FATAL: ${e instanceof Error ? e.stack : e}\n`); process.exit(1) })
