/**
 * Comprehensive edge search on historical bulk data (Binance + Bybit).
 * Tests dozens of hypotheses across 10 diverse market days.
 *
 * Goal: find ANY statistically significant predictive signal for short-horizon
 * BTC futures returns, robust across regimes.
 */
import fs from 'node:fs'
import readline from 'node:readline'
import type { Bar10s } from '../core/strategy.js'

// ── Types ──

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

// ── Stats helpers ──

function tTest(v: number[]): { mean: number; t: number; n: number; se: number } {
  const n = v.length; if (n < 3) return { mean: 0, t: 0, n, se: 0 }
  const mean = v.reduce((a, b) => a + b, 0) / n
  const variance = v.reduce((s, x) => s + (x - mean) ** 2, 0) / (n - 1)
  const se = Math.sqrt(variance / n)
  return { mean, se, t: se > 0 ? mean / se : 0, n }
}

function twoSampleT(a: number[], b: number[]): { diff: number; t: number } {
  const ta = tTest(a), tb = tTest(b)
  const diff = ta.mean - tb.mean
  const se = Math.sqrt(ta.se ** 2 + tb.se ** 2)
  return { diff, t: se > 0 ? diff / se : 0 }
}

function percentile(arr: number[], p: number): number {
  const s = [...arr].sort((a, b) => a - b)
  const i = Math.floor(s.length * p)
  return s[Math.min(i, s.length - 1)]
}

function autocorrelation(x: number[], lag: number): number {
  const n = x.length, mean = x.reduce((a, b) => a + b, 0) / n
  let num = 0, den = 0
  for (let i = 0; i < n; i++) den += (x[i] - mean) ** 2
  for (let i = lag; i < n; i++) num += (x[i] - mean) * (x[i - lag] - mean)
  return den > 0 ? num / den : 0
}

function sig(t: number): string {
  const a = Math.abs(t)
  return a > 3.29 ? '***' : a > 2.58 ? '**' : a > 1.96 ? '*' : ''
}

function fmt(v: number, d = 3): string { return v.toFixed(d) }
function pad(s: string, w = 10): string { return s.padStart(w) }

// ── Data loading ──

async function loadAllHistorical(): Promise<Map<string, RBar[]>> {
  const sessions = new Map<string, RBar[]>()
  const histDir = 'data/historical'
  const dates = fs.readdirSync(histDir).filter(d => {
    try { return fs.statSync(`${histDir}/${d}/trades.jsonl`).size > 100_000 } catch { return false }
  }).sort()

  for (const date of dates) {
    process.stderr.write(`Loading ${date}...`)
    const barsMap = 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)
      const p = d.data.price, ex = d.data.exchange, side = d.data.side, vol = d.data.notionalUsd
      const e = barsMap.get(k)
      if (!e) {
        barsMap.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 = [...barsMap.values()].sort((a, b) => a.ts - b.ts)
    sessions.set(date, bars)
    process.stderr.write(` ${bars.length} bars\n`)
  }
  return sessions
}

// ── Feature computation ──

function delta(b: RBar): number {
  const v = b.buyVol + b.sellVol
  return v > 0 ? (b.buyVol - b.sellVol) / v : 0
}

function barReturn(b: RBar): number {
  return b.o > 0 ? (b.c - b.o) / b.o * 10000 : 0
}

function fwdReturn(bars: RBar[], i: number, fwd: number): number {
  if (i + fwd >= bars.length) return NaN
  return bars[i].c > 0 ? (bars[i + fwd].c - bars[i].c) / bars[i].c * 10000 : NaN
}

/** Realized vol: stdev of 6-bar close-to-close returns in bps */
function rv(bars: RBar[], i: number, lookback = 6): number {
  if (i < lookback) return 0
  const rets: number[] = []
  for (let j = i - lookback + 1; j <= i; j++) {
    if (bars[j - 1].c > 0) rets.push((bars[j].c - bars[j - 1].c) / bars[j - 1].c * 10000)
  }
  if (rets.length < 3) return 0
  const mean = rets.reduce((a, b) => a + b, 0) / rets.length
  return Math.sqrt(rets.reduce((s, r) => s + (r - mean) ** 2, 0) / (rets.length - 1))
}

