/**
 * Backtest: hold until the score flips to the OPPOSITE direction.
 *
 * Exit rules:
 *   1. STOP: price moves -stopBps from entry → immediate exit
 *   2. FLIP: at every bar, recompute score. When (score * dir) < 0
 *            (i.e. score was positive for LONG, now negative) → exit next bar
 *   3. TIMEOUT: safety cap at maxBars to prevent infinite holds
 *
 * Compare vs current best:
 *   - Baseline:    time exit, hold=60-240min, ext_thresh=8  (t=3.40)
 *   - Flip-only:   no time exit, exit when score flips
 *   - Flip+min:    flip exit, but min hold of N bars first (avoid premature flips)
 *   - Flip+thresh: flip only when |flip_score| >= threshold (ignore weak flips)
 */
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}
}

function tt(v: number[]): { mean: number; t: number; n: number; wr: number; med: number } {
  const n=v.length; if(n<10) return {mean:0,t:0,n,wr:0,med:0}
  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)
  const s=[...v].sort((a,b)=>a-b)
  return {mean:m,t:se>0?m/se:0,n,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.89?'****':a>3.29?'***':a>2.58?'**':a>1.96?'*':'' }

interface TradeResult { net: number; holdBars: number; month: string; stopped: boolean; timedOut: boolean }

// --- Strategy 1: Baseline (time exit + extensions, current config) ---
function simBaseline(bars: Bar[], stopBps = 100, fee = 4): TradeResult[] {
  const results: TradeResult[] = []
  const entryThresh = 10, extThresh = 8, minHold = 60, maxHold = 240
  let cd = 0
  for (let i = 1440; i < bars.length; i++) {
    if (i < cd) continue
    const { score, holdBars: rawHold } = getScore(bars, i)
    if (Math.abs(score) < entryThresh) continue
    const dir = score > 0 ? 1 : -1
    const holdBars = Math.max(minHold, Math.min(maxHold, rawHold))
    const entry = bars[i].c
    let deadline = i + holdBars
    let exitBar = deadline, stopped = false, curStart = i
    while (true) {
      if (deadline >= bars.length) { exitBar = bars.length - 2; break }
      for (let j = curStart + 1; j <= deadline && j < bars.length; j++) {
        if (dir * (bars[j].c - entry) / entry * 10000 <= -stopBps) { exitBar = j; stopped = true; break }
      }
      if (stopped) break
      const { score: es, holdBars: erh } = getScore(bars, deadline)
      const eh = Math.max(minHold, Math.min(maxHold, erh))
      if ((es * dir) > 0 && Math.abs(es) >= extThresh) { curStart = deadline; deadline = Math.min(bars.length-2, deadline+eh); continue }
      exitBar = deadline; break
    }
    if (exitBar >= bars.length) exitBar = bars.length - 2
    const gross = stopped ? -stopBps : dir * (bars[exitBar].c - entry) / entry * 10000
    const hold = exitBar - i
    results.push({ net: gross - fee, holdBars: hold, month: new Date(bars[i].ts).toISOString().slice(0,7), stopped, timedOut: false })
    cd = exitBar + 5
  }
  return results
}

// --- Strategy 2: Flip exit ---
// minHold: don't check for flip in first N bars
// flipThresh: only exit if |flipScore| >= threshold (strong enough opposite signal)
// maxBars: safety timeout
function simFlip(
  bars: Bar[],
  entryThresh = 10,
  minHold = 0,
  flipThresh = 0,        // min |score| on the opposite side to trigger exit
  maxBars = 1440,        // safety cap (24h)
  stopBps = 100,
  fee = 4
): TradeResult[] {
  const results: TradeResult[] = []
  let cd = 0
  for (let i = 1440; i < bars.length; i++) {
    if (i < cd) continue
    const { score: entryScore } = getScore(bars, i)
    if (Math.abs(entryScore) < entryThresh) continue
    const dir = entryScore > 0 ? 1 : -1
    const entry = bars[i].c
    let exitBar = -1, stopped = false, timedOut = false

    for (let j = i + 1; j < Math.min(bars.length, i + maxBars + 1); j++) {
      // Stop check
      if (dir * (bars[j].c - entry) / entry * 10000 <= -stopBps) {
        exitBar = j; stopped = true; break
      }
      // Flip check (after minHold)
      if (j - i >= minHold) {
        const { score: curScore } = getScore(bars, j)
        const flipped = (curScore * dir) < 0                              // opposite direction
        const strongEnough = flipThresh === 0 || Math.abs(curScore) >= flipThresh
        if (flipped && strongEnough) { exitBar = j; break }
      }
    }

    if (exitBar === -1) {
      // Timed out — exit at safety cap
      exitBar = Math.min(bars.length - 2, i + maxBars)
      timedOut = true
    }

    const gross = stopped ? -stopBps : dir * (bars[exitBar].c - entry) / entry * 10000
    results.push({ net: gross - fee, holdBars: exitBar - i, month: new Date(bars[i].ts).toISOString().slice(0,7), stopped, timedOut })
    cd = exitBar + 5
  }
  return results
}

