/**
 * Rolling walk-forward validation for baseline vs A-only.
 *
 * Uses runner-matched logic:
 *   entry |score| >= 10
 *   ext same direction and |score| >= 8
 *   hold 60-240m weighted by signals
 *   stop 100bps (baseline) / shortStop=75bps (A-only)
 *   5-bar cooldown after exit
 *   no SL directional cooldown
 *   fee 4bps RT
 *
 * Two OOS protocols:
 *   1) Trailing 6 calendar months train -> next calendar month test
 *   2) Anchored expanding train -> next calendar month test
 */
import fs from 'node:fs'
import readline from 'node:readline'

interface Bar { ts: number; o: number; h: number; l: number; c: number }
interface Trade { entryBar: number; month: string; net: number; stopped: boolean }
interface Config { name: string; shortStop: number }

async function load(): Promise<Bar[]> {
  const bars: Bar[] = []
  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 lbRet(b: Bar[], i: number, n: number) { return i>=n&&b[i-n].c>0?(b[i].c-b[i-n].c)/b[i-n].c*10000:0 }
function closePos(b: Bar) { return b.h>b.l?(b.c-b.l)/(b.h-b.l):0.5 }
function avgCP(b: Bar[], i: number, n: number) { let s=0;for(let j=Math.max(0,i-n+1);j<=i;j++)s+=closePos(b[j]);return s/Math.min(n,i+1) }
function rsi(b: Bar[], i: number, n: number) {
  if(i<n)return 50;let u=0,d=0
  for(let j=i-n+1;j<=i;j++){const dd=b[j].c-b[j-1].c;if(dd>0)u+=dd;else d-=dd}
  return d===0?100:100-100/(1+u/d)
}
function score(b: Bar[], i: number) {
  let w=0,hW=0,tW=0
  const d=new Date(b[i].ts),h=d.getUTCHours(),m=d.getUTCMinutes(),dow=d.getUTCDay()
  const add=(dir:number,wt:number,hold:number)=>{w+=dir*wt;hW+=hold*wt;tW+=wt}
  if(dow===4)add(-1,34.41,240)
  if(dow===3)add(1,19.05,240)
  if(dow===0)add(1,15.05,240)
  if(dow===1)add(1,10.61,240)
  if(dow===5)add(-1,10.84,240)
  if(h===21)add(1,17.90,60)
  if(h===20)add(1,9.49,60)
  if(h===23)add(-1,8.94,30)
  const bp=avgCP(b,i,60)
  if(bp>0.58)add(1,15.49,240)
  if(bp<0.42)add(-1,15.49,240)
  if((h===13||(h===14&&m<=15))&&m<=15&&i>=60){const g=lbRet(b,i,60);if(Math.abs(g)>5)add(g>0?-1:1,6.87,120)}
  const rv=rsi(b,i,60)
  if(rv<30)add(1,4.63,120)
  if(rv>70)add(-1,4.63,120)
  if(i>=1440){const dy=lbRet(b,i,1440);if(Math.abs(dy)>30)add(dy>0?-1:1,5.0,240)}
  return { score:w, holdBars:tW>0?Math.max(60,Math.min(240,Math.round(hW/tW))):120 }
}
function tt(v: number[]) {
  const n=v.length
  if(n<3) return { n, mean: 0, t: 0, wr: 0 }
  const mean=v.reduce((a,b)=>a+b,0)/n
  const se=Math.sqrt(v.reduce((s,x)=>s+(x-mean)**2,0)/(n-1)/n)
  return { n, mean, t: se>0?mean/se:0, wr: v.filter(x=>x>0).length/n }
}
function sig(t:number){const a=Math.abs(t);return a>3.29?'***':a>2.58?'**':a>1.96?'*':''}

function simulate(bars: Bar[], cfg: Config): Trade[] {
  const trades: Trade[] = []
  let cd = 0
  for (let i = 1440; i < bars.length; i++) {
    if (i < cd) continue
    const s0 = score(bars, i)
    const dir = s0.score > 0 ? 1 : -1
    const side: 'long'|'short' = dir > 0 ? 'long' : 'short'
    if (Math.abs(s0.score) < 10) continue
    const stop = side === 'long' ? 100 : cfg.shortStop
    const holdBars = Math.max(60, Math.min(240, s0.holdBars))
    const entry = bars[i].c
    let deadline = i + holdBars
    let exitBar = deadline
    let stopped = false
    let cur = i
    while (true) {
      if (deadline >= bars.length) { exitBar = bars.length - 2; break }
      for (let j = cur + 1; j <= deadline && j < bars.length; j++) {
        if (dir * (bars[j].c - entry) / entry * 10000 <= -stop) { exitBar = j; stopped = true; break }
      }
      if (stopped) break
      const es = score(bars, deadline)
      const eh = Math.max(60, Math.min(240, es.holdBars))
      if ((es.score * dir) > 0 && Math.abs(es.score) >= 8) {
        cur = deadline
        deadline = Math.min(bars.length - 2, deadline + eh)
        continue
      }
      exitBar = deadline
      break
    }
    const gross = stopped ? -stop : dir * (bars[exitBar].c - entry) / entry * 10000
    trades.push({
      entryBar: i,
      month: new Date(bars[i].ts).toISOString().slice(0, 7),
      net: gross - 4,
      stopped
    })
    cd = exitBar + 5
  }
  return trades
}

function uniqueMonths(bars: Bar[]): string[] {
  return [...new Set(bars.map(b => new Date(b.ts).toISOString().slice(0,7)))].sort()
}

function sliceForMonths(allBars: Bar[], months: string[], startMonthIdx: number, endMonthIdxExclusive: number): Bar[] {
  const startMonth = months[startMonthIdx]
  const endMonth = months[endMonthIdxExclusive]
  const startTs = allBars.find(b => new Date(b.ts).toISOString().slice(0,7) === startMonth)?.ts
  const endTs = endMonth
    ? allBars.find(b => new Date(b.ts).toISOString().slice(0,7) === endMonth)?.ts
    : undefined
  if (startTs == null) return []
  const warmupTs = startTs - 1440 * 60_000
  return allBars.filter(b => b.ts >= warmupTs && (endTs == null || b.ts < endTs))
}

function summarizeMonth(trades: Trade[], month: string) {
  const nets = trades.filter(t => t.month === month).map(t => t.net)
  return tt(nets)
}

async function main() {
  console.log('Loading bars...')
  const bars = await load()
  const months = uniqueMonths(bars)
  const configs: Config[] = [
    { name: 'BASELINE', shortStop: 100 },
    { name: 'A_ONLY', shortStop: 75 }
  ]

  console.log(`Loaded ${bars.length} bars across ${months.length} months: ${months[0]} -> ${months[months.length-1]}`)

  // Start at month index 6 so each test month has 6 prior months available.
  const testMonthIdxs = Array.from({ length: months.length - 6 }, (_, k) => k + 6)

  console.log('\n══════════════════════════════════════════════════════')
  console.log('TRAILING 6-MONTH TRAIN -> NEXT MONTH TEST')
  console.log('══════════════════════════════════════════════════════')
  for (const cfg of configs) {
    console.log(`\n${cfg.name}`)
    const aggTrain: number[] = []
    const aggTest: number[] = []
    for (const mi of testMonthIdxs) {
      const testMonth = months[mi]
      const trainBars = sliceForMonths(bars, months, mi - 6, mi)
      const testBars = sliceForMonths(bars, months, mi, mi + 1)
      const trainTrades = simulate(trainBars, cfg).filter(t => months.slice(mi - 6, mi).includes(t.month))
      const testTrades = simulate(testBars, cfg).filter(t => t.month === testMonth)
      const trainT = tt(trainTrades.map(t => t.net))
      const testT = tt(testTrades.map(t => t.net))
      aggTrain.push(...trainTrades.map(t => t.net))
      aggTest.push(...testTrades.map(t => t.net))
      console.log(`  ${testMonth}  TRAIN mean=${trainT.mean.toFixed(2).padStart(6)} t=${trainT.t.toFixed(2).padStart(5)} n=${String(trainT.n).padStart(3)}  |  TEST mean=${testT.mean.toFixed(2).padStart(6)} t=${testT.t.toFixed(2).padStart(5)} n=${String(testT.n).padStart(3)} WR=${(testT.wr*100).toFixed(0).padStart(3)}% ${sig(testT.t)}`)
    }
    const tr = tt(aggTrain), te = tt(aggTest)
    console.log(`  AGG  TRAIN mean=${tr.mean.toFixed(2).padStart(6)} t=${tr.t.toFixed(2).padStart(5)} n=${tr.n}  |  TEST mean=${te.mean.toFixed(2).padStart(6)} t=${te.t.toFixed(2).padStart(5)} n=${te.n} WR=${(te.wr*100).toFixed(1)}% ${sig(te.t)}`)
  }

  console.log('\n══════════════════════════════════════════════════════')
  console.log('ANCHORED EXPANDING TRAIN -> NEXT MONTH TEST')
  console.log('══════════════════════════════════════════════════════')
  for (const cfg of configs) {
    console.log(`\n${cfg.name}`)
    const aggTrain: number[] = []
    const aggTest: number[] = []
    for (const mi of testMonthIdxs) {
      const testMonth = months[mi]
      const trainBars = sliceForMonths(bars, months, 0, mi)
      const testBars = sliceForMonths(bars, months, mi, mi + 1)
      const trainTrades = simulate(trainBars, cfg).filter(t => months.slice(0, mi).includes(t.month))
      const testTrades = simulate(testBars, cfg).filter(t => t.month === testMonth)
      const trainT = tt(trainTrades.map(t => t.net))
      const testT = tt(testTrades.map(t => t.net))
      aggTrain.push(...trainTrades.map(t => t.net))
      aggTest.push(...testTrades.map(t => t.net))
      console.log(`  ${testMonth}  TRAIN mean=${trainT.mean.toFixed(2).padStart(6)} t=${trainT.t.toFixed(2).padStart(5)} n=${String(trainT.n).padStart(3)}  |  TEST mean=${testT.mean.toFixed(2).padStart(6)} t=${testT.t.toFixed(2).padStart(5)} n=${String(testT.n).padStart(3)} WR=${(testT.wr*100).toFixed(0).padStart(3)}% ${sig(testT.t)}`)
    }
    const tr = tt(aggTrain), te = tt(aggTest)
    console.log(`  AGG  TRAIN mean=${tr.mean.toFixed(2).padStart(6)} t=${tr.t.toFixed(2).padStart(5)} n=${tr.n}  |  TEST mean=${te.mean.toFixed(2).padStart(6)} t=${te.t.toFixed(2).padStart(5)} n=${te.n} WR=${(te.wr*100).toFixed(1)}% ${sig(te.t)}`)
  }

  // Compact winner summary on OOS months only
  console.log('\n══════════════════════════════════════════════════════')
  console.log('OOS MONTH-BY-MONTH WINNER (trailing 6m protocol)')
  console.log('══════════════════════════════════════════════════════')
  let aWins = 0, bWins = 0, ties = 0
  for (const mi of testMonthIdxs) {
    const month = months[mi]
    const testBars = sliceForMonths(bars, months, mi, mi + 1)
    const base = summarizeMonth(simulate(testBars, configs[0]), month)
    const a = summarizeMonth(simulate(testBars, configs[1]), month)
    const winner = a.mean > base.mean ? 'A_ONLY' : base.mean > a.mean ? 'BASELINE' : 'TIE'
    if (winner === 'A_ONLY') aWins++
    else if (winner === 'BASELINE') bWins++
    else ties++
    console.log(`  ${month}  BASE=${base.mean.toFixed(2).padStart(6)}bps  A=${a.mean.toFixed(2).padStart(6)}bps  -> ${winner}`)
  }
  console.log(`  Totals: A_ONLY ${aWins}, BASELINE ${bWins}, ties ${ties}`)
}

main().catch(err => { console.error(err); process.exit(1) })
