/**
 * BitMEX fee analysis + individual vs combined signal comparison.
 *
 * BitMEX BTCUSD perpetual fees:
 *   Maker: -0.025% = -2.5 bps (REBATE — you get paid)
 *   Taker:  0.075% =  7.5 bps
 *
 * Round-trip maker: -5 bps (net EARN per trade just from fees)
 * This flips every signal that has positive gross return into profitable.
 *
 * Also tests: each signal alone vs combined, to answer which is better.
 */
import fs from 'node:fs'
import readline from 'node:readline'

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

async function load(): Promise<K[]> {
  const bars: K[] = []
  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 tt(v: number[]): { mean: number; t: number; n: number } {
  const n = v.length; if (n < 5) 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): string {
  const a = Math.abs(t); return a > 3.89 ? '****' : 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)
}
function pct(a: number[], p: number) { const s=[...a].sort((x,y)=>x-y); return s[Math.min(s.length-1,Math.floor(s.length*p))]??0 }

// ── Signal helpers ──
function lbRet(bars: K[], i: number, n: number): 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: K): number { return b.h > b.l ? (b.c - b.l) / (b.h - b.l) : 0.5 }
function avgCP(bars: K[], i: number, n: number): 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: K[], i: number, n: number): number {
  if (i < n) return 50
  let up = 0, down = 0
  for (let j = i-n+1; j <= i; j++) { const d = bars[j].c - bars[j-1].c; if (d > 0) up += d; else down -= d }
  return down === 0 ? 100 : 100 - 100 / (1 + up / down)
}

// ── Individual signal definitions ──
interface SigDef {
  name: string
  fn: (bars: K[], i: number) => { dir: number; hold: number } | null
}

const SIGNALS: SigDef[] = [
  {
    name: 'DOW_Thu_short',
    fn: (bars, i) => new Date(bars[i].ts).getUTCDay() === 4 ? { dir: -1, hold: 240 } : null
  },
  {
    name: 'DOW_Wed_long',
    fn: (bars, i) => new Date(bars[i].ts).getUTCDay() === 3 ? { dir: 1, hold: 240 } : null
  },
  {
    name: 'DOW_Sun_long',
    fn: (bars, i) => new Date(bars[i].ts).getUTCDay() === 0 ? { dir: 1, hold: 240 } : null
  },
  {
    name: 'DOW_Mon_long',
    fn: (bars, i) => new Date(bars[i].ts).getUTCDay() === 1 ? { dir: 1, hold: 240 } : null
  },
  {
    name: 'DOW_Fri_short',
    fn: (bars, i) => new Date(bars[i].ts).getUTCDay() === 5 ? { dir: -1, hold: 240 } : null
  },
  {
    name: 'HOUR_21_long',
    fn: (bars, i) => new Date(bars[i].ts).getUTCHours() === 21 ? { dir: 1, hold: 60 } : null
  },
  {
    name: 'BUYP_60m',
    fn: (bars, i) => {
      if (i < 60) return null
      const bp = avgCP(bars, i, 60)
      if (bp > 0.58) return { dir: 1, hold: 240 }
      if (bp < 0.42) return { dir: -1, hold: 240 }
      return null
    }
  },
  {
    name: 'OVERNIGHT_REV',
    fn: (bars, i) => {
      const d = new Date(bars[i].ts)
      if (d.getUTCHours() !== 21 || d.getUTCMinutes() > 15 || i < 480) return null
      const day = lbRet(bars, i, 480)
      return Math.abs(day) > 10 ? { dir: day > 0 ? -1 : 1, hold: 480 } : null
    }
  },
  {
    name: 'US_GAP_REV',
    fn: (bars, i) => {
      const d = new Date(bars[i].ts)
      const h = d.getUTCHours(), m = d.getUTCMinutes()
      if (!((h === 13 || (h === 14 && m <= 15)) && m <= 15) || i < 60) return null
      const gap = lbRet(bars, i, 60)
      return Math.abs(gap) > 5 ? { dir: gap > 0 ? -1 : 1, hold: 120 } : null
    }
  },
  {
    name: 'RSI_REV_60m',
    fn: (bars, i) => {
      if (i < 60) return null
      const r = rsi(bars, i, 60)
      if (r < 30) return { dir: 1, hold: 120 }
      if (r > 70) return { dir: -1, hold: 120 }
      return null
    }
  },
  {
    name: 'REV_24h',
    fn: (bars, i) => {
      if (i < 1440) return null
      const day = lbRet(bars, i, 1440)
      return Math.abs(day) > 30 ? { dir: day > 0 ? -1 : 1, hold: 240 } : null
    }
  },
]

