/**
 * Full-year backtest of momentum strategy on 1-minute klines.
 *
 * Adapts the 10s-bar strategy to 1-min bars:
 *   lookback = 60 bars (60 min)
 *   hold     = 30 bars (30 min)
 *   threshold = 5 bps
 *   cooldown = 33 bars
 *
 * Runs:
 * 1. Single pass with compounding equity at 2%, 5%, 10% risk
 * 2. Monthly breakdown to show consistency
 * 3. Drawdown analysis
 * 4. Monte Carlo on the observed trade distribution
 */
import fs from 'node:fs'
import readline from 'node:readline'

interface Kline { ts: number; o: number; h: number; l: number; c: number; v: number; usd: number }

// ── Load ──
async function loadKlines(file: string): Promise<Kline[]> {
  const bars: Kline[] = []
  const rl = readline.createInterface({ input: fs.createReadStream(file) })
  for await (const line of rl) {
    if (!line.trim()) continue
    bars.push(JSON.parse(line))
  }
  return bars
}

// ── Stats ──
function tTest(v: number[]): { mean: number; t: number; n: number } {
  const n = v.length; if (n < 3) 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 pct(arr: number[], p: number) {
  const s = [...arr].sort((a,b) => a-b)
  return s[Math.max(0, Math.min(s.length-1, Math.floor(s.length*p)))] ?? 0
}
function sig(t: number): string {
  const a = Math.abs(t); return a > 3.29 ? '***' : a > 2.58 ? '**' : a > 1.96 ? '*' : ''
}
function money(n: number): string {
  if (n >= 1e9) return '$' + (n/1e9).toFixed(2) + 'B'
  if (n >= 1e6) return '$' + (n/1e6).toFixed(2) + 'M'
  if (n >= 1e3) return '$' + (n/1e3).toFixed(1) + 'k'
  return '$' + n.toFixed(0)
}

// ── Extract raw trade outcomes ──
interface RawTrade {
  entryBar: number; grossBps: number; side: 'long'|'short'
  entryPrice: number; exitPrice: number; month: string
}

function extractTrades(bars: Kline[], lb = 60, hold = 30, thresh = 5, cd = 33): RawTrade[] {
  const trades: RawTrade[] = []
  let lastExit = -cd
  for (let i = Math.max(lb, 60); i < bars.length - hold; i++) {
    if (i - lastExit < cd) continue
    const ref = bars[i - lb].c; if (ref <= 0) continue
    const mom = (bars[i].c - ref) / ref * 10000
    if (Math.abs(mom) < thresh) continue
    const side: 'long'|'short' = mom > 0 ? 'long' : 'short'
    const dir = mom > 0 ? 1 : -1
    const exitPrice = bars[i + hold].c
    const grossBps = dir * (exitPrice - bars[i].c) / bars[i].c * 10000
    const month = new Date(bars[i].ts).toISOString().slice(0, 7)
    trades.push({ entryBar: i, grossBps, side, entryPrice: bars[i].c, exitPrice, month })
    lastExit = i + hold
  }
  return trades
}

// ── Equity curve simulation ──
function equityCurve(
  trades: RawTrade[], startEq: number, riskPct: number,
  feeBps: number, stopBps: number, leverage: number
): {
  finalEq: number; maxDdPct: number; maxDdUsd: number
  curve: number[]; tradeNets: number[]; tradePnlUsd: number[]
} {
  const sd = stopBps / 10000
  let eq = startEq, peak = eq, maxDdPct = 0, maxDdUsd = 0
  const curve = [eq], tradeNets: number[] = [], tradePnlUsd: number[] = []

  for (const t of trades) {
    const net = t.grossBps - 2 * feeBps
    const notional = Math.min(eq * riskPct / sd, eq * leverage)
    const pnlUsd = notional * (net / 10000)
    eq = Math.max(0, eq + pnlUsd)
    if (eq > peak) peak = eq
    const dd = peak - eq; const ddPct = peak > 0 ? dd / peak : 0
    if (ddPct > maxDdPct) maxDdPct = ddPct
    if (dd > maxDdUsd) maxDdUsd = dd
    curve.push(eq)
    tradeNets.push(net)
    tradePnlUsd.push(pnlUsd)
  }
  return { finalEq: eq, maxDdPct, maxDdUsd, curve, tradeNets, tradePnlUsd }
}

async function main() {
  const klineFile = 'data/klines/BTCUSDT-1m.jsonl'
  if (!fs.existsSync(klineFile)) throw new Error(`Missing ${klineFile} — run download-klines first`)

  process.stderr.write('Loading klines...')
  const bars = await loadKlines(klineFile)
  process.stderr.write(` ${bars.length} bars\n`)

  const firstDate = new Date(bars[0].ts).toISOString().slice(0, 10)
  const lastDate = new Date(bars[bars.length - 1].ts).toISOString().slice(0, 10)
  const priceStart = bars[0].o, priceEnd = bars[bars.length - 1].c
  const btcReturn = ((priceEnd - priceStart) / priceStart * 100).toFixed(1)

  const trades = extractTrades(bars)
  const feeBps = 2
  const netBps = trades.map(t => t.grossBps - 2 * feeBps)
  const tt = tTest(netBps)
  const wr = netBps.filter(n => n > 0).length / netBps.length
  const wins = netBps.filter(n => n > 0), losses = netBps.filter(n => n <= 0)
  const avgWin = wins.reduce((a,b) => a+b, 0) / wins.length
  const avgLoss = Math.abs(losses.reduce((a,b) => a+b, 0) / losses.length)

  // Streaks
  let maxLoseStreak = 0, streak = 0
  for (const n of netBps) { if (n <= 0) { streak++; if (streak > maxLoseStreak) maxLoseStreak = streak } else streak = 0 }

  console.log(`\n${'═'.repeat(90)}`)
  console.log('FULL-YEAR BACKTEST — MOMENTUM STRATEGY')
  console.log(`${'═'.repeat(90)}`)
  console.log()
  console.log(`  Data:       ${bars.length} 1-minute candles, ${firstDate} → ${lastDate}`)
  console.log(`  BTC:        $${priceStart.toFixed(0)} → $${priceEnd.toFixed(0)} (${btcReturn}%)`)
  console.log(`  Signal:     lb=60min hold=30min thresh=5bps fee=${feeBps}bps/side`)
  console.log(`  Trades:     ${trades.length}`)
  console.log(`  Mean net:   ${tt.mean.toFixed(2)} bps/trade`)
  console.log(`  t-stat:     ${tt.t.toFixed(2)} ${sig(tt.t)}`)
  console.log(`  Win rate:   ${(wr * 100).toFixed(1)}%  (${wins.length}W / ${losses.length}L)`)
  console.log(`  Avg win:    +${avgWin.toFixed(1)} bps`)
  console.log(`  Avg loss:   -${avgLoss.toFixed(1)} bps`)
  console.log(`  W/L ratio:  ${(avgWin / avgLoss).toFixed(2)}`)
  console.log(`  Max streak: ${maxLoseStreak} consecutive losses`)
  console.log(`  Worst trade: ${Math.min(...netBps).toFixed(1)} bps`)
  console.log(`  Best trade:  ${Math.max(...netBps).toFixed(1)} bps`)
  console.log()

  // ── Monthly breakdown ──
  console.log(`${'─'.repeat(90)}`)
  console.log('MONTHLY BREAKDOWN')
  console.log()
  console.log('  Month      Trades   WR    Mean net    Cumulative    t-stat')
  console.log('  ' + '─'.repeat(65))

  const byMonth = new Map<string, number[]>()
  trades.forEach(t => {
    if (!byMonth.has(t.month)) byMonth.set(t.month, [])
    byMonth.get(t.month)!.push(t.grossBps - 2 * feeBps)
  })

  let cumNet = 0
  let positiveMonths = 0, totalMonths = 0
  for (const [month, nets] of [...byMonth.entries()].sort()) {
    const mtt = tTest(nets)
    const mwr = nets.filter(n => n > 0).length / nets.length
    cumNet += nets.reduce((a, b) => a + b, 0)
    totalMonths++
    if (mtt.mean > 0) positiveMonths++
    console.log(`  ${month}    ${String(nets.length).padStart(5)}   ${(mwr*100).toFixed(0).padStart(3)}%   ${mtt.mean.toFixed(2).padStart(8)} bps   ${cumNet.toFixed(0).padStart(10)} bps   ${mtt.t.toFixed(2).padStart(6)} ${sig(mtt.t)}`)
  }
  console.log()
  console.log(`  Positive months: ${positiveMonths}/${totalMonths}`)

  // ── Equity simulation at 2%, 5%, 10% ──
  console.log()
  console.log(`${'─'.repeat(90)}`)
  console.log('COMPOUNDING EQUITY — $500 start, 100x leverage')
  console.log()

  const START = 500
  const RISKS = [
    { pct: 0.02, label: '2%  conservative' },
    { pct: 0.05, label: '5%  moderate' },
    { pct: 0.10, label: '10% half-Kelly' },
  ]

  for (const { pct: rp, label } of RISKS) {
    const { finalEq, maxDdPct, maxDdUsd, tradePnlUsd } = equityCurve(trades, START, rp, feeBps, 100, 100)
    const ret = ((finalEq - START) / START * 100)
    const winTrades = tradePnlUsd.filter(p => p > 0).length
    console.log(`  ${label}:`)
    console.log(`    $${START} → ${money(finalEq)} (${ret >= 0 ? '+' : ''}${ret.toFixed(0)}%)`)
    console.log(`    Max DD: ${(maxDdPct * 100).toFixed(1)}% (${money(maxDdUsd)})`)
    console.log(`    WR (by $): ${(winTrades / trades.length * 100).toFixed(0)}%`)

    // Monthly equity snapshots
    const monthlyEq: [string, number][] = []
    let eq = START
    const sd = 100 / 10000  // stopBps
    for (const t of trades) {
      const net = t.grossBps - 2 * feeBps
      const notional = Math.min(eq * rp / sd, eq * 100)
      eq = Math.max(0, eq + notional * (net / 10000))
      const month = t.month
      if (monthlyEq.length === 0 || monthlyEq[monthlyEq.length - 1][0] !== month) {
        monthlyEq.push([month, eq])
      } else {
        monthlyEq[monthlyEq.length - 1][1] = eq
      }
    }
    process.stdout.write('    Monthly: ')
    for (const [m, e] of monthlyEq) process.stdout.write(`${m.slice(5)}:${money(e)} `)
    console.log()
    console.log()
  }

  // ── Monte Carlo ──
  console.log(`${'─'.repeat(90)}`)
  console.log('MONTE CARLO — 5000 shuffled orderings of same trades')
  console.log()

  const grossPool = trades.map(t => t.grossBps)
  for (const { pct: rp, label } of RISKS) {
    const finals: number[] = [], dds: number[] = []
    let ruins = 0
    for (let iter = 0; iter < 5000; iter++) {
      const shuffled = [...grossPool]
      for (let i = shuffled.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
      }
      let eq = START, peak = eq, maxDd = 0
      const sd = 100 / 10000
      for (const g of shuffled) {
        const net = g - 2 * feeBps
        const notional = Math.min(eq * rp / sd, eq * 100)
        eq = Math.max(0, eq + notional * (net / 10000))
        if (eq > peak) peak = eq
        const dd = peak > 0 ? (peak - eq) / peak : 0
        if (dd > maxDd) maxDd = dd
      }
      finals.push(eq); dds.push(maxDd)
      if (eq < START * 0.2) ruins++
    }

    console.log(`  ${label}:`)
    console.log(`    Final equity:  p10=${money(pct(finals, 0.10))}  p50=${money(pct(finals, 0.50))}  p90=${money(pct(finals, 0.90))}`)
    console.log(`    Max DD:        p50=${(pct(dds, 0.50)*100).toFixed(1)}%  p90=${(pct(dds, 0.90)*100).toFixed(1)}%  p99=${(pct(dds, 0.99)*100).toFixed(1)}%`)
    console.log(`    Ruin prob:     ${(ruins / 5000 * 100).toFixed(2)}%`)
    console.log()
  }

  // ── Breakeven check ──
  console.log(`${'─'.repeat(90)}`)
  console.log('FEE SENSITIVITY')
  console.log()
  for (const fee of [0, 1, 2, 3, 4, 5.5]) {
    const nets = trades.map(t => t.grossBps - 2 * fee)
    const t2 = tTest(nets)
    const w = nets.filter(n => n > 0).length / nets.length
    console.log(`  fee=${String(fee).padStart(4)}bps/side: mean=${t2.mean.toFixed(2).padStart(7)}bps  WR=${(w*100).toFixed(0)}%  t=${t2.t.toFixed(2)} ${sig(t2.t)}`)
  }
  console.log(`  Breakeven fee: ~${(tTest(trades.map(t => t.grossBps)).mean / 2).toFixed(1)}bps each way`)

  console.log()
  console.log(`${'═'.repeat(90)}`)
}

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