/**
 * Exhaustive edge search on 525K 1-minute BTC klines (full year).
 * Tests every known short-horizon alpha hypothesis.
 *
 * With 8000+ trade observations per signal, we have massive statistical power.
 * Bonferroni-aware: with ~200 tests, significance threshold is t > 3.5 (p < 0.00025).
 */
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 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))]
}

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

// bar return in bps
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
}
// lookback return
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 : NaN
}
// realized vol (stdev of 1-min returns over last N bars)
function rv(bars: K[], i: number, n: number): number {
  if (i < n) return 0
  const r: number[] = []
  for (let j = i-n+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))
}
// bar range relative to close
function barRange(b: K): number { return b.c > 0 ? (b.h - b.l) / b.c * 10000 : 0 }
// rolling average volume
function avgVol(bars: K[], i: number, n: number): number {
  let s = 0; for (let j = Math.max(0,i-n+1); j <= i; j++) s += bars[j].usd; return s / Math.min(n,i+1)
}
// volume ratio (current vs average)
function volRatio(bars: K[], i: number, n: number): number {
  const avg = avgVol(bars, i-1, n); return avg > 0 ? bars[i].usd / avg : 1
}
// close position within bar (0=low, 1=high) — proxy for buy pressure
function closePosition(b: K): number { return b.h > b.l ? (b.c - b.l) / (b.h - b.l) : 0.5 }
// rolling close position (buy pressure proxy over N bars)
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)
}
// EMA
function ema(bars: K[], 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
}
// Bollinger width (bps)
function bbWidth(bars: K[], i: number, n: number): number {
  if (i < n) return 0
  const prices: number[] = []
  for (let j = i-n+1; j <= i; j++) prices.push(bars[j].c)
  const m = prices.reduce((a,b)=>a+b,0)/prices.length
  const std = Math.sqrt(prices.reduce((s,x)=>s+(x-m)**2,0)/(prices.length-1))
  return m > 0 ? std / m * 10000 : 0
}
// RSI
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)
}

