/**
 * Backtest: SL directional cooldown hypothesis.
 *
 * After a stop-loss, block same direction for N bars.
 * Opposite direction can still fire immediately.
 *
 * Tests: does the trade immediately after an SL in the same direction
 * tend to also lose? If yes, blocking it improves PnL.
 */
import fs from 'node:fs'
import readline from 'node:readline'

interface Bar { ts: number; o: number; h: number; l: number; c: 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 getScore(bars: Bar[], i: number): { score: number; holdBars: number } {
  let w=0, hW=0, tW=0
  const d=new Date(bars[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(bars,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(bars,i,60);if(Math.abs(g)>5)add(g>0?-1:1,6.87,120)}
  const rv=rsi(bars,i,60); if(rv<30)add(1,4.63,120); if(rv>70)add(-1,4.63,120)
  if(i>=1440){const dy=lbRet(bars,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 }
}

const ENTRY_FEE=2, EXIT_FEE=2, STOP_BPS=100, SCORE_THRESH=10

interface Trade { grossBps: number; netBps: number; month: string; stoppedOut: boolean; dir: number }

function simulate(bars: Bar[], slCooldownBars: number): Trade[] {
  const trades: Trade[] = []
  let cd=0
  let slCooldownDir=0, slCooldownUntil=-1  // 0=none, 1=long blocked, -1=short blocked

  for (let i=1440; i<bars.length; i++) {
    if (i < cd) continue
    const { score, holdBars } = getScore(bars, i)
    if (Math.abs(score) < SCORE_THRESH) continue
    const dir = score > 0 ? 1 : -1

    // SL directional cooldown: same dir blocked, opposite allowed
    if (slCooldownDir !== 0 && i < slCooldownUntil && dir === slCooldownDir) continue

    const entry = bars[i].c
    let exitBar = i + holdBars
    let stoppedOut = false

    // Walk with stop
    for (let j=i+1; j<=i+holdBars && j<bars.length; j++) {
      const pnl = dir * (bars[j].c - entry) / entry * 10000
      if (pnl <= -STOP_BPS) { exitBar=j; stoppedOut=true; break }
    }
    if (exitBar >= bars.length) exitBar = bars.length - 1

    const grossBps = stoppedOut ? -STOP_BPS : dir * (bars[exitBar].c - entry) / entry * 10000
    const netBps = grossBps - ENTRY_FEE - EXIT_FEE

    trades.push({ grossBps, netBps, month: new Date(bars[i].ts).toISOString().slice(0,7), stoppedOut, dir })

    if (stoppedOut && slCooldownBars > 0) {
      slCooldownDir = dir
      slCooldownUntil = exitBar + slCooldownBars
    }
    // Extension check (already validated as beneficial)
    const extExitBar = Math.max(exitBar + 5, i + holdBars)
    cd = Math.max(extExitBar, exitBar + 5)
  }
  return trades
}

function tt(v: number[]): { mean: number; t: number; n: number } {
  const n=v.length; if(n<3) 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 monthlyPositive(trades: Trade[]): string {
  const m=new Map<string,number[]>()
  trades.forEach(t=>{if(!m.has(t.month))m.set(t.month,[]);m.get(t.month)!.push(t.netBps)})
  let pos=0; for(const ns of m.values()) if(ns.reduce((a,b)=>a+b,0)>0)pos++
  return `${pos}/${m.size}`
}

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

  console.log('═'.repeat(90))
  console.log('SL DIRECTIONAL COOLDOWN BACKTEST')
  console.log('After stop-loss: block same direction for N bars. Opposite still fires.')
  console.log('═'.repeat(90))
  console.log()

  // ── What trades come right after an SL? ──
  console.log('1. WHAT HAPPENS TO THE TRADE IMMEDIATELY AFTER AN SL (same direction)?')
  console.log('─'.repeat(90))
  console.log()

  const base = simulate(bars, 0)
  const slTrades = base.filter(t => t.stoppedOut)
  const slIndices: number[] = []

  // Find the trade index right after each SL in same direction
  const postSlSame: number[] = []   // next same-dir trade after SL
  const postSlOpp: number[] = []    // next opposite-dir trade after SL

  // Re-run to find post-SL trade outcomes
  let cd=0, slDir=0, slBar=-1
  for (let i=1440; i<bars.length; i++) {
    if (i < cd) continue
    const { score, holdBars } = getScore(bars, i)
    if (Math.abs(score) < SCORE_THRESH) continue
    const dir = score > 0 ? 1 : -1
    const entry = bars[i].c
    let exitBar=i+holdBars, stoppedOut=false
    for (let j=i+1; j<=i+holdBars&&j<bars.length; j++) {
      if (dir*(bars[j].c-entry)/entry*10000 <= -STOP_BPS) { exitBar=j; stoppedOut=true; break }
    }
    if (exitBar >= bars.length) exitBar=bars.length-1
    const gross = stoppedOut ? -STOP_BPS : dir*(bars[exitBar].c-entry)/entry*10000
    const net = gross - ENTRY_FEE - EXIT_FEE

    // Was this trade within 240 bars of an SL in same direction?
    if (slDir !== 0 && i < slBar+240) {
      if (dir === slDir) postSlSame.push(net)
      else postSlOpp.push(net)
    }

    if (stoppedOut) { slDir=dir; slBar=exitBar }
    cd = Math.max(exitBar+5, i+holdBars)
  }

  const tSame = tt(postSlSame), tOpp = tt(postSlOpp), tAll = tt(base.map(t=>t.netBps))
  const tSl = tt(slTrades.map(t=>t.netBps))

  console.log(`  SL trades in dataset: ${slTrades.length} (${(slTrades.length/base.length*100).toFixed(0)}% of all trades)`)
  console.log(`  All trades:           mean=${tAll.mean.toFixed(2)}bps  t=${tAll.t.toFixed(2)} ${sig(tAll.t)}`)
  console.log()
  console.log(`  Trades within 240min of SL, SAME direction: n=${postSlSame.length}  mean=${tSame.mean.toFixed(2)}bps  t=${tSame.t.toFixed(2)} ${sig(tSame.t)}`)
  console.log(`  Trades within 240min of SL, OPP  direction: n=${postSlOpp.length}  mean=${tOpp.mean.toFixed(2)}bps   t=${tOpp.t.toFixed(2)} ${sig(tOpp.t)}`)
  console.log()
  if (tSame.mean < tAll.mean) {
    console.log(`  ✅ Post-SL same-dir trades underperform average by ${(tAll.mean - tSame.mean).toFixed(2)}bps → blocking them is correct`)
  } else {
    console.log(`  ✗ Post-SL same-dir trades perform BETTER than average → blocking hurts`)
  }

  // ── Cooldown sweep ──
  console.log()
  console.log('2. COOLDOWN SWEEP')
  console.log('─'.repeat(90))
  console.log()
  console.log('  Cooldown    n     mean    t-stat   WR    cum bps  pos/mo  SLs  blocked')
  console.log('  ' + '─'.repeat(72))

  const cooldowns = [0, 30, 60, 120, 180, 240, 360, 480, 720]
  for (const cd_bars of cooldowns) {
    const trades = simulate(bars, cd_bars)
    const t = tt(trades.map(t=>t.netBps))
    const wr = trades.filter(t=>t.netBps>0).length/trades.length
    const cum = trades.map(t=>t.netBps).reduce((a,b)=>a+b,0)
    const sls = trades.filter(t=>t.stoppedOut).length
    const blocked = base.length - trades.length  // trades that were blocked
    const mo = monthlyPositive(trades)
    const label = cd_bars === 0 ? 'none' : `${cd_bars}min`
    console.log(
      `  ${label.padEnd(10)}  ${String(t.n).padStart(4)}  ${t.mean.toFixed(2).padStart(6)}  ${t.t.toFixed(2).padStart(6)} ${sig(t.t).padEnd(4)}  ${(wr*100).toFixed(0).padStart(2)}%  ${cum.toFixed(0).padStart(8)}  ${mo.padStart(6)}  ${sls.toString().padStart(4)}  ${blocked.toString().padStart(5)}`)
  }

  // ── Deep dive: what exactly are we blocking? ──
  console.log()
  console.log('3. WHAT GETS BLOCKED AT 240min COOLDOWN?')
  console.log('─'.repeat(90))
  console.log()

  // Find trades blocked by 240min cooldown
  const withCd = simulate(bars, 240)
  const blockedGross: number[] = []
  // Re-simulate tracking both what fires and what doesn't
  {
    let cd2=0, slDir2=0, slUntil2=-1
    for (let i=1440; i<bars.length; i++) {
      if (i < cd2) continue
      const { score, holdBars } = getScore(bars, i)
      if (Math.abs(score) < SCORE_THRESH) continue
      const dir = score > 0 ? 1 : -1

      if (slDir2 !== 0 && i < slUntil2 && dir === slDir2) {
        // This trade gets blocked — what would it have returned?
        const entry = bars[i].c
        let exitBar=i+holdBars, stoppedOut=false
        for (let j=i+1; j<=i+holdBars&&j<bars.length; j++) {
          if (dir*(bars[j].c-entry)/entry*10000 <= -STOP_BPS) { exitBar=j; stoppedOut=true; break }
        }
        if (exitBar >= bars.length) exitBar=bars.length-1
        const gross = stoppedOut ? -STOP_BPS : dir*(bars[exitBar].c-entry)/entry*10000
        blockedGross.push(gross - ENTRY_FEE - EXIT_FEE)
        cd2 = Math.max(exitBar+5, i+holdBars)
        continue
      }

      const entry = bars[i].c
      let exitBar=i+holdBars, stoppedOut=false
      for (let j=i+1; j<=i+holdBars&&j<bars.length; j++) {
        if (dir*(bars[j].c-entry)/entry*10000 <= -STOP_BPS) { exitBar=j; stoppedOut=true; break }
      }
      if (exitBar >= bars.length) exitBar=bars.length-1
      if (stoppedOut) { slDir2=dir; slUntil2=exitBar+240 }
      cd2 = Math.max(exitBar+5, i+holdBars)
    }
  }

  const tBlocked = tt(blockedGross)
  const tKept = tt(withCd.map(t=>t.netBps))
  console.log(`  Blocked trades (n=${blockedGross.length}): mean=${tBlocked.mean.toFixed(2)}bps  t=${tBlocked.t.toFixed(2)} ${sig(tBlocked.t)}`)
  console.log(`  Kept trades   (n=${tKept.n}):  mean=${tKept.mean.toFixed(2)}bps   t=${tKept.t.toFixed(2)} ${sig(tKept.t)}`)
  console.log()
  if (tBlocked.mean < 0) {
    console.log(`  ✅ Blocked trades are NEGATIVE on average (${tBlocked.mean.toFixed(2)}bps) → blocking saves money`)
    console.log(`  Cumulative saved by blocking: ${(blockedGross.reduce((a,b)=>a+b,0)).toFixed(0)} bps  =  ${blockedGross.length} trades × ${tBlocked.mean.toFixed(1)} bps avg`)
  } else {
    console.log(`  ✗ Blocked trades are POSITIVE — blocking costs money`)
  }

  // ── Verdict ──
  console.log()
  console.log('═'.repeat(90))
  console.log('VERDICT')
  console.log('═'.repeat(90))
  console.log()
  const best = cooldowns.map(cd_bars => {
    const trades = simulate(bars, cd_bars)
    return { cd_bars, ...tt(trades.map(t=>t.netBps)) }
  }).sort((a,b)=>b.t-a.t)[0]

  const baseline = tt(base.map(t=>t.netBps))
  console.log(`  Baseline (no cooldown): mean=${baseline.mean.toFixed(2)}bps  t=${baseline.t.toFixed(2)}  n=${baseline.n}`)
  console.log(`  Best cooldown (${best.cd_bars}min):   mean=${best.mean.toFixed(2)}bps  t=${best.t.toFixed(2)}  n=${best.n}`)
  console.log()
  const diff = best.mean - baseline.mean
  if (diff > 0) {
    console.log(`  ✅ SL cooldown IMPROVES edge by +${diff.toFixed(2)}bps/trade (+${(diff/baseline.mean*100).toFixed(0)}%)`)
    console.log(`  Mechanism: post-SL same-dir trades avg ${tSame.mean.toFixed(2)}bps vs all-trades avg ${tAll.mean.toFixed(2)}bps`)
    console.log(`  Blocking them avoids ${(tAll.mean-tSame.mean).toFixed(2)}bps of underperformance per blocked trade`)
  } else {
    console.log(`  ✗ SL cooldown does NOT help (${diff.toFixed(2)}bps/trade)`)
    console.log(`  Post-SL same-dir trades perform just as well as average — blocking costs opportunity`)
  }
}

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