function monthlyBreakdown(results: TradeResult[]): void {
  const m = new Map<string, number[]>()
  for (const r of results) {
    if (!m.has(r.month)) m.set(r.month, [])
    m.get(r.month)!.push(r.net)
  }
  const months = [...m.keys()].sort()
  let pos = 0
  for (const mo of months) {
    const v = m.get(mo)!
    const { mean, t, n } = tt(v)
    const dir = mean >= 0 ? '✓' : '✗'
    if (mean >= 0) pos++
    console.log(`  ${mo}  n=${String(n).padStart(3)}  mean=${mean>=0?'+':''}${mean.toFixed(1).padStart(5)}bps  t=${t.toFixed(2).padStart(5)}  ${dir}`)
  }
  console.log(`  Positive months: ${pos}/${months.length}`)
}

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

  // ── 1. Baseline ──
  const base = simBaseline(bars)
  const bStats = tt(base.map(r => r.net))
  const bStops = base.filter(r => r.stopped).length
  const bAvgHold = Math.round(base.reduce((s,r) => s+r.holdBars,0)/base.length)
  console.log('══════════════════════════════════════════════════════')
  console.log('BASELINE (time exit + ext_thresh=8, hold=60-240min)')
  console.log('══════════════════════════════════════════════════════')
  console.log(`n=${bStats.n}  mean=${bStats.mean.toFixed(2)}bps  t=${bStats.t.toFixed(2)}${sig(bStats.t)}  WR=${(bStats.wr*100).toFixed(1)}%  med=${bStats.med.toFixed(1)}bps  stops=${bStops} (${(bStops/base.length*100).toFixed(0)}%)  avgHold=${bAvgHold}min`)
  monthlyBreakdown(base)

  // ── 2. Flip exit sweep ──
  console.log('\n══════════════════════════════════════════════════════')
  console.log('FLIP EXIT SWEEP (exit when score flips direction)')
  console.log('══════════════════════════════════════════════════════')
  console.log('minHold=min bars before checking flip, flipThresh=min |score| to accept flip')
  console.log()
  console.log('minHold  flipThresh  maxBars    n     mean     t       WR      avgHold  stops  timeout')
  console.log('─'.repeat(90))

  const configs = [
    // Pure flip (no minimum hold, any flip)
    { minHold: 0,   flipThresh: 0,  maxBars: 1440 },
    { minHold: 0,   flipThresh: 5,  maxBars: 1440 },
    { minHold: 0,   flipThresh: 10, maxBars: 1440 },
    { minHold: 0,   flipThresh: 15, maxBars: 1440 },
    // With minimum hold to avoid premature flips
    { minHold: 30,  flipThresh: 0,  maxBars: 1440 },
    { minHold: 30,  flipThresh: 5,  maxBars: 1440 },
    { minHold: 30,  flipThresh: 10, maxBars: 1440 },
    { minHold: 60,  flipThresh: 0,  maxBars: 1440 },
    { minHold: 60,  flipThresh: 5,  maxBars: 1440 },
    { minHold: 60,  flipThresh: 10, maxBars: 1440 },
    { minHold: 60,  flipThresh: 15, maxBars: 1440 },
    { minHold: 120, flipThresh: 0,  maxBars: 1440 },
    { minHold: 120, flipThresh: 5,  maxBars: 1440 },
    { minHold: 120, flipThresh: 10, maxBars: 1440 },
    { minHold: 240, flipThresh: 0,  maxBars: 1440 },
    { minHold: 240, flipThresh: 5,  maxBars: 1440 },
    { minHold: 240, flipThresh: 10, maxBars: 1440 },
    // Extended safety cap
    { minHold: 60,  flipThresh: 10, maxBars: 720  },
    { minHold: 60,  flipThresh: 10, maxBars: 2880 },
  ]

  let best = { t: -99, label: '' }
  const results: Array<{ cfg: typeof configs[0]; stats: ReturnType<typeof tt>; avgHold: number; stops: number; timeouts: number }> = []

  for (const cfg of configs) {
    const r = simFlip(bars, 10, cfg.minHold, cfg.flipThresh, cfg.maxBars)
    const s = tt(r.map(x => x.net))
    const avgHold = Math.round(r.reduce((a,b) => a+b.holdBars, 0)/r.length)
    const stops = r.filter(x => x.stopped).length
    const timeouts = r.filter(x => x.timedOut).length
    results.push({ cfg, stats: s, avgHold, stops, timeouts })
    if (s.t > best.t) best = { t: s.t, label: `minHold=${cfg.minHold} flipThresh=${cfg.flipThresh} maxBars=${cfg.maxBars}` }
    console.log(
      `${String(cfg.minHold).padStart(7)}  ${String(cfg.flipThresh).padStart(10)}  ${String(cfg.maxBars).padStart(7)}` +
      `  ${String(s.n).padStart(5)}` +
      `  ${(s.mean>=0?'+':'')+s.mean.toFixed(2).padStart(6)}bps` +
      `  ${s.t.toFixed(2).padStart(6)}${sig(s.t).padEnd(4)}` +
      `  ${(s.wr*100).toFixed(1).padStart(5)}%` +
      `  ${String(avgHold).padStart(7)}min` +
      `  ${stops} (${(stops/s.n*100).toFixed(0)}%)`.padStart(8) +
      `  ${timeouts} (${(timeouts/s.n*100).toFixed(0)}%)`
    )
  }

  // ── 3. Best flip config — monthly breakdown ──
  const bestCfg = results.sort((a,b) => b.stats.t - a.stats.t)[0]
  console.log(`\n══════════════════════════════════════════════════════`)
  console.log(`BEST FLIP CONFIG: minHold=${bestCfg.cfg.minHold} flipThresh=${bestCfg.cfg.flipThresh} maxBars=${bestCfg.cfg.maxBars}`)
  console.log(`t=${bestCfg.stats.t.toFixed(2)}${sig(bestCfg.stats.t)}  mean=${bestCfg.stats.mean.toFixed(2)}bps  n=${bestCfg.stats.n}`)
  console.log('══════════════════════════════════════════════════════')
  const bestResults = simFlip(bars, 10, bestCfg.cfg.minHold, bestCfg.cfg.flipThresh, bestCfg.cfg.maxBars)
  monthlyBreakdown(bestResults)

  // Hold distribution of best flip
  const holds = bestResults.map(r => r.holdBars)
  holds.sort((a,b)=>a-b)
  console.log(`\nHold distribution (best flip):`)
  console.log(`  p10=${holds[Math.floor(holds.length*0.10)]}min  p25=${holds[Math.floor(holds.length*0.25)]}min  p50=${holds[Math.floor(holds.length*0.50)]}min  p75=${holds[Math.floor(holds.length*0.75)]}min  p90=${holds[Math.floor(holds.length*0.90)]}min  max=${holds[holds.length-1]}min`)

  // ── 4. Summary comparison ──
  console.log('\n══════════════════════════════════════════════════════')
  console.log('SUMMARY: BASELINE vs BEST FLIP')
  console.log('══════════════════════════════════════════════════════')
  console.log(`Baseline:    n=${bStats.n}  mean=${bStats.mean.toFixed(2)}bps  t=${bStats.t.toFixed(2)}${sig(bStats.t)}  WR=${(bStats.wr*100).toFixed(1)}%  avgHold=${bAvgHold}min`)
  console.log(`Best flip:   n=${bestCfg.stats.n}  mean=${bestCfg.stats.mean.toFixed(2)}bps  t=${bestCfg.stats.t.toFixed(2)}${sig(bestCfg.stats.t)}  WR=${(bestCfg.stats.wr*100).toFixed(1)}%  avgHold=${bestCfg.avgHold}min`)
  console.log()
  const winner = bestCfg.stats.t > bStats.t ? '✓ FLIP IS BETTER' : '✗ BASELINE IS BETTER'
  console.log(`Winner: ${winner}  (Δt = ${(bestCfg.stats.t - bStats.t).toFixed(2)})`)
}

main().catch(console.error)
