/**
 * Backtest: extension direction-only vs magnitude threshold.
 * Tests the fix: drop |score|>=10 for extensions, use direction-only.
 *
 * Also sweeps: what magnitude threshold (if any) is optimal for extensions?
 */
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}
}

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

function simulate(
  bars: Bar[],
  extMagThresh: number,   // 0 = direction only, >0 = require |score|>=thresh to extend
  slCooldown: number      // 120 from previous backtest
): Trade[] {
  const trades: Trade[] = []
  let cd = 0
  let slDir = 0, slUntil = -1
  const STOP = 100, FEE = 4, ENTRY_THRESH = 10

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

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

    // Walk forward with extension checks at each deadline
    let deadline = i + holdBars
    let extensions = 0
    const maxExt = 10

    while (extensions <= maxExt) {
      if (deadline >= bars.length) { deadline = bars.length - 1; break }

      // Check stop along the way
      let stoppedAt = -1
      for (let j = (extensions === 0 ? i+1 : deadline - holdBars + 1); j <= deadline && j < bars.length; j++) {
        if (dir * (bars[j].c - entry) / entry * 10000 <= -STOP) { stoppedAt = j; break }
      }
      if (stoppedAt !== -1) { exitBar = stoppedAt; break }

      // At deadline: check extension
      const { score: extScore, holdBars: extHold } = getScore(bars, deadline)
      const directionMatch = (extScore * dir) > 0
      const magnitudeOk = extMagThresh === 0 ? true : Math.abs(extScore) >= extMagThresh
      const shouldExtend = directionMatch && magnitudeOk

      if (shouldExtend && extensions < maxExt) {
        extensions++
        extended = true
        deadline = Math.min(bars.length - 1, deadline + extHold)
      } else {
        exitBar = deadline
        break
      }
    }

    if (exitBar >= bars.length) exitBar = bars.length - 1

    // Check stop in final segment
    const finalStart = extensions > 0 ? deadline - holdBars : i
    for (let j = Math.max(i+1, finalStart); j <= exitBar && j < bars.length; j++) {
      if (dir * (bars[j].c - entry) / entry * 10000 <= -STOP) { exitBar = j; break }
    }

    const grossBps = dir * (bars[exitBar].c - entry) / entry * 10000
    const stoppedOut = grossBps <= -STOP + 0.1
    const netBps = stoppedOut ? -STOP - FEE : grossBps - FEE

    if (stoppedOut && slCooldown > 0) { slDir = dir; slUntil = exitBar + slCooldown }
    trades.push({ netBps, grossBps: stoppedOut ? -STOP : grossBps, month: new Date(bars[i].ts).toISOString().slice(0,7), extended })
    cd = Math.max(exitBar + 5, i + holdBars)
  }
  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) { 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('EXTENSION MAGNITUDE THRESHOLD BACKTEST')
  console.log('Entry threshold always 10. Extension threshold swept 0→10.')
  console.log('SL cooldown fixed at 120min (from previous backtest).')
  console.log('═'.repeat(90))
  console.log()

  console.log('  Threshold    n     mean    t-stat    WR    cum     pos/mo   exts   description')
  console.log('  ' + '─'.repeat(83))

  const SL = 120
  const rows: { thresh: number; trades: Trade[]; t: ReturnType<typeof tt> }[] = []

  for (const thresh of [10, 8, 5, 3, 1, 0]) {
    const trades = simulate(bars, thresh, SL)
    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 exts = trades.filter(t=>t.extended).length
    const mo = monthlyPositive(trades)
    const desc = thresh === 10 ? '← original (buggy: blocks short=-8)'
               : thresh === 0  ? '← fix: direction only'
               : ''
    rows.push({ thresh, trades, t })
    console.log(
      `  thresh=${String(thresh).padStart(2)}    ${String(t.n).padStart(5)}  ${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(7)}  ${mo.padStart(6)}  ${String(exts).padStart(5)}  ${desc}`)
  }

  // ── Extended trade analysis: do extensions with low score hurt? ──
  console.log()
  console.log('─'.repeat(90))
  console.log('QUALITY OF EXTENSION SEGMENTS by score at deadline (direction-only, thresh=0)')
  console.log()

  const d0 = simulate(bars, 0, SL)
  // Re-run to capture score at each extension point
  const extScores: { score: number; segNet: number }[] = []
  {
    let cd=0, slDir=0, slUntil=-1
    for (let i=1440; i<bars.length; i++) {
      if(i<cd)continue
      const{score,holdBars}=getScore(bars,i)
      if(Math.abs(score)<10)continue
      const dir=score>0?1:-1
      if(slDir!==0&&i<slUntil&&dir===slDir)continue
      const entry=bars[i].c
      let deadline=i+holdBars
      let prevDeadline=i
      while(deadline<bars.length) {
        let stopped=false
        for(let j=prevDeadline+1;j<=deadline&&j<bars.length;j++){
          if(dir*(bars[j].c-entry)/entry*10000<=-100){deadline=j;stopped=true;break}
        }
        if(stopped)break
        const{score:extScore,holdBars:extHold}=getScore(bars,deadline)
        const sameDir=(extScore*dir)>0
        if(!sameDir)break
        // Record: what is the gross of the extension segment?
        const nextDeadline=Math.min(bars.length-1,deadline+extHold)
        if(nextDeadline>=bars.length)break
        const segGross=dir*(bars[nextDeadline].c-bars[deadline].c)/bars[deadline].c*10000
        extScores.push({score:extScore,segNet:segGross-4})
        prevDeadline=deadline; deadline=nextDeadline
      }
      const exitBar = Math.min(deadline, bars.length - 2)
      const gross=dir*(bars[exitBar].c-entry)/entry*10000
      if(Math.abs(gross)<=100){if(Math.abs(gross)>99)slDir=dir,slUntil=exitBar+120}
      cd=Math.max(exitBar+5,i+holdBars)
    }
  }

  // Bucket by |score| at extension point
  console.log('  |score| at ext  n     mean net    t-stat')
  console.log('  ' + '─'.repeat(45))
  for (const [lo, hi] of [[0,3],[3,5],[5,8],[8,10],[10,15],[15,25],[25,99]] as [number,number][]) {
    const segs = extScores.filter(e => Math.abs(e.score) >= lo && Math.abs(e.score) < hi).map(e=>e.segNet)
    const t = tt(segs)
    if (t.n < 3) continue
    console.log(`  |${lo}–${hi}|          ${String(t.n).padStart(4)}  ${t.mean.toFixed(2).padStart(9)}bps  t=${t.t.toFixed(2)} ${sig(t.t)}`)
  }

  // ── Verdict ──
  console.log()
  console.log('═'.repeat(90))
  console.log('VERDICT')
  console.log('═'.repeat(90))
  const sorted = [...rows].sort((a,b) => b.t.t - a.t.t)
  const best = sorted[0]
  const original = rows.find(r=>r.thresh===10)!
  console.log()
  console.log(`  Original (thresh=10): mean=${original.t.mean.toFixed(2)}bps  t=${original.t.t.toFixed(2)}  n=${original.t.n}`)
  console.log(`  Best     (thresh=${best.thresh}): mean=${best.t.mean.toFixed(2)}bps  t=${best.t.t.toFixed(2)}  n=${best.t.n}`)
  console.log()
  const diff = best.t.mean - original.t.mean
  console.log(`  Improvement: +${diff.toFixed(2)}bps/trade (+${(diff/original.t.mean*100).toFixed(0)}% of edge)`)
  console.log(`  Mechanism: lower threshold allows extensions when score drifts 5–9`)
  console.log(`             (same structural signal, temporarily suppressed by BUYP/Rev24h)`)
}

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