type TestResult = { name: string; mean: number; t: number; n: number; fwd: number }

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

  const results: TestResult[] = []

  function test(name: string, fwd: number, signalFn: (i: number) => number | null) {
    const rets: number[] = []
    for (let i = 360; i < bars.length - fwd; i += 1) {
      const sig = signalFn(i)
      if (sig === null) continue
      const r = ret(bars, i, fwd)
      if (isNaN(r)) continue
      rets.push(sig > 0 ? r : -r) // positive = signal-aligned
    }
    const t = tt(rets)
    results.push({ name: `${name} [fwd=${fwd}m]`, mean: t.mean, t: t.t, n: t.n, fwd })
  }

  const FWDS = [5, 15, 30, 60, 120, 240]
  const WARMUP = 360

  process.stderr.write('Running tests...\n')

  // ═══════════════════════════════════════════
  // 1. RETURN AUTOCORRELATION / MOMENTUM / MEAN-REVERSION
  // ═══════════════════════════════════════════
  process.stderr.write('  1. Momentum / Mean-reversion\n')
  for (const lookback of [5, 10, 15, 30, 60, 120, 240, 480, 720, 1440]) {
    for (const fwd of FWDS) {
      // Momentum: lookback return predicts continuation
      test(`MOM lb=${lookback}m`, fwd, (i) => {
        const m = lb(bars, i, lookback); return Math.abs(m) > 3 ? m : null
      })
      // Mean-reversion: lookback return predicts reversal
      test(`REV lb=${lookback}m`, fwd, (i) => {
        const m = lb(bars, i, lookback); return Math.abs(m) > 3 ? -m : null
      })
    }
  }

  // ═══════════════════════════════════════════
  // 2. VOLUME SPIKE + DIRECTION
  // ═══════════════════════════════════════════
  process.stderr.write('  2. Volume spikes\n')
  for (const vrThresh of [2, 3, 5]) {
    for (const fwd of FWDS) {
      // Volume spike → continuation of bar direction
      test(`VOL_CONT vr>${vrThresh}`, fwd, (i) => {
        const vr = volRatio(bars, i, 60)
        if (vr < vrThresh) return null
        const r = (bars[i].c - bars[i].o) / bars[i].o * 10000
        return Math.abs(r) > 1 ? r : null
      })
      // Volume spike → reversal
      test(`VOL_REV vr>${vrThresh}`, fwd, (i) => {
        const vr = volRatio(bars, i, 60)
        if (vr < vrThresh) return null
        const r = (bars[i].c - bars[i].o) / bars[i].o * 10000
        return Math.abs(r) > 1 ? -r : null
      })
      // Volume spike → follow buy-pressure direction
      test(`VOL_BUY vr>${vrThresh}`, fwd, (i) => {
        const vr = volRatio(bars, i, 60)
        if (vr < vrThresh) return null
        const cp = closePosition(bars[i])
        return Math.abs(cp - 0.5) > 0.2 ? (cp > 0.5 ? 1 : -1) : null
      })
    }
  }

  // ═══════════════════════════════════════════
  // 3. BUY PRESSURE PROXY (close position in bar)
  // ═══════════════════════════════════════════
  process.stderr.write('  3. Buy pressure\n')
  for (const window of [5, 15, 30, 60]) {
    for (const fwd of FWDS) {
      test(`BUYP ${window}m`, fwd, (i) => {
        const bp = avgClosePos(bars, i, window)
        return Math.abs(bp - 0.5) > 0.08 ? (bp > 0.5 ? 1 : -1) : null
      })
      // Fade extreme buy pressure
      test(`BUYP_FADE ${window}m`, fwd, (i) => {
        const bp = avgClosePos(bars, i, window)
        return Math.abs(bp - 0.5) > 0.08 ? (bp > 0.5 ? -1 : 1) : null
      })
    }
  }

  // ═══════════════════════════════════════════
  // 4. VOLATILITY REGIME CONDITIONING
  // ═══════════════════════════════════════════
  process.stderr.write('  4. Vol regime\n')
  for (const [rvMin, rvMax, label] of [[0,2,'quiet'],[2,4,'low'],[4,8,'mod'],[8,15,'high'],[15,999,'extreme']] as [number,number,string][]) {
    for (const lbk of [30, 60, 120]) {
      for (const fwd of [15, 30, 60]) {
        test(`MOM_RV:${label} lb=${lbk}m`, fwd, (i) => {
          const r = rv(bars, i, 30)
          if (r < rvMin || r >= rvMax) return null
          const m = lb(bars, i, lbk)
          return Math.abs(m) > 3 ? m : null
        })
      }
    }
  }

  // ═══════════════════════════════════════════
  // 5. TIME-OF-DAY
  // ═══════════════════════════════════════════
  process.stderr.write('  5. Time of day\n')
  for (let hour = 0; hour < 24; hour++) {
    for (const fwd of [15, 30, 60]) {
      // Is this hour bullish or bearish?
      test(`HOUR_${hour} bias`, fwd, (i) => {
        const h = new Date(bars[i].ts).getUTCHours()
        if (h !== hour) return null
        return 1 // always long — positive mean = bullish hour
      })
    }
  }

  // ═══════════════════════════════════════════
  // 6. DAY-OF-WEEK
  // ═══════════════════════════════════════════
  process.stderr.write('  6. Day of week\n')
  for (let dow = 0; dow < 7; dow++) {
    for (const fwd of [30, 60, 240]) {
      test(`DOW_${['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][dow]}`, fwd, (i) => {
        if (new Date(bars[i].ts).getUTCDay() !== dow) return null
        return 1
      })
    }
  }

  // ═══════════════════════════════════════════
  // 7. BOLLINGER SQUEEZE → BREAKOUT
  // ═══════════════════════════════════════════
  process.stderr.write('  7. BB squeeze\n')
  for (const period of [60, 120, 240]) {
    for (const fwd of [30, 60, 120]) {
      test(`BB_SQZ ${period}m break`, fwd, (i) => {
        if (i < period + 60) return null
        const curWidth = bbWidth(bars, i, period)
        const prevWidth = bbWidth(bars, i - 30, period)
        if (curWidth === 0 || prevWidth === 0) return null
        // Squeeze: current width < 60% of recent width
        if (curWidth > prevWidth * 0.6) return null
        // Direction: follow current 15-min momentum
        const m = lb(bars, i, 15)
        return Math.abs(m) > 2 ? m : null
      })
      // Expansion: wide BB + momentum = continuation
      test(`BB_EXP ${period}m cont`, fwd, (i) => {
        if (i < period + 60) return null
        const curWidth = bbWidth(bars, i, period)
        const prevWidth = bbWidth(bars, i - 30, period)
        if (curWidth === 0 || prevWidth === 0) return null
        if (curWidth < prevWidth * 1.5) return null
        const m = lb(bars, i, 15)
        return Math.abs(m) > 2 ? m : null
      })
    }
  }

  // ═══════════════════════════════════════════
  // 8. RSI EXTREME → REVERSAL / CONTINUATION
  // ═══════════════════════════════════════════
  process.stderr.write('  8. RSI\n')
  for (const period of [14, 30, 60]) {
    for (const fwd of [15, 30, 60, 120]) {
      // RSI oversold → long (reversion)
      test(`RSI_REV ${period}m`, fwd, (i) => {
        const r = rsi(bars, i, period)
        if (r > 30 && r < 70) return null
        return r < 30 ? 1 : -1 // oversold → long, overbought → short
      })
      // RSI extreme → continuation
      test(`RSI_MOM ${period}m`, fwd, (i) => {
        const r = rsi(bars, i, period)
        if (r > 30 && r < 70) return null
        return r > 70 ? 1 : -1 // overbought → still long
      })
    }
  }

  // ═══════════════════════════════════════════
  // 9. LARGE CANDLE → FOLLOW-THROUGH vs EXHAUST
  // ═══════════════════════════════════════════
  process.stderr.write('  9. Large candles\n')
  for (const rangeThresh of [3, 5, 10]) {
    for (const fwd of [5, 15, 30, 60]) {
      const avgR = bars.slice(WARMUP, WARMUP + 10000).reduce((s, b) => s + barRange(b), 0) / 10000
      test(`BIGBAR_CONT >${rangeThresh}x`, fwd, (i) => {
        if (barRange(bars[i]) < avgR * rangeThresh) return null
        return bars[i].c > bars[i].o ? 1 : -1 // follow close direction
      })
      test(`BIGBAR_REV >${rangeThresh}x`, fwd, (i) => {
        if (barRange(bars[i]) < avgR * rangeThresh) return null
        return bars[i].c > bars[i].o ? -1 : 1 // fade close direction
      })
    }
  }

  // ═══════════════════════════════════════════
  // 10. EMA CROSSOVER
  // ═══════════════════════════════════════════
  process.stderr.write('  10. EMA cross\n')
  for (const [fast, slow] of [[5,15],[15,60],[30,120],[60,240]] as [number,number][]) {
    for (const fwd of [15, 30, 60, 120]) {
      test(`EMA ${fast}/${slow}`, fwd, (i) => {
        if (i < slow * 3 + 1) return null
        const fNow = ema(bars, i, fast), sNow = ema(bars, i, slow)
        const fPrev = ema(bars, i-1, fast), sPrev = ema(bars, i-1, slow)
        if ((fNow > sNow) !== (fPrev > sPrev)) return fNow > sNow ? 1 : -1
        return null
      })
    }
  }

  // ═══════════════════════════════════════════
  // 11. SESSION OPEN PATTERNS (Asia/Europe/US)
  // ═══════════════════════════════════════════
  process.stderr.write('  11. Session opens\n')
  const SESSION_OPENS = [
    { name: 'ASIA', hour: 0 },    // 00:00 UTC
    { name: 'EUROPE', hour: 7 },  // 07:00 UTC
    { name: 'US', hour: 13 },     // 13:00 UTC (pre-market)
    { name: 'US_OPEN', hour: 14 },// 14:30 UTC
  ]
  for (const sess of SESSION_OPENS) {
    for (const fwd of [30, 60, 120, 240]) {
      // First 30 min of session: momentum from overnight gap
      test(`${sess.name}_GAP_MOM`, fwd, (i) => {
        const h = new Date(bars[i].ts).getUTCHours()
        const m = new Date(bars[i].ts).getUTCMinutes()
        if (h !== sess.hour || m > 15) return null
        const gap = lb(bars, i, 60)
        return Math.abs(gap) > 5 ? gap : null
      })
      // Session open: fade the gap
      test(`${sess.name}_GAP_REV`, fwd, (i) => {
        const h = new Date(bars[i].ts).getUTCHours()
        const m = new Date(bars[i].ts).getUTCMinutes()
        if (h !== sess.hour || m > 15) return null
        const gap = lb(bars, i, 60)
        return Math.abs(gap) > 5 ? -gap : null
      })
    }
  }

  // ═══════════════════════════════════════════
  // 12. PREVIOUS DAY HIGH/LOW BREAKOUT
  // ═══════════════════════════════════════════
  process.stderr.write('  12. Prev day H/L\n')
  for (const fwd of [30, 60, 120, 240]) {
    test(`PREV_DAY_BREAK`, fwd, (i) => {
      if (i < 1440) return null
      let dayHigh = -Infinity, dayLow = Infinity
      for (let j = i - 1440; j < i - 60; j++) { dayHigh = Math.max(dayHigh, bars[j].h); dayLow = Math.min(dayLow, bars[j].l) }
      if (bars[i].c > dayHigh) return 1
      if (bars[i].c < dayLow) return -1
      return null
    })
    test(`PREV_DAY_FADE`, fwd, (i) => {
      if (i < 1440) return null
      let dayHigh = -Infinity, dayLow = Infinity
      for (let j = i - 1440; j < i - 60; j++) { dayHigh = Math.max(dayHigh, bars[j].h); dayLow = Math.min(dayLow, bars[j].l) }
      if (bars[i].c > dayHigh) return -1
      if (bars[i].c < dayLow) return 1
      return null
    })
  }

  // ═══════════════════════════════════════════
  // 13. ROUND NUMBER ATTRACTION / REPULSION
  // ═══════════════════════════════════════════
  process.stderr.write('  13. Round numbers\n')
  for (const fwd of [15, 30, 60]) {
    test(`ROUND_1000 attract`, fwd, (i) => {
      const price = bars[i].c
      const nearestK = Math.round(price / 1000) * 1000
      const dist = (price - nearestK) / price * 10000 // distance in bps
      if (Math.abs(dist) > 30 || Math.abs(dist) < 5) return null
      return dist > 0 ? -1 : 1 // price attracted toward round number
    })
  }

  // ═══════════════════════════════════════════
  // 14. OVERNIGHT RETURN (hold from close to close)
  // ═══════════════════════════════════════════
  process.stderr.write('  14. Overnight\n')
  for (const fwd of [60, 240, 480]) {
    test(`OVERNIGHT_MOM`, fwd, (i) => {
      const h = new Date(bars[i].ts).getUTCHours()
      if (h !== 21) return null // 21:00 UTC = late US
      const dayRet = lb(bars, i, 480) // ~8h return
      return Math.abs(dayRet) > 10 ? dayRet : null
    })
    test(`OVERNIGHT_REV`, fwd, (i) => {
      const h = new Date(bars[i].ts).getUTCHours()
      if (h !== 21) return null
      const dayRet = lb(bars, i, 480)
      return Math.abs(dayRet) > 10 ? -dayRet : null
    })
  }

  // ═══════════════════════════════════════════
  // 15. COMBO: Vol regime × Time × Direction
  // ═══════════════════════════════════════════
  process.stderr.write('  15. Combos\n')
  for (const fwd of [30, 60]) {
    // High vol + US session + momentum
    test(`COMBO highvol+US+mom`, fwd, (i) => {
      const h = new Date(bars[i].ts).getUTCHours()
      if (h < 13 || h > 20) return null
      const r = rv(bars, i, 30)
      if (r < 5) return null
      const m = lb(bars, i, 30)
      return Math.abs(m) > 5 ? m : null
    })
    // Low vol + Asia + reversion
    test(`COMBO lowvol+Asia+rev`, fwd, (i) => {
      const h = new Date(bars[i].ts).getUTCHours()
      if (h > 7) return null
      const r = rv(bars, i, 30)
      if (r > 3) return null
      const m = lb(bars, i, 30)
      return Math.abs(m) > 2 ? -m : null
    })
    // Squeeze + breakout + high vol
    test(`COMBO squeeze+break`, fwd, (i) => {
      if (i < 300) return null
      const w = bbWidth(bars, i, 120)
      const pw = bbWidth(bars, i-60, 120)
      if (w === 0 || pw === 0 || w > pw * 0.7) return null
      const m = lb(bars, i, 5)
      return Math.abs(m) > 3 ? m : null
    })
  }

  // ═══════════════════════════════════════════
  // RANK AND DISPLAY
  // ═══════════════════════════════════════════
  process.stderr.write('\nDone. Ranking results...\n\n')

  // Sort by absolute t-stat
  results.sort((a, b) => Math.abs(b.t) - Math.abs(a.t))

  console.log(`${'═'.repeat(110)}`)
  console.log(`EXHAUSTIVE EDGE SEARCH — ${bars.length} 1-minute candles, full year`)
  console.log(`BONFERRONI: ${results.length} tests → significance requires |t| > 3.5 (p < 0.00025)`)
  console.log(`Fee budget: need mean > 4bps net to survive 2bps/side maker fees`)
  console.log(`${'═'.repeat(110)}`)
  console.log()

  // Top 40
  console.log('TOP 40 BY |t-stat|:')
  console.log()
  console.log('  ' + 'Signal'.padEnd(45) + 'mean(bps)'.padStart(10) + 't-stat'.padStart(8) + 'n'.padStart(8) + '  sig  ' + 'survives fees?')
  console.log('  ' + '─'.repeat(95))

  for (const r of results.slice(0, 40)) {
    const feeSurvives = r.mean > 4 ? '✓ YES' : r.mean > 0 ? '✗ no (too small)' : '✗ NEGATIVE'
    console.log(
      '  ' + r.name.padEnd(45) +
      r.mean.toFixed(2).padStart(10) +
      r.t.toFixed(2).padStart(8) +
      String(r.n).padStart(8) +
      ('  ' + sig(r.t)).padEnd(7) +
      feeSurvives
    )
  }

  // Summary by category
  console.log()
  console.log(`${'─'.repeat(110)}`)
  console.log('SUMMARY BY CATEGORY — best result per category:')
  console.log()

  const categories = [
    { prefix: 'MOM ', label: 'Momentum (continuation)' },
    { prefix: 'REV ', label: 'Mean-reversion' },
    { prefix: 'VOL_', label: 'Volume spike' },
    { prefix: 'BUYP', label: 'Buy pressure proxy' },
    { prefix: 'MOM_RV', label: 'Vol-regime momentum' },
    { prefix: 'HOUR_', label: 'Hour-of-day bias' },
    { prefix: 'DOW_', label: 'Day-of-week' },
    { prefix: 'BB_', label: 'Bollinger band' },
    { prefix: 'RSI_', label: 'RSI signals' },
    { prefix: 'BIG', label: 'Large candle' },
    { prefix: 'EMA ', label: 'EMA crossover' },
    { prefix: 'ASIA\|EUROPE\|US', label: 'Session opens' },
    { prefix: 'PREV_DAY', label: 'Prev day H/L break' },
    { prefix: 'ROUND', label: 'Round number' },
    { prefix: 'OVERNIGHT', label: 'Overnight carry' },
    { prefix: 'COMBO', label: 'Multi-factor combo' },
  ]

  for (const cat of categories) {
    const matching = results.filter(r => new RegExp(cat.prefix).test(r.name))
    if (matching.length === 0) continue
    const best = matching.sort((a, b) => b.t - a.t)[0]
    const feeSurvives = best.mean > 4 ? '✓' : '✗'
    console.log(`  ${cat.label.padEnd(28)} best: ${best.name.padEnd(42)} mean=${best.mean.toFixed(2).padStart(7)}bps  t=${best.t.toFixed(2).padStart(6)}  n=${String(best.n).padStart(7)}  ${feeSurvives}`)
  }

  // Fee-surviving signals only
  console.log()
  console.log(`${'─'.repeat(110)}`)
  const survivors = results.filter(r => r.mean > 4 && Math.abs(r.t) > 2)
  if (survivors.length > 0) {
    console.log(`FEE-SURVIVING SIGNALS (mean > 4bps AND |t| > 2): ${survivors.length} found`)
    console.log()
    for (const r of survivors.slice(0, 20)) {
      console.log(`  ${r.name.padEnd(50)} mean=${r.mean.toFixed(2).padStart(7)}bps  t=${r.t.toFixed(2).padStart(6)}  n=${String(r.n).padStart(7)}  ${sig(r.t)}`)
    }
  } else {
    console.log('FEE-SURVIVING SIGNALS: *** NONE ***')
    console.log('No signal produces mean > 4bps (maker round-trip) with t > 2.')
  }

  // Gross-positive signals (before fees)
  const grossPos = results.filter(r => r.mean > 0).sort((a, b) => b.t - a.t)
  console.log()
  console.log(`GROSS-POSITIVE SIGNALS (mean > 0, before fees): ${grossPos.length}/${results.length}`)
  if (grossPos.length > 0) {
    console.log('Top 10:')
    for (const r of grossPos.slice(0, 10)) {
      console.log(`  ${r.name.padEnd(50)} mean=${r.mean.toFixed(2).padStart(7)}bps  t=${r.t.toFixed(2).padStart(6)}  n=${String(r.n).padStart(7)}`)
    }
  }

  console.log()
  console.log(`${'═'.repeat(110)}`)
  console.log(`Total tests: ${results.length}`)
  console.log(`Gross positive (mean > 0): ${grossPos.length} (${(grossPos.length/results.length*100).toFixed(0)}%)`)
  console.log(`Fee-surviving (mean > 4bps, |t| > 2): ${survivors.length}`)
  console.log(`${'═'.repeat(110)}`)
}

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