/**
 * Robustness analysis focused on:
 *  - increasing WR
 *  - increasing positive months
 *  - keeping tests aligned with runner logic
 *
 * Baseline logic is matched to src/runners/combined-demo.ts:
 *   entry |score| >= 10
 *   ext if same direction and |score| >= 8 at deadline
 *   hold 60-240 min weighted by active signals
 *   stop 100 bps
 *   5-bar re-entry cooldown after exit
 *   no SL directional cooldown
 *   fee = 4 bps round trip (Bybit maker) by default
 */
import fs from 'node:fs'
import readline from 'node:readline'

interface Bar { ts: number; o: number; h: number; l: number; c: number }
interface ScoreDetail {
  score: number
  holdBars: number
  signals: string[]
  dow: number
  hour: number
}
interface Trade {
  entryBar: number
  exitBar: number
  side: 'long'|'short'
  entry: number
  exit: number
  net: number
  gross: number
  stopped: boolean
  month: string
  holdBars: number
  entryScore: number
  exitScore: number
  signals: string[]
  dow: number
  hour: number
  reason: 'time'|'stop'
}

interface SimCfg {
  longEntry: number
  shortEntry: number
  extThresh: number
  minHold: number
  maxHold: number
  longStop: number
  shortStop: number
  fee: number
  maxSameDirStopsPerDay: number // 0 = disabled
}

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 computeScore(bars: Bar[], i: number): ScoreDetail {
  let w=0,hW=0,tW=0
  const sigs: string[] = []
  const d=new Date(bars[i].ts),h=d.getUTCHours(),m=d.getUTCMinutes(),dow=d.getUTCDay()
  const add=(name:string,dir:number,wt:number,hold:number)=>{w+=dir*wt;hW+=hold*wt;tW+=wt;sigs.push(`${name}${dir>0?'+':'-'}`)}
  if(dow===4)add('Thu',-1,34.41,240)
  if(dow===3)add('Wed',1,19.05,240)
  if(dow===0)add('Sun',1,15.05,240)
  if(dow===1)add('Mon',1,10.61,240)
  if(dow===5)add('Fri',-1,10.84,240)
  if(h===21)add('H21',1,17.90,60)
  if(h===20)add('H20',1,9.49,60)
  if(h===23)add('H23',-1,8.94,30)
  const bp=avgCP(bars,i,60)
  if(bp>0.58)add('BUYP',1,15.49,240)
  if(bp<0.42)add('BUYP',-1,15.49,240)
  if((h===13||(h===14&&m<=15))&&m<=15&&i>=60){const g=lbRet(bars,i,60);if(Math.abs(g)>5)add('USGAP',g>0?-1:1,6.87,120)}
  const rv=rsi(bars,i,60)
  if(rv<30)add('RSI',1,4.63,120)
  if(rv>70)add('RSI',-1,4.63,120)
  if(i>=1440){const dy=lbRet(bars,i,1440);if(Math.abs(dy)>30)add('REV24',dy>0?-1:1,5.0,240)}
  return { score:w, holdBars:tW>0?Math.max(60,Math.min(240,Math.round(hW/tW))):120, signals:sigs, dow, hour:h }
}

function tStat(v: number[]) {
  const n=v.length
  if(n<3) return { n, mean: 0, t: 0, wr: 0, med: 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)
  const s=[...v].sort((a,b)=>a-b)
  return { n, mean, t: se>0?mean/se:0, wr: v.filter(x=>x>0).length/n, med: s[Math.floor(n/2)] }
}
function sig(t:number){const a=Math.abs(t);return a>3.29?'***':a>2.58?'**':a>1.96?'*':''}

