/**
 * Combined strategy: merge all 8 fee-surviving signals into a single
 * always-on scoring system. Each signal contributes a directional vote
 * weighted by its t-stat. When the combined score exceeds a threshold,
 * enter in that direction. Hold for the dominant signal's horizon.
 *
 * Signals:
 *   1. DOW_Wed/Sun bullish (4h)         — calendar
 *   2. HOUR_21 bullish (1h)             — intraday seasonality
 *   3. BUYP 60m continuation (4h)       — order flow proxy
 *   4. OVERNIGHT_REV (8h)               — fade daytime move
 *   5. US_GAP_REV (2h)                  — fade US open gap
 *   6. RSI_REV 60m (2h)                 — mean reversion
 *
 * Tests the combined system with simulation, monthly breakdown, equity curves.
 */
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)
}

// ── Helpers ──
function lb(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 ret(bars: K[], i: number, fwd: number): number {
  return i+fwd < bars.length && bars[i].c > 0 ? (bars[i+fwd].c - bars[i].c) / bars[i].c * 10000 : NaN
}
function closePosition(b: K): number { return b.h > b.l ? (b.c - b.l) / (b.h - b.l) : 0.5 }
function avgClosePos(bars: K[], i: number, n: number): number {
  let s = 0; for (let j = Math.max(0,i-n+1); j <= i; j++) s += closePosition(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)
}

// ── Signal definitions ──
// Each returns: { dir: 1 (long) | -1 (short) | 0 (no signal), weight, holdBars }
interface Vote { dir: number; weight: number; holdBars: number; name: string }

function getVotes(bars: K[], i: number): Vote[] {
  const votes: Vote[] = []
  const d = new Date(bars[i].ts)
  const hour = d.getUTCHours()
  const minute = d.getUTCMinutes()
  const dow = d.getUTCDay()

  // 1. DOW_Wed bullish (t=19.05)
  if (dow === 3) votes.push({ dir: 1, weight: 19.05, holdBars: 240, name: 'DOW_Wed' })

  // 2. DOW_Sun bullish (t=15.05)
  if (dow === 0) votes.push({ dir: 1, weight: 15.05, holdBars: 240, name: 'DOW_Sun' })

  // 2b. DOW_Thu bearish (t=-34.41, strongest single signal!)
  if (dow === 4) votes.push({ dir: -1, weight: 34.41, holdBars: 240, name: 'DOW_Thu' })

  // 2c. DOW_Fri bearish (t=-10.84)
  if (dow === 5) votes.push({ dir: -1, weight: 10.84, holdBars: 240, name: 'DOW_Fri' })

  // 2d. DOW_Mon bullish (t=10.61)
  if (dow === 1) votes.push({ dir: 1, weight: 10.61, holdBars: 240, name: 'DOW_Mon' })

  // 3. HOUR_21 bullish (t=17.90) — only at that hour
  if (hour === 21) votes.push({ dir: 1, weight: 17.90, holdBars: 60, name: 'HOUR_21' })

  // 3b. HOUR_20 bullish (t=9.49)
  if (hour === 20) votes.push({ dir: 1, weight: 9.49, holdBars: 60, name: 'HOUR_20' })

  // 3c. HOUR_23 bearish (t=-8.94)
  if (hour === 23) votes.push({ dir: -1, weight: 8.94, holdBars: 30, name: 'HOUR_23' })

  // 4. BUYP 60m continuation (t=15.49) — buy pressure proxy
  if (i >= 60) {
    const bp = avgClosePos(bars, i, 60)
    if (bp > 0.58) votes.push({ dir: 1, weight: 15.49, holdBars: 240, name: 'BUYP_long' })
    if (bp < 0.42) votes.push({ dir: -1, weight: 15.49, holdBars: 240, name: 'BUYP_short' })
  }

  // 5. OVERNIGHT_REV (t=7.84) — fade daytime move at 21:00 UTC
  if (hour === 21 && minute < 15 && i >= 480) {
    const dayMove = lb(bars, i, 480)
    if (Math.abs(dayMove) > 10) votes.push({ dir: dayMove > 0 ? -1 : 1, weight: 7.84, holdBars: 480, name: 'OVERNIGHT_REV' })
  }

  // 6. US_GAP_REV (t=6.87) — fade gap at US open (13:00-14:30 UTC)
  if ((hour === 13 || (hour === 14 && minute <= 15)) && minute <= 15 && i >= 60) {
    const gap = lb(bars, i, 60)
    if (Math.abs(gap) > 5) votes.push({ dir: gap > 0 ? -1 : 1, weight: 6.87, holdBars: 120, name: 'US_GAP_REV' })
  }

  // 7. RSI_REV 60m (t=4.63) — oversold/overbought reversion
  if (i >= 60) {
    const r = rsi(bars, i, 60)
    if (r < 30) votes.push({ dir: 1, weight: 4.63, holdBars: 120, name: 'RSI_oversold' })
    if (r > 70) votes.push({ dir: -1, weight: 4.63, holdBars: 120, name: 'RSI_overbought' })
  }

  // 8. 24h mean-reversion (t=11.29, but only 1.42bps gross — marginal, but helps bias)
  if (i >= 1440) {
    const dayRet = lb(bars, i, 1440)
    if (Math.abs(dayRet) > 30) votes.push({ dir: dayRet > 0 ? -1 : 1, weight: 5.0, holdBars: 240, name: 'REV_24h' })
  }

  return votes
}

// ── Composite score ──
interface CompositeSignal {
  score: number        // weighted sum of votes, positive = long
  strength: number     // absolute score
  holdBars: number     // weighted average hold
  dir: 1 | -1 | 0
  topVote: string
  voteCount: number
}

function computeSignal(bars: K[], i: number): CompositeSignal {
  const votes = getVotes(bars, i)
  if (votes.length === 0) return { score: 0, strength: 0, holdBars: 0, dir: 0, topVote: '', voteCount: 0 }

  let wSum = 0, holdW = 0, totalW = 0
  for (const v of votes) {
    wSum += v.dir * v.weight
    holdW += v.holdBars * v.weight
    totalW += v.weight
  }

  const avgHold = totalW > 0 ? Math.round(holdW / totalW) : 60
  const strength = Math.abs(wSum)
  const dir = wSum > 0 ? 1 : wSum < 0 ? -1 : 0
  const topVote = votes.sort((a, b) => b.weight - a.weight)[0]?.name ?? ''

  return { score: wSum, strength, holdBars: avgHold, dir: dir as 1|-1|0, topVote, voteCount: votes.length }
}

// ── Trade simulation ──
interface Trade {
  bar: number; side: 'long'|'short'; holdBars: number
  entryPrice: number; exitPrice: number
  grossBps: number; netBps: number
  score: number; topVote: string; voteCount: number
  month: string
}

function simulate(
  bars: K[], minScore: number, feeBps: number, minHold: number, maxHold: number
): Trade[] {
  const trades: Trade[] = []
  let cooldownUntil = 0

  for (let i = 1440; i < bars.length - maxHold; i++) {
    if (i < cooldownUntil) continue

    const sig = computeSignal(bars, i)
    if (sig.strength < minScore || sig.dir === 0) continue

    const hold = Math.max(minHold, Math.min(maxHold, sig.holdBars))
    const side: 'long'|'short' = sig.dir > 0 ? 'long' : 'short'
    const entry = bars[i].c
    const exit = bars[i + hold].c
    const grossBps = sig.dir * (exit - entry) / entry * 10000
    const netBps = grossBps - 2 * feeBps
    const month = new Date(bars[i].ts).toISOString().slice(0, 7)

    trades.push({
      bar: i, side, holdBars: hold,
      entryPrice: entry, exitPrice: exit,
      grossBps, netBps,
      score: sig.score, topVote: sig.topVote, voteCount: sig.voteCount,
      month
    })

    cooldownUntil = i + hold + 5
  }
  return trades
}

function equityCurve(
  trades: Trade[], startEq: number, riskPct: number, feeBps: number
): { finalEq: number; maxDdPct: number; maxDdUsd: number; curve: number[] } {
  let eq = startEq, peak = eq, maxDdPct = 0, maxDdUsd = 0
  const curve = [eq]
  for (const t of trades) {
    const stopDist = 0.01  // 100bps
    const notional = Math.min(eq * riskPct / stopDist, eq * 100)
    eq = Math.max(0, eq + notional * (t.netBps / 10000))
    if (eq > peak) peak = eq
    const ddPct = peak > 0 ? (peak - eq) / peak : 0
    if (ddPct > maxDdPct) maxDdPct = ddPct
    const ddUsd = peak - eq
    if (ddUsd > maxDdUsd) maxDdUsd = ddUsd
    curve.push(eq)
  }
  return { finalEq: eq, maxDdPct, maxDdUsd, curve }
}

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

  const FEE = 2

  console.log(`${'═'.repeat(100)}`)
  console.log('COMBINED STRATEGY — ALL FEE-SURVIVING SIGNALS')
  console.log(`${'═'.repeat(100)}`)
  console.log()

  // ── Phase 1: Sweep minimum score threshold ──
  console.log('PHASE 1: Score threshold sweep')
  console.log()
  console.log('  ' + 'Config'.padEnd(35) + 'Trades'.padStart(7) + '  WR'.padStart(5) + '  Mean net'.padStart(10) + '  t-stat'.padStart(8) + '  Cum bps'.padStart(9) + '  AvgHold'.padStart(9))
  console.log('  ' + '─'.repeat(85))

  for (const minScore of [0, 5, 10, 15, 20, 25, 30, 40, 50]) {
    for (const [minH, maxH] of [[30, 120], [60, 240], [60, 480]] as [number,number][]) {
      const trades = simulate(bars, minScore, FEE, minH, maxH)
      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)
      const avgHold = trades.reduce((s, t) => s + t.holdBars, 0) / trades.length
      console.log(
        '  ' + `score>=${minScore} hold=${minH}-${maxH}m`.padEnd(35) +
        String(trades.length).padStart(7) +
        (wr * 100).toFixed(0).padStart(5) + '%' +
        t.mean.toFixed(2).padStart(10) +
        t.t.toFixed(2).padStart(8) + ' ' + sig(t.t).padEnd(4) +
        cum.toFixed(0).padStart(9) +
        avgHold.toFixed(0).padStart(8) + 'min'
      )
    }
  }

  // ── Phase 2: Best config — monthly breakdown ──
  console.log()
  console.log(`${'─'.repeat(100)}`)
  console.log('PHASE 2: Best configs — monthly breakdown')
  console.log()

  const bestConfigs: { label: string; minScore: number; minH: number; maxH: number }[] = [
    { label: 'WIDE score>=5 hold=60-240',   minScore: 5,  minH: 60, maxH: 240 },
    { label: 'MED score>=15 hold=60-240',    minScore: 15, minH: 60, maxH: 240 },
    { label: 'TIGHT score>=25 hold=60-240',  minScore: 25, minH: 60, maxH: 240 },
    { label: 'LONG score>=10 hold=60-480',   minScore: 10, minH: 60, maxH: 480 },
  ]

  for (const cfg of bestConfigs) {
    const trades = simulate(bars, cfg.minScore, FEE, cfg.minH, cfg.maxH)
    const nets = trades.map(t => t.netBps)
    const t = tt(nets)
    console.log(`  ${cfg.label}:  n=${trades.length}  mean=${t.mean.toFixed(2)}bps  t=${t.t.toFixed(2)} ${sig(t.t)}`)
    console.log()
    console.log('  Month      Trades  WR    Mean net    Cumulative    t')
    console.log('  ' + '─'.repeat(60))

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

    // Vote breakdown
    const voteCounts = new Map<string, number>()
    trades.forEach(t => voteCounts.set(t.topVote, (voteCounts.get(t.topVote) ?? 0) + 1))
    console.log('  Top vote distribution: ' + [...voteCounts.entries()].sort((a,b)=>b[1]-a[1]).map(([k,v])=>`${k}=${v}`).join(' '))
    console.log()
  }

  // ── Phase 3: Equity curves ──
  console.log(`${'─'.repeat(100)}`)
  console.log('PHASE 3: Equity simulation — $500 start, 100x leverage')
  console.log()

  const bestTrades = simulate(bars, 15, FEE, 60, 240)

  for (const riskPct of [0.02, 0.05, 0.10]) {
    const { finalEq, maxDdPct, maxDdUsd } = equityCurve(bestTrades, 500, riskPct, FEE)
    const ret = ((finalEq - 500) / 500 * 100)
    console.log(`  risk=${(riskPct*100).toFixed(0)}%:  $500 → ${money(finalEq)} (${ret >= 0 ? '+' : ''}${ret.toFixed(0)}%)  maxDD=${(maxDdPct*100).toFixed(1)}% (${money(maxDdUsd)})`)
  }

  // ── Phase 4: Fee sensitivity ──
  console.log()
  console.log(`${'─'.repeat(100)}`)
  console.log('PHASE 4: Fee sensitivity (best config)')
  console.log()

  for (const fee of [0, 1, 2, 3, 4, 5.5]) {
    const trades = simulate(bars, 15, fee, 60, 240)
    const nets = trades.map(t => t.netBps)
    const t = tt(nets)
    console.log(`  fee=${String(fee).padStart(4)}bps/side:  mean=${t.mean.toFixed(2).padStart(7)}bps  t=${t.t.toFixed(2).padStart(6)} ${sig(t.t)}  WR=${(nets.filter(n=>n>0).length/nets.length*100).toFixed(0)}%`)
  }

  // ── Phase 5: Walk-forward validation ──
  console.log()
  console.log(`${'─'.repeat(100)}`)
  console.log('PHASE 5: Walk-forward (train on first 6 months, test on last 6)')
  console.log()

  const midpoint = Math.floor(bars.length / 2)
  const trainBars = bars.slice(0, midpoint + 1440)
  const testBars = bars.slice(midpoint)

  for (const minScore of [5, 10, 15, 20, 25]) {
    const trainTrades = simulate(trainBars, minScore, FEE, 60, 240)
    const testTrades = simulate(testBars, minScore, FEE, 60, 240)
    const trainT = tt(trainTrades.map(t => t.netBps))
    const testT = tt(testTrades.map(t => t.netBps))
    console.log(`  score>=${String(minScore).padStart(2)}:  TRAIN mean=${trainT.mean.toFixed(2).padStart(7)} t=${trainT.t.toFixed(2).padStart(6)} n=${trainTrades.length}  |  TEST mean=${testT.mean.toFixed(2).padStart(7)} t=${testT.t.toFixed(2).padStart(6)} n=${testTrades.length} ${sig(testT.t)}`)
  }

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

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