// ── Simulate single signal ──
interface Trade {
  grossBps: number; netBps: number; month: string; side: 'long'|'short'; holdBars: number
}

function simSignal(bars: K[], sigFn: (bars: K[], i: number) => { dir: number; hold: number } | null, feeBps: number): Trade[] {
  const trades: Trade[] = []
  let cd = 0
  for (let i = 1440; i < bars.length; i++) {
    if (i < cd) continue
    const s = sigFn(bars, i)
    if (!s || s.dir === 0) continue
    const hold = s.hold
    if (i + hold >= bars.length) continue
    const gross = s.dir * (bars[i + hold].c - bars[i].c) / bars[i].c * 10000
    trades.push({
      grossBps: gross, netBps: gross - 2 * feeBps,
      month: new Date(bars[i].ts).toISOString().slice(0, 7),
      side: s.dir > 0 ? 'long' : 'short',
      holdBars: hold
    })
    cd = i + hold + 5
  }
  return trades
}

// ── Combined signal (weighted voting) ──
function simCombined(bars: K[], minScore: number, feeBps: number, minHold: number, maxHold: number): Trade[] {
  const trades: Trade[] = []
  let cd = 0
  for (let i = 1440; i < bars.length; i++) {
    if (i < cd) continue
    let wSum = 0, holdW = 0, totalW = 0
    const weights: Record<string, number> = {
      DOW_Thu_short: 34.41, DOW_Wed_long: 19.05, HOUR_21_long: 17.90,
      BUYP_60m: 15.49, DOW_Sun_long: 15.05, REV_24h: 11.29,
      DOW_Fri_short: 10.84, DOW_Mon_long: 10.61,
      OVERNIGHT_REV: 7.84, US_GAP_REV: 6.87, RSI_REV_60m: 4.63,
    }
    for (const sig of SIGNALS) {
      const s = sig.fn(bars, i)
      if (!s || s.dir === 0) continue
      const w = weights[sig.name] ?? 5
      wSum += s.dir * w; holdW += s.hold * w; totalW += w
    }
    const strength = Math.abs(wSum)
    if (strength < minScore) continue
    const dir = wSum > 0 ? 1 : -1
    const hold = Math.max(minHold, Math.min(maxHold, totalW > 0 ? Math.round(holdW / totalW) : 120))
    if (i + hold >= bars.length) continue
    const gross = dir * (bars[i + hold].c - bars[i].c) / bars[i].c * 10000
    trades.push({
      grossBps: gross, netBps: gross - 2 * feeBps,
      month: new Date(bars[i].ts).toISOString().slice(0, 7),
      side: dir > 0 ? 'long' : 'short',
      holdBars: hold
    })
    cd = i + hold + 5
  }
  return trades
}

function equityCurve(trades: Trade[], startEq: number, riskPct: number): { final: number; maxDdPct: number } {
  let eq = startEq, peak = eq, maxDd = 0
  for (const t of trades) {
    const notional = Math.min(eq * riskPct / 0.01, eq * 100)
    eq = Math.max(0, eq + notional * (t.netBps / 10000))
    if (eq > peak) peak = eq
    const dd = peak > 0 ? (peak - eq) / peak : 0
    if (dd > maxDd) maxDd = dd
  }
  return { final: eq, maxDdPct: maxDd }
}

