/**
 * Compounding projection — no notional cap.
 * Pure math: $500 starting capital, 100x leverage, risk-based sizing.
 *
 * Position per trade = equity × riskPct / stopDist
 *   (stopDist = 100bps = 1%, so 21% risk → 21× equity notional)
 *
 * Assumes edge (11.71bps mean) persists at scale.
 * Numbers beyond ~$50k notional are theoretical — BTC liquidity
 * would compress the edge in practice, but the math is shown as-is.
 */
import fs from 'node:fs'
import readline from 'node:readline'

interface RBar { ts: number; o:number; h:number; l:number; c:number; buyVol:number; sellVol:number; n:number; exVol:Record<string,number> }

async function loadAll(): Promise<Map<string, RBar[]>> {
  const sessions = new Map<string, RBar[]>()
  const dir = 'data/historical'
  for (const date of fs.readdirSync(dir).filter(d => {
    try { return fs.statSync(`${dir}/${d}/trades.jsonl`).size > 100_000 } catch { return false }
  }).sort()) {
    process.stderr.write(`Loading ${date}...`)
    const m = new Map<number, RBar>()
    const rl = readline.createInterface({ input: fs.createReadStream(`${dir}/${date}/trades.jsonl`) })
    for await (const line of rl) {
      const d = JSON.parse(line)
      const k = Math.floor(d.ts / 10000), p = d.data.price, vol = d.data.notionalUsd, side = d.data.side, ex = d.data.exchange
      const e = m.get(k)
      if (!e) m.set(k, { ts:k*10000, o:p, h:p, l:p, c:p, buyVol:side==='buy'?vol:0, sellVol:side==='sell'?vol:0, n:1, exVol:{[ex]:vol} })
      else { e.h=Math.max(e.h,p); e.l=Math.min(e.l,p); e.c=p; if(side==='buy') e.buyVol+=vol; else e.sellVol+=vol; e.n++; e.exVol[ex]=(e.exVol[ex]||0)+vol }
    }
    sessions.set(date, [...m.values()].sort((a,b) => a.ts - b.ts))
    process.stderr.write(` ${sessions.get(date)!.length}\n`)
  }
  return sessions
}

function extractGross(bars: RBar[], lb=360, hold=180, thresh=5, cd=183): number[] {
  const out: number[] = []
  let last = -cd
  for (let i = Math.max(lb,60); i < bars.length - hold; i++) {
    if (i - last < 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
    out.push((mom > 0 ? 1 : -1) * (bars[i+hold].c - bars[i].c) / bars[i].c * 10000)
    last = i + hold
  }
  return out
}

function pct(arr: number[], p: number): 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 fmt(n: number): string {
  if (!isFinite(n) || n < 0) return '$0'
  if (n >= 1e12) return '$' + (n/1e12).toFixed(2) + 'T'
  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)
}

function xRet(eq: number, start: number): string {
  const x = eq / start
  if (x >= 1000) return (x/1000).toFixed(1) + 'kx'
  if (x >= 100)  return x.toFixed(0) + 'x'
  if (x >= 10)   return x.toFixed(1) + 'x'
  return x.toFixed(2) + 'x'
}

function runPath(
  pool: number[], trades: number,
  start: number, riskPct: number, feeBps: number,
  stopBps: number, leverage: number,
  checkpoints: Set<number>
): { snaps: Map<number,number>; maxDdPct: number; ruined: boolean } {
  const sd = stopBps / 10000
  let eq = start, peak = eq, maxDd = 0, ruined = false
  const snaps = new Map<number,number>()
  for (let i = 0; i < trades; i++) {
    const g = pool[Math.floor(Math.random() * pool.length)]
    const net = g - 2 * feeBps
    // notional = equity × riskPct / stopDist, capped at equity × leverage
    const notional = Math.min(eq * riskPct / sd, eq * leverage)
    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
    if (eq <= 0 && !ruined) ruined = true
    if (checkpoints.has(i + 1)) snaps.set(i + 1, eq)
  }
  return { snaps, maxDdPct: maxDd, ruined }
}