function simulate(bars: Bar[], cfg: SimCfg): Trade[] {
  const trades: Trade[] = []
  let cd = 0
  const stopHitsByDay = new Map<string, { long: number; short: number }>()

  for (let i = 1440; i < bars.length; i++) {
    if (i < cd) continue
    const s0 = computeScore(bars, i)
    const dir = s0.score > 0 ? 1 : -1
    const side: 'long'|'short' = dir > 0 ? 'long' : 'short'
    const entryThresh = side === 'long' ? cfg.longEntry : cfg.shortEntry
    if (Math.abs(s0.score) < entryThresh) continue

    const dayKey = new Date(bars[i].ts).toISOString().slice(0, 10)
    if (cfg.maxSameDirStopsPerDay > 0) {
      const rec = stopHitsByDay.get(dayKey) ?? { long: 0, short: 0 }
      if ((side === 'long' ? rec.long : rec.short) >= cfg.maxSameDirStopsPerDay) continue
    }

    const entry = bars[i].c
    const stopBps = side === 'long' ? cfg.longStop : cfg.shortStop
    const holdBars = Math.max(cfg.minHold, Math.min(cfg.maxHold, s0.holdBars))
    let deadline = i + holdBars
    let exitBar = deadline
    let stopped = false
    let exitScore = s0.score
    let curStart = i

    while (true) {
      if (deadline >= bars.length) { exitBar = bars.length - 2; exitScore = computeScore(bars, exitBar).score; break }
      for (let j = curStart + 1; j <= deadline && j < bars.length; j++) {
        if (dir * (bars[j].c - entry) / entry * 10000 <= -stopBps) {
          exitBar = j
          exitScore = computeScore(bars, j).score
          stopped = true
          break
        }
      }
      if (stopped) break
      const ext = computeScore(bars, deadline)
      exitScore = ext.score
      const extHold = Math.max(cfg.minHold, Math.min(cfg.maxHold, ext.holdBars))
      const dirMatch = (ext.score * dir) > 0
      const magOk = cfg.extThresh === 0 ? true : Math.abs(ext.score) >= cfg.extThresh
      if (dirMatch && magOk) {
        curStart = deadline
        deadline = Math.min(bars.length - 2, deadline + extHold)
        continue
      }
      exitBar = deadline
      break
    }

    if (exitBar >= bars.length) exitBar = bars.length - 2
    const gross = stopped ? -stopBps : dir * (bars[exitBar].c - entry) / entry * 10000
    const net = gross - cfg.fee
    const month = new Date(bars[i].ts).toISOString().slice(0, 7)
    trades.push({
      entryBar: i,
      exitBar,
      side,
      entry,
      exit: bars[exitBar].c,
      gross,
      net,
      stopped,
      month,
      holdBars: exitBar - i,
      entryScore: s0.score,
      exitScore,
      signals: s0.signals,
      dow: s0.dow,
      hour: s0.hour,
      reason: stopped ? 'stop' : 'time'
    })
    if (stopped && cfg.maxSameDirStopsPerDay > 0) {
      const rec = stopHitsByDay.get(dayKey) ?? { long: 0, short: 0 }
      if (side === 'long') rec.long += 1
      else rec.short += 1
      stopHitsByDay.set(dayKey, rec)
    }
    cd = exitBar + 5
  }
  return trades
}

function monthlyStats(trades: Trade[]) {
  const m = new Map<string, Trade[]>()
  for (const t of trades) {
    if (!m.has(t.month)) m.set(t.month, [])
    m.get(t.month)!.push(t)
  }
  const rows = [...m.entries()].sort(([a],[b])=>a.localeCompare(b)).map(([month, ts]) => {
    const s = tStat(ts.map(t => t.net))
    return { month, ...s, longs: ts.filter(t=>t.side==='long').length, shorts: ts.filter(t=>t.side==='short').length }
  })
  return rows
}

function sideStats(trades: Trade[]) {
  const mk = (side: 'long'|'short') => {
    const ts = trades.filter(t => t.side === side)
    const s = tStat(ts.map(t => t.net))
    const stops = ts.filter(t => t.stopped).length
    return { side, ...s, stops, stopRate: stops / ts.length }
  }
  return [mk('long'), mk('short')]
}

function bucketStats(trades: Trade[], key: (t: Trade) => string) {
  const m = new Map<string, Trade[]>()
  for (const t of trades) {
    const k = key(t)
    if (!m.has(k)) m.set(k, [])
    m.get(k)!.push(t)
  }
  return [...m.entries()].map(([k, ts]) => ({ k, ...tStat(ts.map(t=>t.net)), stops: ts.filter(t=>t.stopped).length }))
}

function halfStats(trades: Trade[]) {
  const midTs = trades[Math.floor(trades.length/2)]?.entryBar ?? 0
  const h1 = trades.filter(t => t.entryBar <= midTs)
  const h2 = trades.filter(t => t.entryBar > midTs)
  return { h1: tStat(h1.map(t=>t.net)), h2: tStat(h2.map(t=>t.net)) }
}

function summary(label: string, trades: Trade[]) {
  const s = tStat(trades.map(t=>t.net))
  const months = monthlyStats(trades)
  const posMonths = months.filter(m => m.mean > 0).length
  const avgHold = Math.round(trades.reduce((a,b)=>a+b.holdBars,0)/trades.length)
  return {
    label,
    n: s.n,
    mean: s.mean,
    t: s.t,
    wr: s.wr,
    posMonths,
    totalMonths: months.length,
    avgHold,
    stops: trades.filter(t=>t.stopped).length,
    months,
    half: halfStats(trades)
  }
}

