/**
 * Long-horizon edge search: 5min to 4h holds.
 *
 * At 4bps round-trip, we need gross > 4bps/trade. Short horizons can't
 * deliver that. Longer holds give more room for directional accuracy to
 * compound into fee-surviving PnL.
 *
 * Tests: momentum, mean-reversion, trend-following, breakout, VWAP deviation,
 * volume-regime conditioning, and combinations — all at 5m/15m/30m/1h/2h/4h.
 */
import fs from 'node:fs'
import readline from 'node:readline'
import type { Bar10s } from '../core/strategy.js'

interface RBar extends Bar10s {
  ts: number
  exBuyVol: Record<string, number>
  exSellVol: Record<string, number>
}

// ── 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 mean = v.reduce((a, b) => a + b, 0) / n
  const var_ = v.reduce((s, x) => s + (x - mean) ** 2, 0) / (n - 1)
  const se = Math.sqrt(var_ / n)
  return { mean, t: se > 0 ? mean / se : 0, n }
}

function sig(t: number): string {
  const a = Math.abs(t); return a > 3.29 ? '***' : a > 2.58 ? '**' : a > 1.96 ? '*' : ''
}
function pct(arr: number[], p: number): number {
  const s = [...arr].sort((a, b) => a - b); return s[Math.min(Math.floor(s.length * p), s.length - 1)]
}
function pad(s: string, w = 9): string { return s.padStart(w) }
function fmt(v: number, d = 2): string { return v.toFixed(d) }

// ── Data ──

async function loadAll(): Promise<Map<string, RBar[]>> {
  const sessions = new Map<string, RBar[]>()
  const histDir = 'data/historical'
  for (const date of fs.readdirSync(histDir).filter(d => {
    try { return fs.statSync(`${histDir}/${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(`${histDir}/${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, ex = d.data.exchange, side = d.data.side, vol = d.data.notionalUsd
      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}, exBuyVol:{[ex]:side==='buy'?vol:0}, exSellVol:{[ex]:side==='sell'?vol:0} })
      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; e.exBuyVol[ex]=(e.exBuyVol[ex]||0)+(side==='buy'?vol:0); e.exSellVol[ex]=(e.exSellVol[ex]||0)+(side==='sell'?vol:0) }
    }
    const bars = [...m.values()].sort((a, b) => a.ts - b.ts)
    sessions.set(date, bars)
    process.stderr.write(` ${bars.length}\n`)
  }
  return sessions
}

// ── Features ──