async function main() {
  const sessions = await loadAll()
  const pool: number[] = []
  for (const [, bars] of sessions) pool.push(...extractGross(bars))

  const START     = 500
  const FEE       = 2
  const STOP_BPS  = 100
  const LEVERAGE  = 100
  const ITERS     = 10_000
  const TPD       = pool.length / sessions.size   // trades per day ~23

  const nets = pool.map(g => g - 2*FEE)
  const mean = nets.reduce((a,b)=>a+b,0) / nets.length
  const wr   = nets.filter(n=>n>0).length / nets.length
  const wArr = nets.filter(n=>n>0), lArr = nets.filter(n=>n<=0)
  const avgW = wArr.reduce((a,b)=>a+b,0)/wArr.length
  const avgL = Math.abs(lArr.reduce((a,b)=>a+b,0)/lArr.length)
  const kelly = wr - (1-wr)/(avgW/avgL)

  console.log(`\n${'═'.repeat(88)}`)
  console.log('COMPOUNDING PROJECTION  —  $500 @ 100x, no cap, pure Kelly math')
  console.log(`${'═'.repeat(88)}`)
  console.log()
  console.log(`  Start: $${START}  |  Leverage: ${LEVERAGE}x  |  Stop: ${STOP_BPS}bps sizing`)
  console.log(`  Edge:  mean=${mean.toFixed(2)}bps  WR=${(wr*100).toFixed(0)}%  avgW=${avgW.toFixed(1)}  avgL=-${avgL.toFixed(1)}  Kelly=${(kelly*100).toFixed(1)}%`)
  console.log(`  Pace:  ~${TPD.toFixed(0)} trades/day (~1 every ${(1440/TPD).toFixed(0)} min)`)
  console.log()
  console.log(`  Position per trade = equity × risk% / 1%  (e.g. 21% risk → 21× equity notional)`)
  console.log(`  Leverage cap binds only when risk% > ${LEVERAGE}% — never for any scenario below.`)
  console.log()

  const RISKS = [
    { r: 0.02,  tag: '2%',  label: '2%  conservative' },
    { r: 0.05,  tag: '5%',  label: '5%  moderate' },
    { r: 0.10,  tag: '10%', label: '10% half-Kelly' },
    { r: 0.21,  tag: '21%', label: '21% full Kelly' },
  ]

  // Per-risk daily multiplier (geometric approximation for reference)
  console.log('  Notional multiplier & theoretical daily growth rate:')
  for (const { r, label } of RISKS) {
    const notionalMult = r / (STOP_BPS / 10000)   // = r × 100
    const dailyLogGrowth = pool.reduce((s, g) => {
      const net = g - 2*FEE
      const growth = 1 + notionalMult * (net / 10000)
      return s + (growth > 0 ? Math.log(growth) : -10)
    }, 0) / pool.length
    const dailyGrowth = Math.exp(dailyLogGrowth * TPD)
    console.log(`    ${label.padEnd(20)}  ${notionalMult.toFixed(0)}x notional  →  ~${((dailyGrowth-1)*100).toFixed(1)}%/day median log-growth`)
  }
  console.log()

  // Checkpoints
  const DAYS    = [1, 7, 30, 90, 180, 360]
  const TC      = DAYS.map(d => Math.round(d * TPD))
  const cpSet   = new Set(TC)
  const maxTC   = TC[TC.length - 1]

  // Run MC
  const snapData = new Map<number, Map<number, number[]>>()  // riskIdx → tradeCount → equities
  RISKS.forEach((_, ri) => { const m = new Map<number, number[]>(); TC.forEach(tc => m.set(tc, [])); snapData.set(ri, m) })
  const ddData   = new Map<number, number[]>(); RISKS.forEach((_, ri) => ddData.set(ri, []))
  const ruinData = new Map<number, number>(); RISKS.forEach((_, ri) => ruinData.set(ri, 0))

  for (let ri = 0; ri < RISKS.length; ri++) {
    process.stderr.write(`MC risk=${RISKS[ri].tag}...`)
    for (let iter = 0; iter < ITERS; iter++) {
      if (iter % 2000 === 0) process.stderr.write(` ${iter}`)
      const { snaps, maxDdPct, ruined } = runPath(pool, maxTC, START, RISKS[ri].r, FEE, STOP_BPS, LEVERAGE, cpSet)
      const sm = snapData.get(ri)!
      for (const tc of TC) sm.get(tc)!.push(snaps.get(tc) ?? 0)
      ddData.get(ri)!.push(maxDdPct)
      if (ruined) ruinData.set(ri, (ruinData.get(ri) ?? 0) + 1)
    }
    process.stderr.write(' done\n')
  }
  process.stderr.write('\n')

  // ── Table: p50 (median) ──
  const COL = 16
  const printRow = (label: string, vals: string[]) => {
    process.stdout.write('  ' + label.padEnd(22))
    vals.forEach(v => process.stdout.write(v.padStart(COL)))
    process.stdout.write('\n')
  }

  console.log(`${'─'.repeat(88)}`)
  console.log('MEDIAN EQUITY  (50th percentile — half of paths are above this)')
  console.log()
  printRow('', DAYS.map(d => d + 'd'))
  printRow('─'.repeat(20), DAYS.map(() => '─'.repeat(14)))
  for (let ri = 0; ri < RISKS.length; ri++) {
    const sm = snapData.get(ri)!
    printRow(RISKS[ri].label, TC.map(tc => fmt(pct(sm.get(tc)!, 0.50))))
  }

  // ── Multiples ──
  console.log()
  console.log('  (as multiples of starting $500)')
  printRow('', DAYS.map(d => d + 'd'))
  printRow('─'.repeat(20), DAYS.map(() => '─'.repeat(14)))
  for (let ri = 0; ri < RISKS.length; ri++) {
    const sm = snapData.get(ri)!
    printRow(RISKS[ri].label, TC.map(tc => xRet(pct(sm.get(tc)!, 0.50), START)))
  }

  // ── p10 / p50 / p90 at key milestones ──
  for (const [dayIdx, days] of DAYS.entries()) {
    const tc = TC[dayIdx]
    console.log()
    console.log(`${'─'.repeat(88)}`)
    console.log(`DAY ${days} — full range (p10 pessimistic / p50 median / p90 optimistic)`)
    console.log()
    console.log('  ' + 'Risk'.padEnd(22) + 'p10 (bad luck)'.padStart(18) + 'p50 (median)'.padStart(18) + 'p90 (good luck)'.padStart(18) + 'worst DD p90'.padStart(14))
    console.log('  ' + '─'.repeat(84))
    for (let ri = 0; ri < RISKS.length; ri++) {
      const sm = snapData.get(ri)!; const snaps = sm.get(tc)!
      const dd = ddData.get(ri)!
      const ruin = (ruinData.get(ri) ?? 0) / ITERS * 100
      const p10v = pct(snaps, 0.10), p50v = pct(snaps, 0.50), p90v = pct(snaps, 0.90)
      const ddp90 = pct(dd, 0.90) * 100
      const ruinStr = ruin > 0.1 ? ` !! ruin=${ruin.toFixed(1)}%` : ''
      process.stdout.write(
        '  ' + RISKS[ri].label.padEnd(22) +
        (fmt(p10v) + ' (' + xRet(p10v,START) + ')').padStart(18) +
        (fmt(p50v) + ' (' + xRet(p50v,START) + ')').padStart(18) +
        (fmt(p90v) + ' (' + xRet(p90v,START) + ')').padStart(18) +
        (ddp90.toFixed(1)+'%').padStart(14) +
        ruinStr + '\n'
      )
    }
  }

  // ── Day-by-day table: first 30 days ──
  console.log()
  console.log(`${'─'.repeat(88)}`)
  console.log('DAY-BY-DAY MEDIAN — FIRST 30 DAYS')
  console.log()
  process.stdout.write('  Day  ')
  for (const { label } of RISKS) process.stdout.write(label.slice(0,3).padStart(16))
  console.log()
  process.stdout.write('  ───  ')
  for (const _ of RISKS) process.stdout.write('─'.repeat(16))
  console.log()

  const dailyIters = 3000
  const dailySnaps: number[][][] = RISKS.map(() => Array.from({length:30}, () => []))

  for (let ri = 0; ri < RISKS.length; ri++) {
    for (let iter = 0; iter < dailyIters; iter++) {
      let eq = START
      const sd = STOP_BPS / 10000
      for (let day = 0; day < 30; day++) {
        for (let t = 0; t < Math.round(TPD); t++) {
          const g = pool[Math.floor(Math.random() * pool.length)]
          const notional = Math.min(eq * RISKS[ri].r / sd, eq * LEVERAGE)
          eq = Math.max(0, eq + notional * ((g - 2*FEE) / 10000))
        }
        dailySnaps[ri][day].push(eq)
      }
    }
  }

  for (let day = 0; day < 30; day++) {
    process.stdout.write('  ' + String(day+1).padStart(3) + '  ')
    for (let ri = 0; ri < RISKS.length; ri++) {
      process.stdout.write(fmt(pct(dailySnaps[ri][day], 0.50)).padStart(16))
    }
    // Spark bar for full Kelly
    const fk = pct(dailySnaps[RISKS.length-1][day], 0.50)
    const fkMax = pct(dailySnaps[RISKS.length-1][29], 0.50)
    const bar = '█'.repeat(Math.min(20, Math.round(fk / fkMax * 20)))
    process.stdout.write('  ' + bar)
    console.log()
  }

  // ── Summary ──
  console.log()
  console.log(`${'═'.repeat(88)}`)
  console.log('SUMMARY — 1 YEAR, NO POSITION CAP')
  console.log(`${'═'.repeat(88)}`)
  console.log()

  const yr360tc = TC[TC.length - 1]
  for (let ri = 0; ri < RISKS.length; ri++) {
    const snaps = snapData.get(ri)!.get(yr360tc)!
    const dd    = ddData.get(ri)!
    const ruins = (ruinData.get(ri) ?? 0) / ITERS * 100
    const p10v  = pct(snaps, 0.10), p50v = pct(snaps, 0.50), p90v = pct(snaps, 0.90)
    const ddMed = pct(dd, 0.50)*100, ddP90 = pct(dd, 0.90)*100
    const notMult = RISKS[ri].r / (STOP_BPS/10000)
    console.log(`  ${RISKS[ri].label}  (${notMult.toFixed(0)}× notional per trade):`)
    console.log(`    Median 1yr:     ${fmt(p50v).padStart(14)}  (${xRet(p50v, START)})`)
    console.log(`    Pessimistic p10: ${fmt(p10v).padStart(13)}  (${xRet(p10v, START)})`)
    console.log(`    Optimistic  p90: ${fmt(p90v).padStart(13)}  (${xRet(p90v, START)})`)
    console.log(`    Max DD:  median=${ddMed.toFixed(0)}%  p90=${ddP90.toFixed(0)}%  ruin=${ruins.toFixed(2)}%`)
    console.log()
  }
}

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