function monthlyBreakdown(trades: Trade[]) {
  const byM = new Map<string, number[]>()
  trades.forEach(t => { if (!byM.has(t.month)) byM.set(t.month, []); byM.get(t.month)!.push(t.netBps) })
  let cum = 0, pos = 0
  for (const [m, ns] of [...byM.entries()].sort()) {
    cum += ns.reduce((a,b)=>a+b,0)
    const mt = tt(ns)
    if (mt.mean > 0) pos++
    console.log(`    ${m}  n=${String(ns.length).padStart(4)}  WR=${(ns.filter(n=>n>0).length/ns.length*100).toFixed(0).padStart(2)}%  mean=${mt.mean.toFixed(2).padStart(7)}  cum=${cum.toFixed(0).padStart(7)}  t=${mt.t.toFixed(2).padStart(6)} ${sig(mt.t)}`)
  }
  console.log(`    Positive months: ${pos}/${byM.size}`)
}

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

  // Fee models
  const FEES = {
    bybit_maker:  { name: 'Bybit maker',   fee:  2.0 },   //  0.02% each side
    bybit_taker:  { name: 'Bybit taker',   fee:  5.5 },   //  0.055%
    bitmex_maker: { name: 'BitMEX maker',  fee: -2.5 },   // -0.025% REBATE
    bitmex_taker: { name: 'BitMEX taker',  fee:  7.5 },   //  0.075%
    zero:         { name: 'Zero fees',      fee:  0   },
  }

  console.log(`${'═'.repeat(100)}`)
  console.log('BITMEX FEE ANALYSIS + INDIVIDUAL vs COMBINED SIGNALS')
  console.log(`${'═'.repeat(100)}`)
  console.log()
  console.log('  Fee structures:')
  console.log('    Bybit maker:   +2.0 bps/side  = +4.0 bps round-trip (you PAY)')
  console.log('    BitMEX maker:  -2.5 bps/side  = -5.0 bps round-trip (you EARN)')
  console.log('    Difference:     9 bps/trade swing')
  console.log()

  // ═══════════════════════════════════════════════
  // PART 1: Each signal individually, all fee models
  // ═══════════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('PART 1: EACH SIGNAL ALONE — across fee models')
  console.log()
  console.log('  ' + 'Signal'.padEnd(20) +
    'Trades'.padStart(7) + '  ' +
    '| Gross'.padStart(8) + '  | Bybit(+4)'.padStart(12) + '  | BitMEX(-5)'.padStart(13) +
    '  | Bybit t'.padStart(10) + '  | BitMEX t'.padStart(11) +
    '  | WR(bitmex)')
  console.log('  ' + '─'.repeat(96))

  const sigResults: { name: string; bybitMean: number; bitmexMean: number; bitmexT: number; trades: number; bitmexTrades: Trade[] }[] = []

  for (const s of SIGNALS) {
    const grossTrades = simSignal(bars, s.fn, 0)
    const bybitTrades = simSignal(bars, s.fn, FEES.bybit_maker.fee)
    const bitmexTrades = simSignal(bars, s.fn, FEES.bitmex_maker.fee)

    const gT = tt(grossTrades.map(t => t.netBps))
    const byT = tt(bybitTrades.map(t => t.netBps))
    const bmT = tt(bitmexTrades.map(t => t.netBps))
    const bmWr = bitmexTrades.length > 0 ? bitmexTrades.filter(t => t.netBps > 0).length / bitmexTrades.length : 0

    sigResults.push({ name: s.name, bybitMean: byT.mean, bitmexMean: bmT.mean, bitmexT: bmT.t, trades: bitmexTrades.length, bitmexTrades })

    console.log('  ' + s.name.padEnd(20) +
      String(grossTrades.length).padStart(7) +
      ('  ' + gT.mean.toFixed(2)).padStart(10) +
      ('  ' + byT.mean.toFixed(2)).padStart(12) +
      ('  ' + bmT.mean.toFixed(2)).padStart(13) +
      ('  ' + byT.t.toFixed(2) + ' ' + sig(byT.t)).padStart(12) +
      ('  ' + bmT.t.toFixed(2) + ' ' + sig(bmT.t)).padStart(13) +
      ('  ' + (bmWr*100).toFixed(0) + '%').padStart(8)
    )
  }

  // ═══════════════════════════════════════════════
  // PART 2: Combined system at BitMEX fees
  // ═══════════════════════════════════════════════
  console.log()
  console.log(`${'─'.repeat(100)}`)
  console.log('PART 2: COMBINED SYSTEM — BitMEX maker fees (-2.5 bps/side)')
  console.log()

  for (const minScore of [0, 5, 10, 15, 20, 25, 30]) {
    const trades = simCombined(bars, minScore, FEES.bitmex_maker.fee, 60, 240)
    if (trades.length < 20) continue
    const nets = trades.map(t => t.netBps)
    const t = tt(nets)
    const wr = nets.filter(n => n > 0).length / nets.length
    const cum = nets.reduce((a,b) => a+b, 0)
    console.log(`  score>=${String(minScore).padStart(2)} hold=60-240m:  n=${String(trades.length).padStart(5)}  WR=${(wr*100).toFixed(0).padStart(2)}%  mean=${t.mean.toFixed(2).padStart(7)}bps  t=${t.t.toFixed(2).padStart(6)} ${sig(t.t).padEnd(4)}  cum=${cum.toFixed(0).padStart(7)}bps`)
  }

  // ═══════════════════════════════════════════════
  // PART 3: Best individual vs best combined head-to-head
  // ═══════════════════════════════════════════════
  console.log()
  console.log(`${'─'.repeat(100)}`)
  console.log('PART 3: HEAD-TO-HEAD at BitMEX maker fees')
  console.log()

  // Best individual by t-stat
  sigResults.sort((a, b) => Math.abs(b.bitmexT) - Math.abs(a.bitmexT))
  const top3Individual = sigResults.slice(0, 3)

  // Best combined
  const bestCombined = simCombined(bars, 10, FEES.bitmex_maker.fee, 60, 240)
  const bestCombinedT = tt(bestCombined.map(t => t.netBps))

  console.log('  BEST INDIVIDUALS:')
  for (const s of top3Individual) {
    const t = tt(s.bitmexTrades.map(t => t.netBps))
    console.log(`    ${s.name.padEnd(22)} mean=${t.mean.toFixed(2).padStart(7)}bps  t=${t.t.toFixed(2).padStart(6)} ${sig(t.t)}  n=${s.trades}`)
  }
  console.log()
  console.log(`  BEST COMBINED (score>=10):`)
  console.log(`    ${'combined'.padEnd(22)} mean=${bestCombinedT.mean.toFixed(2).padStart(7)}bps  t=${bestCombinedT.t.toFixed(2).padStart(6)} ${sig(bestCombinedT.t)}  n=${bestCombined.length}`)

  // ═══════════════════════════════════════════════
  // PART 4: Top 3 individual — monthly breakdown
  // ═══════════════════════════════════════════════
  for (const s of top3Individual) {
    console.log()
    console.log(`${'─'.repeat(100)}`)
    console.log(`SIGNAL: ${s.name} — BitMEX maker — monthly`)
    console.log()
    monthlyBreakdown(s.bitmexTrades)
  }

  // Combined monthly
  console.log()
  console.log(`${'─'.repeat(100)}`)
  console.log(`COMBINED score>=10 — BitMEX maker — monthly`)
  console.log()
  monthlyBreakdown(bestCombined)

  // ═══════════════════════════════════════════════
  // PART 5: Equity curves at BitMEX fees
  // ═══════════════════════════════════════════════
  console.log()
  console.log(`${'─'.repeat(100)}`)
  console.log('EQUITY CURVES — $500 start, BitMEX maker fees')
  console.log()

  const strategies: { name: string; trades: Trade[] }[] = [
    ...top3Individual.map(s => ({ name: s.name, trades: s.bitmexTrades })),
    { name: 'COMBINED score>=10', trades: bestCombined },
    { name: 'COMBINED score>=25', trades: simCombined(bars, 25, FEES.bitmex_maker.fee, 60, 240) },
  ]

  console.log('  ' + 'Strategy'.padEnd(28) + '  risk=2%'.padStart(18) + '  risk=5%'.padStart(18) + '  risk=10%'.padStart(18))
  console.log('  ' + '─'.repeat(82))

  for (const s of strategies) {
    const r2 = equityCurve(s.trades, 500, 0.02)
    const r5 = equityCurve(s.trades, 500, 0.05)
    const r10 = equityCurve(s.trades, 500, 0.10)
    console.log(
      '  ' + s.name.padEnd(28) +
      `  ${money(r2.final)} DD=${(r2.maxDdPct*100).toFixed(0)}%`.padStart(18) +
      `  ${money(r5.final)} DD=${(r5.maxDdPct*100).toFixed(0)}%`.padStart(18) +
      `  ${money(r10.final)} DD=${(r10.maxDdPct*100).toFixed(0)}%`.padStart(18)
    )
  }

  // ═══════════════════════════════════════════════
  // PART 6: Walk-forward at BitMEX fees
  // ═══════════════════════════════════════════════
  console.log()
  console.log(`${'─'.repeat(100)}`)
  console.log('WALK-FORWARD — train first 6 months, test last 6 — BitMEX maker')
  console.log()

  const mid = Math.floor(bars.length / 2)
  const train = bars.slice(0, mid + 1440)
  const test = bars.slice(mid)

  for (const s of SIGNALS) {
    const trainTr = simSignal(train, s.fn, FEES.bitmex_maker.fee)
    const testTr = simSignal(test, s.fn, FEES.bitmex_maker.fee)
    const trainT = tt(trainTr.map(t => t.netBps))
    const testT = tt(testTr.map(t => t.netBps))
    const oos = testT.mean > 0 && testT.t > 1.5 ? '  ✓ OOS HOLD' : testT.mean > 0 ? '  ~ marginal' : '  ✗ fails OOS'
    console.log(`  ${s.name.padEnd(22)}  TRAIN: mean=${trainT.mean.toFixed(2).padStart(7)} t=${trainT.t.toFixed(2).padStart(6)}  |  TEST: mean=${testT.mean.toFixed(2).padStart(7)} t=${testT.t.toFixed(2).padStart(6)} ${sig(testT.t)}${oos}`)
  }

  // Combined walk-forward
  for (const minScore of [10, 20, 25]) {
    const trainTr = simCombined(train, minScore, FEES.bitmex_maker.fee, 60, 240)
    const testTr = simCombined(test, minScore, FEES.bitmex_maker.fee, 60, 240)
    const trainT = tt(trainTr.map(t => t.netBps))
    const testT = tt(testTr.map(t => t.netBps))
    const oos = testT.mean > 0 && testT.t > 1.5 ? '  ✓ OOS HOLD' : testT.mean > 0 ? '  ~ marginal' : '  ✗ fails OOS'
    console.log(`  ${'COMBINED s>=' + minScore}`.padEnd(24) + `  TRAIN: mean=${trainT.mean.toFixed(2).padStart(7)} t=${trainT.t.toFixed(2).padStart(6)}  |  TEST: mean=${testT.mean.toFixed(2).padStart(7)} t=${testT.t.toFixed(2).padStart(6)} ${sig(testT.t)}${oos}`)
  }

  // ═══════════════════════════════════════════════
  // SUMMARY
  // ═══════════════════════════════════════════════
  console.log()
  console.log(`${'═'.repeat(100)}`)
  console.log('VERDICT')
  console.log(`${'═'.repeat(100)}`)
  console.log()
  console.log('  Bybit maker (2bps/side):   All signals marginal or negative. Not tradeable.')
  console.log('  BitMEX maker (-2.5bps/side): Fee rebate adds 9bps/trade vs Bybit.')
  console.log()

  // Rank strategies by BitMEX t-stat
  const allStrats = [
    ...sigResults.map(s => ({ name: s.name, t: s.bitmexT, mean: s.bitmexMean, n: s.trades })),
    ...([10, 20, 25] as number[]).map(ms => {
      const tr = simCombined(bars, ms, FEES.bitmex_maker.fee, 60, 240)
      const t = tt(tr.map(t => t.netBps))
      return { name: `COMBINED s>=${ms}`, t: t.t, mean: t.mean, n: tr.length }
    })
  ].sort((a, b) => b.t - a.t)

  console.log('  RANKED BY t-stat (BitMEX maker):')
  for (const s of allStrats.slice(0, 10)) {
    const surv = s.mean > 0 && s.t > 2 ? '✓' : '✗'
    console.log(`    ${surv} ${s.name.padEnd(25)} mean=${s.mean.toFixed(2).padStart(7)}bps  t=${s.t.toFixed(2).padStart(6)} ${sig(s.t)}  n=${s.n}`)
  }
  console.log()
}

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