function printTop(title: string, rows: any[], limit = 10) {
  console.log(`\n${title}`)
  for (const r of rows.slice(0, limit)) console.log(r)
}

async function main() {
  console.log('Loading bars...')
  const bars = await load()
  console.log(`Loaded ${bars.length} bars`)

  const baselineCfg: SimCfg = {
    longEntry: 10,
    shortEntry: 10,
    extThresh: 8,
    minHold: 60,
    maxHold: 240,
    longStop: 100,
    shortStop: 100,
    fee: 4,
    maxSameDirStopsPerDay: 0
  }

  const baseTrades = simulate(bars, baselineCfg)
  const base = summary('baseline', baseTrades)

  console.log('\n══════════════════════════════════════════════════════')
  console.log('BASELINE — verified against runner logic')
  console.log('══════════════════════════════════════════════════════')
  console.log(`n=${base.n}  mean=${base.mean.toFixed(2)}bps  t=${base.t.toFixed(2)}${sig(base.t)}  WR=${(base.wr*100).toFixed(1)}%  months=${base.posMonths}/${base.totalMonths}  avgHold=${base.avgHold}m  stops=${base.stops}`)
  console.log(`Half split: H1 t=${base.half.h1.t.toFixed(2)} mean=${base.half.h1.mean.toFixed(2)}bps | H2 t=${base.half.h2.t.toFixed(2)} mean=${base.half.h2.mean.toFixed(2)}bps`)

  console.log('\nMonthly:')
  for (const m of base.months) console.log(`  ${m.month}  n=${String(m.n).padStart(3)}  mean=${m.mean>=0?'+':''}${m.mean.toFixed(1)}bps  t=${m.t.toFixed(2)}  longs=${m.longs} shorts=${m.shorts}`)

  printTop('Side stats', sideStats(baseTrades))
  printTop('Score buckets (entry |score|)', bucketStats(baseTrades, t => {
    const a = Math.abs(t.entryScore)
    if (a < 15) return '10-15'
    if (a < 20) return '15-20'
    if (a < 30) return '20-30'
    return '30+'
  }).sort((a,b)=>a.k.localeCompare(b.k)), 10)
  printTop('Day-of-week x side', bucketStats(baseTrades, t => `${['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][t.dow]}_${t.side}`).sort((a,b)=>b.mean-a.mean), 14)
  printTop('Hour x side (best to worst)', bucketStats(baseTrades, t => `${String(t.hour).padStart(2,'0')}_${t.side}`).sort((a,b)=>b.mean-a.mean), 20)
  printTop('Signal-set worst buckets', bucketStats(baseTrades, t => t.signals.slice().sort().join('+')).filter(r => r.n >= 20).sort((a,b)=>a.mean-b.mean), 15)
  printTop('Signal-set best buckets', bucketStats(baseTrades, t => t.signals.slice().sort().join('+')).filter(r => r.n >= 20).sort((a,b)=>b.mean-a.mean), 15)

  // Search 1: optimize positive months / WR in a constrained plausible family
  const candidates: Array<{ cfg: SimCfg; out: ReturnType<typeof summary> }> = []
  const longEntries = [8,10,12,15]
  const shortEntries = [8,10,12,15,18,20]
  const extThreshes = [0,5,8,10]
  const holdRanges: Array<[number,number]> = [[60,180],[60,240],[60,360]]
  const longStops = [100]
  const shortStops = [75,100,150]
  const stopCaps = [0,1,2]

  let checked = 0
  for (const longEntry of longEntries) {
    for (const shortEntry of shortEntries) {
      for (const extThresh of extThreshes) {
        for (const [minHold, maxHold] of holdRanges) {
          for (const longStop of longStops) {
            for (const shortStop of shortStops) {
              for (const maxSameDirStopsPerDay of stopCaps) {
                const cfg: SimCfg = { longEntry, shortEntry, extThresh, minHold, maxHold, longStop, shortStop, fee: 4, maxSameDirStopsPerDay }
                const trades = simulate(bars, cfg)
                const out = summary('candidate', trades)
                candidates.push({ cfg, out })
                checked++
              }
            }
          }
        }
      }
    }
  }
  console.log(`\nChecked ${checked} constrained candidates.`)

  const byT = [...candidates].sort((a,b)=>b.out.t-a.out.t)
  const byWr = [...candidates].filter(x => x.out.mean > 0).sort((a,b)=>b.out.wr-a.out.wr || b.out.t-a.out.t)
  const byMonths = [...candidates].sort((a,b)=>b.out.posMonths-a.out.posMonths || b.out.t-a.out.t)
  const thirteen = byMonths.filter(x => x.out.posMonths === x.out.totalMonths)

  console.log('\n══════════════════════════════════════════════════════')
  console.log('SEARCH RESULTS — constrained, plausible family')
  console.log('══════════════════════════════════════════════════════')

  console.log('\nTop 12 by t-stat:')
  for (const r of byT.slice(0, 12)) {
    const c = r.cfg, o = r.out
    console.log(`  t=${o.t.toFixed(2)}${sig(o.t)} mean=${o.mean.toFixed(2)}bps WR=${(o.wr*100).toFixed(1)}% months=${o.posMonths}/${o.totalMonths} | LE=${c.longEntry} SE=${c.shortEntry} x=${c.extThresh} h=${c.minHold}-${c.maxHold} Lstop=${c.longStop} Sstop=${c.shortStop} stopCap=${c.maxSameDirStopsPerDay}`)
  }

  console.log('\nTop 12 by WR (positive expectancy only):')
  for (const r of byWr.slice(0, 12)) {
    const c = r.cfg, o = r.out
    console.log(`  WR=${(o.wr*100).toFixed(1)}% t=${o.t.toFixed(2)}${sig(o.t)} mean=${o.mean.toFixed(2)}bps months=${o.posMonths}/${o.totalMonths} | LE=${c.longEntry} SE=${c.shortEntry} x=${c.extThresh} h=${c.minHold}-${c.maxHold} Lstop=${c.longStop} Sstop=${c.shortStop} stopCap=${c.maxSameDirStopsPerDay}`)
  }

  console.log('\nTop 12 by positive months:')
  for (const r of byMonths.slice(0, 12)) {
    const c = r.cfg, o = r.out
    console.log(`  months=${o.posMonths}/${o.totalMonths} t=${o.t.toFixed(2)}${sig(o.t)} mean=${o.mean.toFixed(2)}bps WR=${(o.wr*100).toFixed(1)}% | LE=${c.longEntry} SE=${c.shortEntry} x=${c.extThresh} h=${c.minHold}-${c.maxHold} Lstop=${c.longStop} Sstop=${c.shortStop} stopCap=${c.maxSameDirStopsPerDay}`)
  }

  console.log(`\n13/13 month candidates found: ${thirteen.length}`)
  for (const r of thirteen.slice(0, 10)) {
    const c = r.cfg, o = r.out
    console.log(`  t=${o.t.toFixed(2)}${sig(o.t)} mean=${o.mean.toFixed(2)}bps WR=${(o.wr*100).toFixed(1)}% | LE=${c.longEntry} SE=${c.shortEntry} x=${c.extThresh} h=${c.minHold}-${c.maxHold} Sstop=${c.shortStop} stopCap=${c.maxSameDirStopsPerDay}`)
  }

  const interesting = [
    byT[0],
    byWr[0],
    byMonths[0],
    thirteen.sort((a,b)=>b.out.t-a.out.t)[0]
  ].filter(Boolean) as Array<{ cfg: SimCfg; out: ReturnType<typeof summary> }>

  console.log('\n══════════════════════════════════════════════════════')
  console.log('INTERPRETATION AID — detailed months for key candidates')
  console.log('══════════════════════════════════════════════════════')
  for (const r of interesting) {
    const c = r.cfg, o = r.out
    console.log(`\nLE=${c.longEntry} SE=${c.shortEntry} x=${c.extThresh} h=${c.minHold}-${c.maxHold} Lstop=${c.longStop} Sstop=${c.shortStop} stopCap=${c.maxSameDirStopsPerDay}`)
    console.log(`  n=${o.n} mean=${o.mean.toFixed(2)}bps t=${o.t.toFixed(2)}${sig(o.t)} WR=${(o.wr*100).toFixed(1)}% months=${o.posMonths}/${o.totalMonths}`)
    console.log(`  Half split: H1 t=${o.half.h1.t.toFixed(2)} H2 t=${o.half.h2.t.toFixed(2)}`)
    for (const m of o.months) console.log(`    ${m.month} ${m.mean>=0?'+':''}${m.mean.toFixed(1)}bps t=${m.t.toFixed(2)} n=${m.n}`)
  }
}

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