function mom(bars: RBar[], 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 fwd(bars: RBar[], i: number, n: number): number {
  return i+n < bars.length && bars[i].c > 0 ? (bars[i+n].c - bars[i].c) / bars[i].c * 10000 : NaN
}
function rv(bars: RBar[], i: number, lb = 6): number {
  if (i < lb) return 0
  const r: number[] = []
  for (let j = i-lb+1; j <= i; j++) if (bars[j-1].c > 0) r.push((bars[j].c - bars[j-1].c) / bars[j-1].c * 10000)
  if (r.length < 3) return 0
  const m = r.reduce((a,b)=>a+b,0)/r.length
  return Math.sqrt(r.reduce((s,x)=>s+(x-m)**2,0)/(r.length-1))
}
function delta(b: RBar): number { const v = b.buyVol+b.sellVol; return v > 0 ? (b.buyVol-b.sellVol)/v : 0 }
function rollingDelta(bars: RBar[], i: number, n: number): number {
  let bv=0, sv=0; for (let j=Math.max(0,i-n+1); j<=i; j++) { bv+=bars[j].buyVol; sv+=bars[j].sellVol }
  const t=bv+sv; return t > 0 ? (bv-sv)/t : 0
}
function rollingVol(bars: RBar[], i: number, n: number): number {
  let v = 0; for (let j=Math.max(0,i-n+1); j<=i; j++) v += bars[j].buyVol + bars[j].sellVol; return v
}
function exDelta(b: RBar, ex: string): number {
  const bv = b.exBuyVol[ex]||0, sv = b.exSellVol[ex]||0, t = bv+sv; return t > 0 ? (bv-sv)/t : 0
}
/** VWAP over last N bars */
function vwap(bars: RBar[], i: number, n: number): number {
  let sumPV = 0, sumV = 0
  for (let j = Math.max(0, i-n+1); j <= i; j++) {
    const v = bars[j].buyVol + bars[j].sellVol
    sumPV += bars[j].c * v; sumV += v
  }
  return sumV > 0 ? sumPV / sumV : bars[i].c
}
/** Highest close in last N bars */
function highN(bars: RBar[], i: number, n: number): number {
  let h = -Infinity; for (let j = Math.max(0, i-n+1); j <= i; j++) h = Math.max(h, bars[j].h); return h
}
/** Lowest close in last N bars */
function lowN(bars: RBar[], i: number, n: number): number {
  let l = Infinity; for (let j = Math.max(0, i-n+1); j <= i; j++) l = Math.min(l, bars[j].l); return l
}
/** EMA */
function ema(bars: RBar[], i: number, period: number): number {
  const k = 2 / (period + 1); let e = bars[Math.max(0, i - period * 3)].c
  for (let j = Math.max(0, i - period * 3) + 1; j <= i; j++) e = bars[j].c * k + e * (1 - k)
  return e
}
/** ATR-like: average bar range */
function atr(bars: RBar[], i: number, n: number): number {
  let s = 0; for (let j = Math.max(0, i-n+1); j <= i; j++) s += bars[j].h - bars[j].l; return s / Math.min(n, i+1)
}
/** Rolling buy ratio in N bars */
function buyRatio(bars: RBar[], i: number, n: number): number {
  let bv = 0, sv = 0
  for (let j = Math.max(0, i-n+1); j <= i; j++) { bv += bars[j].buyVol; sv += bars[j].sellVol }
  const t = bv + sv; return t > 0 ? bv / t : 0.5
}

// ── Simulation ──

interface SimTrade { side: 'long'|'short'; grossBps: number; netBps: number; session: string }

function simulate(
  bars: RBar[], session: string,
  signalFn: (bars: RBar[], i: number) => 'long'|'short'|null,
  holdBars: number, feeBps: number, cooldownBars: number
): SimTrade[] {
  const trades: SimTrade[] = []
  let lastExit = -cooldownBars
  for (let i = Math.max(60, holdBars); i < bars.length - holdBars; i++) {
    if (i - lastExit < cooldownBars) continue
    const dir = signalFn(bars, i)
    if (!dir) continue
    const entry = bars[i].c, exit = bars[i+holdBars].c
    const grossBps = dir === 'long' ? (exit-entry)/entry*10000 : (entry-exit)/entry*10000
    trades.push({ side: dir, grossBps, netBps: grossBps - 2*feeBps, session })
    lastExit = i + holdBars
  }
  return trades
}

function report(name: string, trades: SimTrade[], showSessions = false) {
  if (trades.length < 3) { console.log(`  ${name.padEnd(45)} n=${trades.length} (too few)`); return }
  const net = trades.map(t => t.netBps), gross = trades.map(t => t.grossBps)
  const tN = tTest(net), tG = tTest(gross)
  const wr = net.filter(r => r > 0).length / net.length * 100
  let maxDd = 0, cum = 0, peak = 0
  for (const r of net) { cum += r; if (cum > peak) peak = cum; maxDd = Math.max(maxDd, peak - cum) }
  const sharpe = tN.mean / (Math.sqrt(net.reduce((s,x) => s + (x - tN.mean)**2, 0) / (net.length-1)) || 1)
  console.log(`  ${name.padEnd(45)} n=${String(tN.n).padStart(4)} WR=${fmt(wr,0).padStart(3)}% gross=${pad(fmt(tG.mean))}bps net=${pad(fmt(tN.mean))}bps t=${pad(fmt(tN.t))} cum=${pad(fmt(cum,0),6)}bps maxDD=${pad(fmt(maxDd,0),6)}bps sharpe=${fmt(sharpe,3)} ${sig(tN.t)}`)

  if (showSessions) {
    const bySession = new Map<string, number[]>()
    for (const t of trades) { if (!bySession.has(t.session)) bySession.set(t.session, []); bySession.get(t.session)!.push(t.netBps) }
    let pos = 0
    for (const [s, rets] of [...bySession.entries()].sort()) {
      const tt = tTest(rets)
      if (tt.mean > 0) pos++
      console.log(`    ${s} n=${String(rets.length).padStart(3)} mean=${pad(fmt(tt.mean))}bps WR=${fmt(rets.filter(r=>r>0).length/rets.length*100,0)}% cum=${fmt(rets.reduce((a,b)=>a+b,0),1)}bps`)
    }
    console.log(`    Positive sessions: ${pos}/${bySession.size}`)
  }
}

// ── Horizons (bars = 10s each) ──
const HORIZONS: [string, number][] = [
  ['5m', 30], ['15m', 90], ['30m', 180], ['1h', 360], ['2h', 720], ['4h', 1440]
]
const FEE = 2 // maker each way

async function main() {
  const sessions = await loadAll()

  console.log(`\n${'═'.repeat(100)}`)
  console.log(`LONG-HORIZON EDGE SEARCH — ${sessions.size} sessions, 10s bars, holds from 5min to 4h`)
  console.log(`Fee model: ${FEE}bps maker each way = ${2*FEE}bps round trip`)
  console.log(`${'═'.repeat(100)}\n`)

  // ═══════════════════════════════════════════
  // 1. PURE MOMENTUM: price moved over lookback → continue?
  // ═══════════════════════════════════════════
  console.log('1. MOMENTUM (trend-following)')
  console.log('   Lookback price move → bet on continuation')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    console.log(`  Hold=${hName}:`)
    for (const lbMult of [1, 2, 3, 6]) {
      const lb = hBars * lbMult
      const all: SimTrade[] = []
      for (const [name, bars] of sessions) {
        all.push(...simulate(bars, name, (b, i) => {
          const m = mom(b, i, lb)
          if (Math.abs(m) < 5) return null
          return m > 0 ? 'long' : 'short'
        }, hBars, FEE, hBars + 3))
      }
      report(`lb=${fmt(lb*10/60,0)}min`, all)
    }
    console.log()
  }

  // ═══════════════════════════════════════════
  // 2. MEAN REVERSION at longer horizons
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('2. MEAN REVERSION')
  console.log('   Large price move over lookback → bet on reversal')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    console.log(`  Hold=${hName}:`)
    for (const lbMult of [1, 2, 3]) {
      const lb = hBars * lbMult
      const all: SimTrade[] = []
      for (const [name, bars] of sessions) {
        const moves = bars.slice(lb+60).map((_, j) => Math.abs(mom(bars, j+lb+60, lb))).filter(v => v > 0)
        const thresh = pct(moves, 0.85)
        all.push(...simulate(bars, name, (b, i) => {
          const m = mom(b, i, lb)
          if (Math.abs(m) < thresh) return null
          return m > 0 ? 'short' : 'long' // reversion
        }, hBars, FEE, hBars + 3))
      }
      report(`lb=${fmt(lb*10/60,0)}min`, all)
    }
    console.log()
  }

  // ═══════════════════════════════════════════
  // 3. BREAKOUT: new N-bar high/low
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('3. BREAKOUT')
  console.log('   Price breaks N-bar high → long, breaks low → short')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    console.log(`  Hold=${hName}:`)
    for (const lbMult of [2, 6, 12]) {
      const lb = hBars * lbMult
      const all: SimTrade[] = []
      for (const [name, bars] of sessions) {
        all.push(...simulate(bars, name, (b, i) => {
          if (i < lb + 1) return null
          const prevHigh = highN(b, i-1, lb), prevLow = lowN(b, i-1, lb)
          if (b[i].c > prevHigh) return 'long'
          if (b[i].c < prevLow) return 'short'
          return null
        }, hBars, FEE, hBars + 3))
      }
      report(`lb=${fmt(lb*10/60,0)}min`, all)
    }
    console.log()
  }

  // ═══════════════════════════════════════════
  // 4. VWAP DEVIATION: price far from VWAP → revert to VWAP
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('4. VWAP MEAN-REVERSION')
  console.log('   Price deviates from N-bar VWAP → bet on reversion to VWAP')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    console.log(`  Hold=${hName}:`)
    for (const vwapPeriod of [180, 360, 720, 1440]) {
      const all: SimTrade[] = []
      for (const [name, bars] of sessions) {
        const devs = bars.slice(vwapPeriod+60).map((_, j) => {
          const idx = j + vwapPeriod + 60
          const v = vwap(bars, idx, vwapPeriod)
          return v > 0 ? Math.abs((bars[idx].c - v) / v * 10000) : 0
        }).filter(d => d > 0)
        const thresh = pct(devs, 0.85)
        all.push(...simulate(bars, name, (b, i) => {
          if (i < vwapPeriod) return null
          const v = vwap(b, i, vwapPeriod)
          const dev = v > 0 ? (b[i].c - v) / v * 10000 : 0
          if (Math.abs(dev) < thresh) return null
          return dev > 0 ? 'short' : 'long' // revert to VWAP
        }, hBars, FEE, hBars + 3))
      }
      report(`vwap=${fmt(vwapPeriod*10/60,0)}min`, all)
    }
    console.log()
  }

  // ═══════════════════════════════════════════
  // 5. EMA CROSSOVER: fast EMA crosses slow EMA
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('5. EMA CROSSOVER (trend)')
  console.log('   Fast EMA > Slow EMA → long, else short')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    console.log(`  Hold=${hName}:`)
    for (const [fast, slow] of [[30,90],[60,180],[90,360],[180,720]] as [number,number][]) {
      const all: SimTrade[] = []
      for (const [name, bars] of sessions) {
        all.push(...simulate(bars, name, (b, i) => {
          if (i < slow * 3) return null
          const f = ema(b, i, fast), s = ema(b, i, slow)
          const prevF = ema(b, i-1, fast), prevS = ema(b, i-1, slow)
          // Only trade on cross
          if ((f > s) !== (prevF > prevS)) return f > s ? 'long' : 'short'
          return null
        }, hBars, FEE, hBars + 3))
      }
      report(`ema ${fmt(fast*10/60,0)}/${fmt(slow*10/60,0)}min`, all)
    }
    console.log()
  }

  // ═══════════════════════════════════════════
  // 6. VOLUME-WEIGHTED MOMENTUM
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('6. VOLUME-WEIGHTED MOMENTUM')
  console.log('   Strong momentum + high volume → continuation')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    const all: SimTrade[] = []
    for (const [name, bars] of sessions) {
      const lb = hBars * 2
      all.push(...simulate(bars, name, (b, i) => {
        if (i < lb) return null
        const m = mom(b, i, lb)
        if (Math.abs(m) < 10) return null
        // Volume must be above average
        const recentVol = rollingVol(b, i, lb)
        const avgVol = rollingVol(b, i - lb, lb)
        if (avgVol <= 0 || recentVol / avgVol < 1.5) return null
        return m > 0 ? 'long' : 'short'
      }, hBars, FEE, hBars + 3))
    }
    report(`hold=${hName} lb=2x`, all)
  }
  console.log()

  // ═══════════════════════════════════════════
  // 7. CUMULATIVE DELTA DIVERGENCE at longer timeframes
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('7. CUMULATIVE DELTA DIVERGENCE')
  console.log('   Price goes up but cumulative delta goes down → short (and vice versa)')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    const all: SimTrade[] = []
    for (const [name, bars] of sessions) {
      const lb = hBars
      all.push(...simulate(bars, name, (b, i) => {
        if (i < lb) return null
        const priceMom = mom(b, i, lb)
        const delt = rollingDelta(b, i, lb)
        if (Math.abs(priceMom) < 5 || Math.abs(delt) < 0.05) return null
        // Divergence: price and delta disagree
        if (priceMom * delt > 0) return null // they agree → skip
        // Bet on delta direction (the order flow is "right")
        return delt > 0 ? 'long' : 'short'
      }, hBars, FEE, hBars + 3))
    }
    report(`hold=${hName}`, all)
  }
  console.log()

  // ═══════════════════════════════════════════
  // 8. REGIME-GATED MOMENTUM: only trade momentum in trending regimes
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('8. REGIME-GATED MOMENTUM')
  console.log('   Momentum signal, but only when recent ATR is elevated (trending regime)')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    console.log(`  Hold=${hName}:`)
    for (const atrMult of [1.5, 2.0, 3.0]) {
      const lb = hBars * 2
      const all: SimTrade[] = []
      for (const [name, bars] of sessions) {
        // Compute rolling ATR baseline
        const atrBaseline = atr(bars, Math.min(1440, bars.length - 1), 1440) // daily ATR baseline
        all.push(...simulate(bars, name, (b, i) => {
          if (i < lb) return null
          const curAtr = atr(b, i, lb)
          if (curAtr < atrBaseline * atrMult) return null // not trending enough
          const m = mom(b, i, lb)
          if (Math.abs(m) < 5) return null
          return m > 0 ? 'long' : 'short'
        }, hBars, FEE, hBars + 3))
      }
      report(`atr>${atrMult}x`, all)
    }
    console.log()
  }

  // ═══════════════════════════════════════════
  // 9. BUY-RATIO EXTREME → FADE
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('9. EXTREME BUY RATIO → FADE')
  console.log('   When buy ratio is extreme, fade it (contrarian)')
  console.log()

  for (const [hName, hBars] of HORIZONS) {
    const all: SimTrade[] = []
    for (const [name, bars] of sessions) {
      const lb = hBars
      all.push(...simulate(bars, name, (b, i) => {
        if (i < lb) return null
        const br = buyRatio(b, i, lb)
        if (br > 0.60) return 'short' // too many buyers → short
        if (br < 0.40) return 'long'  // too many sellers → long
        return null
      }, hBars, FEE, hBars + 3))
    }
    report(`hold=${hName}`, all)
  }
  console.log()

  // ═══════════════════════════════════════════
  // 10. BEST-OF: find the single best combo and show session detail
  // ═══════════════════════════════════════════
  console.log(`${'═'.repeat(100)}`)
  console.log('10. BEST CANDIDATES — SESSION-LEVEL DETAIL')
  console.log(`${'═'.repeat(100)}\n`)

  // Run the top combos with session detail
  const candidates: { name: string; fn: (bars: RBar[], i: number) => 'long'|'short'|null; hold: number }[] = [
    // Momentum 2x lookback
    ...HORIZONS.map(([hName, hBars]) => ({
      name: `mom_2x hold=${hName}`,
      fn: (bars: RBar[], i: number) => {
        const lb = hBars * 2; if (i < lb) return null
        const m = mom(bars, i, lb); return Math.abs(m) < 5 ? null : m > 0 ? 'long' as const : 'short' as const
      },
      hold: hBars
    })),
    // VWAP reversion
    ...HORIZONS.filter(([,h]) => h <= 720).map(([hName, hBars]) => ({
      name: `vwap_rev hold=${hName}`,
      fn: (bars: RBar[], i: number) => {
        const vp = 720; if (i < vp) return null
        const v = vwap(bars, i, vp); const dev = v > 0 ? (bars[i].c - v) / v * 10000 : 0
        if (Math.abs(dev) < 15) return null
        return dev > 0 ? 'short' as const : 'long' as const
      },
      hold: hBars
    })),
    // Mean reversion top decile
    ...HORIZONS.map(([hName, hBars]) => ({
      name: `mrev_top10 hold=${hName}`,
      fn: (bars: RBar[], i: number) => {
        const lb = hBars * 2; if (i < lb) return null
        const m = mom(bars, i, lb)
        // Use fixed threshold per horizon since we can't compute percentile inline
        const thresh = hBars <= 30 ? 10 : hBars <= 90 ? 20 : hBars <= 360 ? 40 : 80
        if (Math.abs(m) < thresh) return null
        return m > 0 ? 'short' as const : 'long' as const
      },
      hold: hBars
    })),
  ]

  // First pass: find top 5 by net t-stat
  const scored: { name: string; t: number; trades: SimTrade[] }[] = []
  for (const c of candidates) {
    const all: SimTrade[] = []
    for (const [name, bars] of sessions) {
      all.push(...simulate(bars, name, c.fn, c.hold, FEE, c.hold + 3))
    }
    if (all.length < 10) continue
    const tt = tTest(all.map(t => t.netBps))
    scored.push({ name: c.name, t: tt.t, trades: all })
  }
  scored.sort((a, b) => b.t - a.t)

  console.log('Top 8 by net t-stat:\n')
  for (const s of scored.slice(0, 8)) {
    report(s.name, s.trades, true)
    console.log()
  }

  // ═══════════════════════════════════════════
  // 11. FEE SENSITIVITY for top 3
  // ═══════════════════════════════════════════
  console.log(`${'─'.repeat(100)}`)
  console.log('11. FEE SENSITIVITY — TOP 3\n')

  for (const s of scored.slice(0, 3)) {
    console.log(`  ${s.name}:`)
    const grossMean = tTest(s.trades.map(t => t.grossBps)).mean
    for (const fee of [0, 1, 2, 3, 4, 5.5]) {
      const nets = s.trades.map(t => t.grossBps - 2 * fee)
      const tt = tTest(nets)
      console.log(`    fee=${String(fee).padStart(4)}: net=${pad(fmt(tt.mean))}bps t=${pad(fmt(tt.t))} ${sig(tt.t)}`)
    }
    console.log(`    breakeven fee: ~${fmt(grossMean / 2)}bps each way`)
    console.log()
  }
}

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