/** Rolling N-bar delta (OFI) */
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 tot = bv + sv
  return tot > 0 ? (bv - sv) / tot : 0
}

/** Rolling volume */
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 / Math.min(n, i + 1)
}

/** Surge: current bar volume / 20-bar average */
function surge(bars: RBar[], i: number): number {
  const cur = bars[i].buyVol + bars[i].sellVol
  const avg = rollingVol(bars, i - 1, 20)
  return avg > 0 ? cur / avg : 1
}

/** N-bar price momentum in bps */
function momentum(bars: RBar[], i: number, n: number): number {
  if (i < n) return 0
  return bars[i - n].c > 0 ? (bars[i].c - bars[i - n].c) / bars[i - n].c * 10000 : 0
}

/** Exchange-specific delta */
function exDelta(b: RBar, ex: string): number {
  const bv = b.exBuyVol[ex] || 0, sv = b.exSellVol[ex] || 0
  const tot = bv + sv
  return tot > 0 ? (bv - sv) / tot : 0
}

/** Relative bar range: (high - low) / close */
function barRange(b: RBar): number {
  return b.c > 0 ? (b.h - b.l) / b.c * 10000 : 0
}

// ── Main analysis ──

async function main() {
  const sessions = await loadAllHistorical()
  const allBars: RBar[] = []
  for (const [, bars] of sessions) allBars.push(...bars)

  console.log(`\n${'═'.repeat(80)}`)
  console.log(`COMPREHENSIVE EDGE SEARCH — ${sessions.size} sessions, ${allBars.length} bars`)
  console.log(`${'═'.repeat(80)}\n`)

  // ═══════════════════════════════════════
  // 1. RETURN AUTOCORRELATION — momentum vs mean-reversion
  // ═══════════════════════════════════════
  console.log('1. RETURN AUTOCORRELATION (10s bars)')
  console.log('   +AC = continuation, -AC = mean-reversion')
  console.log()

  for (const [name, bars] of sessions) {
    const rets = bars.slice(1).map((b, i) => fwdReturn(bars, i, 1)).filter(r => !isNaN(r))
    const acs = [1, 2, 3, 6, 12, 30].map(lag => autocorrelation(rets, lag))
    console.log(`  ${name}:  ${acs.map((ac, i) => `${[10,20,30,60,120,300][[1,2,3,6,12,30].indexOf([1,2,3,6,12,30][i])]}s=${fmt(ac, 4)}`).join('  ')}`)
  }

  // Pooled across all sessions
  const pooledRets = allBars.slice(1).map((b, i) => {
    if (i >= allBars.length - 1) return NaN
    return allBars[i].c > 0 ? (allBars[i + 1].c - allBars[i].c) / allBars[i].c * 10000 : NaN
  }).filter(r => !isNaN(r))
  console.log()
  for (const lag of [1, 2, 3, 6, 12, 30, 60, 180]) {
    const ac = autocorrelation(pooledRets, lag)
    const crit = 2 / Math.sqrt(pooledRets.length)
    console.log(`  POOLED lag=${String(lag).padStart(3)} (${String(lag*10).padStart(4)}s): AC=${fmt(ac, 5)} ${Math.abs(ac) > crit ? sig(ac / crit * 1.96) : ''}`)
  }

  // ═══════════════════════════════════════
  // 2. ORDER FLOW IMBALANCE → FUTURE RETURNS
  // ═══════════════════════════════════════
  console.log(`\n${'─'.repeat(80)}`)
  console.log('2. ORDER FLOW IMBALANCE (OFI) → FUTURE RETURNS')
  console.log('   Does buy/sell imbalance over N bars predict forward returns?')
  console.log()

  for (const ofiWindow of [1, 3, 5, 10, 30]) {
    console.log(`  OFI window=${ofiWindow} bars (${ofiWindow * 10}s):`)
    for (const fwd of [1, 3, 6, 12, 30, 60]) {
      const rets: number[] = []
      for (const [, bars] of sessions) {
        for (let i = ofiWindow + 60; i < bars.length - fwd; i++) {
          const ofi = rollingDelta(bars, i, ofiWindow)
          if (Math.abs(ofi) < 0.15) continue
          const fr = fwdReturn(bars, i, fwd)
          if (isNaN(fr)) continue
          rets.push(ofi > 0 ? fr : -fr) // positive = OFI-aligned return
        }
      }
      const tt = tTest(rets)
      console.log(`    fwd=${String(fwd).padStart(2)} (${String(fwd*10).padStart(4)}s): mean=${pad(fmt(tt.mean))} t=${pad(fmt(tt.t, 2), 7)} n=${tt.n} ${sig(tt.t)}`)
    }
    console.log()
  }

  // ═══════════════════════════════════════
  // 3. ABSORPTION: DELTA OPPOSES PRICE → REVERSAL?
  // ═══════════════════════════════════════
  console.log(`${'─'.repeat(80)}`)
  console.log('3. ABSORPTION: DELTA OPPOSES PRICE DIRECTION')
  console.log('   When delta and price disagree on a high-volume bar, which is right?')
  console.log()

  for (const surgeThresh of [1.5, 2.0, 3.0, 5.0]) {
    console.log(`  Surge > ${surgeThresh}:`)
    for (const fwd of [1, 3, 6, 12, 30]) {
      const momentumRets: number[] = []  // delta aligns with price move
      const absorptionRets: number[] = [] // delta opposes price move
      for (const [, bars] of sessions) {
        for (let i = 60; i < bars.length - fwd; i++) {
          const s = surge(bars, i)
          if (s < surgeThresh) continue
          const d = delta(bars[i])
          const move = barReturn(bars[i])
          if (Math.abs(d) < 0.20 || Math.abs(move) < 0.5) continue
          const fr = fwdReturn(bars, i, fwd)
          if (isNaN(fr)) continue
          // For absorption: delta says one thing, price says another
          // Bet on delta direction (absorption = buying into weakness)
          const aligned = d > 0 ? fr : -fr
          if (d * move > 0) momentumRets.push(aligned)
          else absorptionRets.push(aligned)
        }
      }
      const mt = tTest(momentumRets), at = tTest(absorptionRets)
      const diff = twoSampleT(absorptionRets, momentumRets)
      console.log(`    fwd=${String(fwd*10).padStart(4)}s: MOM mean=${pad(fmt(mt.mean))} t=${pad(fmt(mt.t, 2), 6)} n=${mt.n}  |  ABS mean=${pad(fmt(at.mean))} t=${pad(fmt(at.t, 2), 6)} n=${at.n}  |  diff t=${fmt(diff.t, 2)} ${sig(diff.t)}`)
    }
    console.log()
  }

  // ═══════════════════════════════════════
  // 4. MEAN REVERSION AFTER LARGE MOVES
  // ═══════════════════════════════════════
  console.log(`${'─'.repeat(80)}`)
  console.log('4. MEAN REVERSION AFTER LARGE MOVES')
  console.log('   After a large N-bar price move, does price revert?')
  console.log()

  for (const lookback of [3, 6, 12, 30]) {
    console.log(`  Move window=${lookback} bars (${lookback * 10}s):`)
    for (const fwd of [3, 6, 12, 30, 60]) {
      const rets: number[] = []
      for (const [, bars] of sessions) {
        const moveThresh = percentile(
          bars.slice(lookback + 60).map((_, i) => Math.abs(momentum(bars, i + lookback + 60, lookback))).filter(v => v > 0),
          0.90
        )
        for (let i = lookback + 60; i < bars.length - fwd; i++) {
          const move = momentum(bars, i, lookback)
          if (Math.abs(move) < moveThresh) continue
          const fr = fwdReturn(bars, i, fwd)
          if (isNaN(fr)) continue
          rets.push(move > 0 ? -fr : fr) // positive = reversion (opposite direction)
        }
      }
      const tt = tTest(rets)
      console.log(`    fwd=${String(fwd*10).padStart(4)}s: reversion mean=${pad(fmt(tt.mean))} t=${pad(fmt(tt.t, 2), 7)} n=${tt.n} ${sig(tt.t)}`)
    }
    console.log()
  }

  // ═══════════════════════════════════════
  // 5. VOLUME EXHAUSTION → REVERSAL
  // ═══════════════════════════════════════
  console.log(`${'─'.repeat(80)}`)
  console.log('5. VOLUME EXHAUSTION')
  console.log('   After a volume spike, does the move stall or reverse?')
  console.log()

  for (const fwd of [3, 6, 12, 30, 60]) {
    const spikeRets: number[] = []   // spike aligned with delta direction
    const calmRets: number[] = []     // no spike control
    for (const [, bars] of sessions) {
      for (let i = 60; i < bars.length - fwd; i++) {
        const s = surge(bars, i)
        const d = delta(bars[i])
        const fr = fwdReturn(bars, i, fwd)
        if (isNaN(fr) || Math.abs(d) < 0.15) continue
        const aligned = d > 0 ? fr : -fr
        if (s > 3) spikeRets.push(aligned)
        else if (s > 0.5 && s < 1.5) calmRets.push(aligned)
      }
    }
    const st = tTest(spikeRets), ct = tTest(calmRets)
    const diff = twoSampleT(spikeRets, calmRets)
    console.log(`  fwd=${String(fwd*10).padStart(4)}s: SPIKE mean=${pad(fmt(st.mean))} t=${pad(fmt(st.t, 2), 6)} n=${st.n}  |  CALM mean=${pad(fmt(ct.mean))} t=${pad(fmt(ct.t, 2), 6)} n=${ct.n}  |  diff t=${fmt(diff.t, 2)} ${sig(diff.t)}`)
  }

  // ═══════════════════════════════════════
  // 6. CROSS-EXCHANGE LEAD-LAG
  // ═══════════════════════════════════════
  console.log(`\n${'─'.repeat(80)}`)
  console.log('6. CROSS-EXCHANGE LEAD-LAG')
  console.log('   Does one exchange delta predict the other exchange next-bar return?')
  console.log()

  for (const fwd of [1, 2, 3, 6]) {
    const rets: number[] = []
    for (const [, bars] of sessions) {
      for (let i = 60; i < bars.length - fwd; i++) {
        const bd = exDelta(bars[i], 'BINANCE')
        const br = bars[i].exBuyVol['BYBIT'] || 0, sr = bars[i].exSellVol['BYBIT'] || 0
        if (Math.abs(bd) < 0.3 || (br + sr) < 10000) continue
        const bybitFutRet = (bars[i + fwd].c - bars[i].c) / bars[i].c * 10000 // composite future
        if (isNaN(bybitFutRet)) continue
        rets.push(bd > 0 ? bybitFutRet : -bybitFutRet)
      }
    }
    const tt = tTest(rets)
    console.log(`  Binance delta → composite fwd=${fwd} (${fwd*10}s): mean=${pad(fmt(tt.mean))} t=${pad(fmt(tt.t, 2), 7)} n=${tt.n} ${sig(tt.t)}`)
  }

  // Reverse: Bybit delta → composite return
  console.log()
  for (const fwd of [1, 2, 3, 6]) {
    const rets: number[] = []
    for (const [, bars] of sessions) {
      for (let i = 60; i < bars.length - fwd; i++) {
        const bd = exDelta(bars[i], 'BYBIT')
        if (Math.abs(bd) < 0.3) continue
        const futRet = fwdReturn(bars, i, fwd)
        if (isNaN(futRet)) continue
        rets.push(bd > 0 ? futRet : -futRet)
      }
    }
    const tt = tTest(rets)
    console.log(`  Bybit delta → composite fwd=${fwd} (${fwd*10}s): mean=${pad(fmt(tt.mean))} t=${pad(fmt(tt.t, 2), 7)} n=${tt.n} ${sig(tt.t)}`)
  }

  // ═══════════════════════════════════════
  // 7. CROSS-EXCHANGE DELTA DIVERGENCE
  // ═══════════════════════════════════════
  console.log(`\n${'─'.repeat(80)}`)
  console.log('7. CROSS-EXCHANGE DELTA DIVERGENCE')
  console.log('   When Binance delta disagrees with Bybit delta, who is right?')
  console.log()

  for (const fwd of [1, 3, 6, 12, 30]) {
    const binanceRight: number[] = [], bybitRight: number[] = []
    for (const [, bars] of sessions) {
      for (let i = 60; i < bars.length - fwd; i++) {
        const bd = exDelta(bars[i], 'BINANCE'), byd = exDelta(bars[i], 'BYBIT')
        if (bd * byd >= 0 || Math.abs(bd) < 0.2 || Math.abs(byd) < 0.2) continue
        const fr = fwdReturn(bars, i, fwd)
        if (isNaN(fr)) continue
        binanceRight.push(bd > 0 ? fr : -fr)
        bybitRight.push(byd > 0 ? fr : -fr)
      }
    }
    const bt = tTest(binanceRight), byt = tTest(bybitRight)
    console.log(`  fwd=${String(fwd*10).padStart(4)}s: follow_Binance mean=${pad(fmt(bt.mean))} t=${pad(fmt(bt.t, 2), 6)} n=${bt.n}  |  follow_Bybit mean=${pad(fmt(byt.mean))} t=${pad(fmt(byt.t, 2), 6)} n=${byt.n}`)
  }

  // ═══════════════════════════════════════
  // 8. VOLATILITY REGIME CONDITIONING
  // ═══════════════════════════════════════
  console.log(`\n${'─'.repeat(80)}`)
  console.log('8. SIGNAL EDGE BY VOLATILITY REGIME')
  console.log('   Does OFI predict better in some vol regimes than others?')
  console.log()

  const rvBuckets = [[0, 3, 'quiet'], [3, 5, 'low'], [5, 7, 'moderate'], [7, 12, 'elevated'], [12, 30, 'high']] as const

  for (const [rvMin, rvMax, label] of rvBuckets) {
    const results: number[][] = []
    for (const fwd of [3, 6, 12, 30]) {
      const rets: number[] = []
      for (const [, bars] of sessions) {
        for (let i = 60; i < bars.length - fwd; i++) {
          const r = rv(bars, i)
          if (r < rvMin || r >= rvMax) continue
          const d = rollingDelta(bars, i, 5)
          if (Math.abs(d) < 0.25) continue
          const s = surge(bars, i)
          if (s < 2) continue
          const fr = fwdReturn(bars, i, fwd)
          if (isNaN(fr)) continue
          rets.push(d > 0 ? fr : -fr)
        }
      }
      results.push([fwd, ...Object.values(tTest(rets))])
    }
    console.log(`  rv ${String(rvMin).padStart(2)}-${String(rvMax).padStart(2)} (${label}):`)
    for (const [fwd, mean, t, n] of results) {
      console.log(`    fwd=${String((fwd as number)*10).padStart(4)}s: mean=${pad(fmt(mean as number))} t=${pad(fmt(t as number, 2), 7)} n=${Math.round(n as number)} ${sig(t as number)}`)
    }
    console.log()
  }

  // ═══════════════════════════════════════
  // 9. DELTA REVERSAL (sequential delta flip)
  // ═══════════════════════════════════════
  console.log(`${'─'.repeat(80)}`)
  console.log('9. DELTA REVERSAL')
  console.log('   After N bars of strong one-direction delta, a flip → entry?')
  console.log()

  for (const fwd of [3, 6, 12, 30]) {
    const rets: number[] = []
    for (const [, bars] of sessions) {
      for (let i = 65; i < bars.length - fwd; i++) {
        // Previous 5 bars: strong delta in one direction
        const prevDelta = rollingDelta(bars, i - 1, 5)
        const curDelta = delta(bars[i])
        if (Math.abs(prevDelta) < 0.30 || Math.abs(curDelta) < 0.20) continue
        if (prevDelta * curDelta >= 0) continue // no flip
        const fr = fwdReturn(bars, i, fwd)
        if (isNaN(fr)) continue
        // Bet on the NEW delta direction (the flip)
        rets.push(curDelta > 0 ? fr : -fr)
      }
    }
    const tt = tTest(rets)
    console.log(`  fwd=${String(fwd*10).padStart(4)}s: mean=${pad(fmt(tt.mean))} t=${pad(fmt(tt.t, 2), 7)} n=${tt.n} ${sig(tt.t)}`)
  }

  // ═══════════════════════════════════════
  // 10. TIME-OF-DAY ANALYSIS
  // ═══════════════════════════════════════
  console.log(`\n${'─'.repeat(80)}`)
  console.log('10. TIME-OF-DAY PATTERNS')
  console.log('    Which UTC hours have the most signal-aligned returns?')
  console.log()

  const hourBuckets: Map<number, number[]> = new Map()
  for (const [, bars] of sessions) {
    for (let i = 60; i < bars.length - 6; i++) {
      const d = rollingDelta(bars, i, 5)
      if (Math.abs(d) < 0.25) continue
      const s = surge(bars, i)
      if (s < 2) continue
      const fr = fwdReturn(bars, i, 6)
      if (isNaN(fr)) continue
      const hour = new Date(bars[i].ts).getUTCHours()
      if (!hourBuckets.has(hour)) hourBuckets.set(hour, [])
      hourBuckets.get(hour)!.push(d > 0 ? fr : -fr)
    }
  }
  for (const hour of [...hourBuckets.keys()].sort((a, b) => a - b)) {
    const rets = hourBuckets.get(hour)!
    const tt = tTest(rets)
    const bar = '█'.repeat(Math.min(30, Math.round(Math.abs(tt.mean) * 3)))
    const dir = tt.mean > 0 ? '+' : '-'
    console.log(`  ${String(hour).padStart(2)}:00 UTC  mean=${pad(fmt(tt.mean))} t=${pad(fmt(tt.t, 2), 6)} n=${String(tt.n).padStart(5)} ${sig(tt.t)} ${dir}${bar}`)
  }

  // ═══════════════════════════════════════
  // 11. RELATIVE RANGE BREAKOUT
  // ═══════════════════════════════════════
  console.log(`\n${'─'.repeat(80)}`)
  console.log('11. RANGE BREAKOUT')
  console.log('    After a bar with unusually wide range, does the close direction predict?')
  console.log()

  for (const fwd of [3, 6, 12, 30, 60]) {
    const rets: number[] = []
    for (const [, bars] of sessions) {
      const avgRange = bars.slice(60, 260).reduce((s, b) => s + barRange(b), 0) / 200
      for (let i = 60; i < bars.length - fwd; i++) {
        const range = barRange(bars[i])
        if (range < avgRange * 3) continue // need big range bar
        const closeDir = bars[i].c > bars[i].o ? 1 : -1
        const fr = fwdReturn(bars, i, fwd)
        if (isNaN(fr)) continue
        rets.push(closeDir * fr) // positive = continuation from close direction
      }
    }
    const tt = tTest(rets)
    console.log(`  fwd=${String(fwd*10).padStart(4)}s: continuation mean=${pad(fmt(tt.mean))} t=${pad(fmt(tt.t, 2), 7)} n=${tt.n} ${sig(tt.t)}`)
  }

  // ═══════════════════════════════════════
  // 12. MULTI-BAR DELTA PERSISTENCE
  // ═══════════════════════════════════════
  console.log(`\n${'─'.repeat(80)}`)
  console.log('12. MULTI-BAR DELTA PERSISTENCE')
  console.log('    Do N consecutive bars of same-sign delta predict continuation?')
  console.log()

  for (const streak of [3, 5, 7, 10]) {
    const fwds = [3, 6, 12, 30]
    console.log(`  Streak >= ${streak} bars:`)
    for (const fwd of fwds) {
      const rets: number[] = []
      for (const [, bars] of sessions) {
        for (let i = streak + 60; i < bars.length - fwd; i++) {
          let ok = true, streakDir = 0
          for (let j = i - streak + 1; j <= i; j++) {
            const d = delta(bars[j])
            if (Math.abs(d) < 0.10) { ok = false; break }
            if (streakDir === 0) streakDir = d > 0 ? 1 : -1
            else if ((d > 0 ? 1 : -1) !== streakDir) { ok = false; break }
          }
          if (!ok || streakDir === 0) continue
          const fr = fwdReturn(bars, i, fwd)
          if (isNaN(fr)) continue
          rets.push(streakDir * fr)
        }
      }
      const tt = tTest(rets)
      console.log(`    fwd=${String(fwd*10).padStart(4)}s: mean=${pad(fmt(tt.mean))} t=${pad(fmt(tt.t, 2), 7)} n=${tt.n} ${sig(tt.t)}`)
    }
    console.log()
  }

  // ═══════════════════════════════════════
  // 13. COMBINED FEATURE SCORING — brute force feature ranking
  // ═══════════════════════════════════════
  console.log(`${'─'.repeat(80)}`)
  console.log('13. FEATURE RANKING: absolute t-stats for fwd=6 (60s)')
  console.log('    Which single features best predict 60s returns?')
  console.log()

  interface Feature { name: string; fn: (bars: RBar[], i: number) => number | null }
  const features: Feature[] = [
    { name: 'delta_1bar', fn: (bars, i) => { const d = delta(bars[i]); return Math.abs(d) > 0.15 ? d : null } },
    { name: 'delta_3bar', fn: (bars, i) => { const d = rollingDelta(bars, i, 3); return Math.abs(d) > 0.15 ? d : null } },
    { name: 'delta_5bar', fn: (bars, i) => { const d = rollingDelta(bars, i, 5); return Math.abs(d) > 0.15 ? d : null } },
    { name: 'delta_10bar', fn: (bars, i) => { const d = rollingDelta(bars, i, 10); return Math.abs(d) > 0.10 ? d : null } },
    { name: 'momentum_3', fn: (bars, i) => { const m = momentum(bars, i, 3); return Math.abs(m) > 1 ? m : null } },
    { name: 'momentum_6', fn: (bars, i) => { const m = momentum(bars, i, 6); return Math.abs(m) > 2 ? m : null } },
    { name: 'momentum_12', fn: (bars, i) => { const m = momentum(bars, i, 12); return Math.abs(m) > 3 ? m : null } },
    { name: 'momentum_30', fn: (bars, i) => { const m = momentum(bars, i, 30); return Math.abs(m) > 5 ? m : null } },
    { name: 'surge_delta', fn: (bars, i) => { const s = surge(bars, i); const d = delta(bars[i]); return s > 2 && Math.abs(d) > 0.2 ? d : null } },
    { name: 'surge_delta_5b', fn: (bars, i) => { const s = surge(bars, i); const d = rollingDelta(bars, i, 5); return s > 2 && Math.abs(d) > 0.2 ? d : null } },
    { name: 'barReturn', fn: (bars, i) => { const r = barReturn(bars[i]); return Math.abs(r) > 1 ? r : null } },
    { name: 'range_dir', fn: (bars, i) => { const r = barRange(bars[i]); const avg = rollingVol(bars, i-1, 20); return r > 5 ? (bars[i].c > bars[i].o ? 1 : -1) : null } },
    { name: 'delta_reversal', fn: (bars, i) => { if (i < 6) return null; const prev = rollingDelta(bars, i-1, 5); const cur = delta(bars[i]); return prev*cur < 0 && Math.abs(prev) > 0.25 && Math.abs(cur) > 0.15 ? cur : null } },
    { name: 'bn_lead', fn: (bars, i) => { const d = exDelta(bars[i], 'BINANCE'); return Math.abs(d) > 0.3 ? d : null } },
    { name: 'by_lead', fn: (bars, i) => { const d = exDelta(bars[i], 'BYBIT'); return Math.abs(d) > 0.3 ? d : null } },
    { name: 'bn_by_div', fn: (bars, i) => { const bd = exDelta(bars[i], 'BINANCE'), byd = exDelta(bars[i], 'BYBIT'); return bd * byd < 0 && Math.abs(bd) > 0.2 ? bd : null } },
    { name: 'absorption', fn: (bars, i) => {
      const s = surge(bars, i), d = delta(bars[i]), m = barReturn(bars[i])
      return s > 2 && Math.abs(d) > 0.2 && d * m < 0 ? d : null
    }},
    { name: 'momentum_align', fn: (bars, i) => {
      const s = surge(bars, i), d = delta(bars[i]), m = barReturn(bars[i])
      return s > 2 && Math.abs(d) > 0.2 && d * m > 0 ? d : null
    }},
  ]

  const featureResults: { name: string; mean: number; t: number; n: number }[] = []
  for (const feat of features) {
    const rets: number[] = []
    for (const [, bars] of sessions) {
      for (let i = 60; i < bars.length - 6; i++) {
        const val = feat.fn(bars, i)
        if (val === null) continue
        const fr = fwdReturn(bars, i, 6)
        if (isNaN(fr)) continue
        rets.push(val > 0 ? fr : -fr)
      }
    }
    const tt = tTest(rets)
    featureResults.push({ name: feat.name, ...tt })
  }

  featureResults.sort((a, b) => Math.abs(b.t) - Math.abs(a.t))
  for (const f of featureResults) {
    const bar = '█'.repeat(Math.min(30, Math.round(Math.abs(f.t))))
    console.log(`  ${f.name.padEnd(18)} mean=${pad(fmt(f.mean))} t=${pad(fmt(f.t, 2), 7)} n=${String(f.n).padStart(7)} ${sig(f.t).padEnd(3)} ${bar}`)
  }

  // ═══════════════════════════════════════
  // 14. TOP FEATURES BY FORWARD HORIZON
  // ═══════════════════════════════════════
  console.log(`\n${'─'.repeat(80)}`)
  console.log('14. TOP 3 FEATURES BY FORWARD HORIZON')
  console.log()

  for (const fwd of [1, 3, 6, 12, 30, 60]) {
    const ranked: { name: string; mean: number; t: number; n: number }[] = []
    for (const feat of features) {
      const rets: number[] = []
      for (const [, bars] of sessions) {
        for (let i = 60; i < bars.length - fwd; i++) {
          const val = feat.fn(bars, i)
          if (val === null) continue
          const fr = fwdReturn(bars, i, fwd)
          if (isNaN(fr)) continue
          rets.push(val > 0 ? fr : -fr)
        }
      }
      const tt = tTest(rets)
      ranked.push({ name: feat.name, ...tt })
    }
    ranked.sort((a, b) => Math.abs(b.t) - Math.abs(a.t))
    console.log(`  fwd=${fwd} (${fwd*10}s):`)
    for (const f of ranked.slice(0, 5)) {
      console.log(`    ${f.name.padEnd(18)} mean=${pad(fmt(f.mean))} t=${pad(fmt(f.t, 2), 7)} n=${f.n} ${sig(f.t)}`)
    }
    console.log()
  }

  // ═══════════════════════════════════════
  // 15. SESSION-LEVEL CONSISTENCY CHECK
  // ═══════════════════════════════════════
  console.log(`${'─'.repeat(80)}`)
  console.log('15. SESSION-LEVEL CONSISTENCY')
  console.log('    For top features: how many sessions show positive mean return?')
  console.log()

  const topFeats = featureResults.slice(0, 5)
  for (const feat of topFeats) {
    const f = features.find(ff => ff.name === feat.name)!
    let posSessions = 0, totalSessions = 0
    const sessionMeans: number[] = []
    for (const [name, bars] of sessions) {
      const rets: number[] = []
      for (let i = 60; i < bars.length - 6; i++) {
        const val = f.fn(bars, i)
        if (val === null) continue
        const fr = fwdReturn(bars, i, 6)
        if (isNaN(fr)) continue
        rets.push(val > 0 ? fr : -fr)
      }
      if (rets.length < 10) continue
      totalSessions++
      const m = rets.reduce((a, b) => a + b, 0) / rets.length
      sessionMeans.push(m)
      if (m > 0) posSessions++
    }
    const tt = tTest(sessionMeans)
    console.log(`  ${feat.name.padEnd(18)} positive_sessions=${posSessions}/${totalSessions} session_mean_t=${fmt(tt.t, 2)} ${sig(tt.t)}`)
  }

  console.log(`\n${'═'.repeat(80)}`)
  console.log('ANALYSIS COMPLETE')
  console.log(`${'═'.repeat(80)}`)
}

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