// @ts-nocheck
import fs from 'node:fs'
import readline from 'node:readline'

type Bar = { ts:number; o:number; h:number; l:number; c:number; v?:number }
type Trade = { gross:number; net:number; month:string; year:number; side:'long'|'short'; bar:number; sizingBps:number; softSlBps:number; isStructL2:boolean; isFallback:boolean; isSl:boolean; isHardSl:boolean; hour:number; dow:number; reason?:string; wouldRecover?:boolean; mfeAfterStopBps?:number; barsToRecover?:number }
type WideCfg = { label:string; softMode:'current'|'none'; hardExtraBps:number; riskExtraBps:number; riskScale?:number }

async function loadBars(files:string[]):Promise<Bar[]>{ const seen=new Set<number>(), all:Bar[]=[]; for(const f of files){ if(!fs.existsSync(f))continue; const rl=readline.createInterface({input:fs.createReadStream(f),crlfDelay:Infinity}); for await(const line of rl){ if(!line.trim())continue; const b=JSON.parse(line); if(!seen.has(b.ts)){seen.add(b.ts); all.push(b)} } } all.sort((a,b)=>a.ts-b.ts); return all }
function lbRet(bars:Bar[],i:number,n:number){return i>=n&&bars[i-n].c>0?(bars[i].c-bars[i-n].c)/bars[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(bars:Bar[],i:number,n:number){let s=0; for(let j=Math.max(0,i-n+1);j<=i;j++)s+=closePos(bars[j]); return s/Math.min(n,i+1)}
function rsi(bars: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 x=bars[j].c-bars[j-1].c; if(x>0)u+=x; else d-=x} return d===0?100:100-100/(1+u/d)}
function computeScore(bars:Bar[],i:number){const d=new Date(bars[i].ts),h=d.getUTCHours(),m=d.getUTCMinutes(),dow=d.getUTCDay(); let w=0,hW=0,tW=0; const add=(dir:number,wt:number,hold:number)=>{w+=dir*wt; hW+=hold*wt; tW+=wt}; if(dow===4)add(-1,20,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,6,240); if(h===22)add(1,2.86,120); if(h===21)add(1,17.9,120); if(h===20)add(1,9.49,60); if(h===23)add(-1,8.94,30); if(i>=60){const bp=avgCP(bars,i,60); if(bp>0.58)add(1,15.49,240); if(bp<0.42)add(-1,8,240)} if(i>=60&&(h===13||(h===14&&m<=15))&&m<=15){const gap=lbRet(bars,i,60); if(Math.abs(gap)>5)add(gap>0?-1:1,15,120)} if(i>=60){const rv=rsi(bars,i,60); if(rv<30)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,240)} return {score:w, hold:tW>0?Math.max(60,Math.min(240,Math.round(hW/tW))):120} }
const SWING_N=5, SWING_LOOKBACK=480
function compositeLiquidityLevel(bars:Bar[],i:number,dir:1|-1,refPrice?:number){const N=i+1,price=refPrice??bars[i].c,candidates:number[]=[]; candidates.push(dir>0?Math.floor(price/1000)*1000:Math.ceil(price/1000)*1000,dir>0?Math.floor(price/500)*500:Math.ceil(price/500)*500,dir>0?Math.floor(price/250)*250:Math.ceil(price/250)*250); let bestSwing:number|null=null,bestDist=Infinity; const scanEnd=N-SWING_N-1, scanStart=Math.max(SWING_N,N-SWING_LOOKBACK-SWING_N); for(let j=scanEnd;j>=scanStart;j--){let ok=true; for(let k=1;k<=SWING_N&&ok;k++){ if(dir>0){ if((bars[j-k]?.l??Infinity)<=bars[j].l)ok=false; if((bars[j+k]?.l??Infinity)<=bars[j].l)ok=false } else { if((bars[j-k]?.h??0)>=bars[j].h)ok=false; if((bars[j+k]?.h??0)>=bars[j].h)ok=false }} if(!ok)continue; const lvl=dir>0?bars[j].l:bars[j].h; if(dir>0?lvl>=price:lvl<=price)continue; const dist=Math.abs(price-lvl); if(dist<bestDist){bestDist=dist; bestSwing=lvl}} if(bestSwing!==null)candidates.push(bestSwing); const d=new Date(bars[N-1].ts); d.setUTCHours(0,0,0,0); const today=d.getTime(); let pdH=0,pdL=Infinity; for(let j=N-1;j>=0;j--){if(bars[j].ts>=today)continue; if(bars[j].ts<today-86400000)break; if(bars[j].h>pdH)pdH=bars[j].h; if(bars[j].l<pdL)pdL=bars[j].l} if(dir>0&&pdL<price&&pdL>0)candidates.push(pdL); if(dir<0&&pdH>price&&pdH>0)candidates.push(pdH); const volMap=new Map<number,number>(); for(let j=N-1;j>=0&&bars[j].ts>=today;j--){const bucket=Math.round(bars[j].c/50)*50; volMap.set(bucket,(volMap.get(bucket)??0)+(bars[j].v??0))} let maxV=0,poc=0; volMap.forEach((v,p)=>{if(v>maxV){maxV=v;poc=p}}); if(poc>0&&(dir>0?poc<price:poc>price))candidates.push(poc); const valid=candidates.filter(l=>dir>0?l<=price:l>=price); if(valid.length===0)return dir>0?Math.floor(price/250)*250:Math.ceil(price/250)*250; return valid.reduce((b,c)=>Math.abs(price-c)<Math.abs(price-b)?c:b)}
function stopLiquidityLevel(bars:Bar[],i:number,dir:1|-1,refPrice:number){const N=i+1,price=refPrice; const c=[dir>0?Math.floor(price/1000)*1000:Math.ceil(price/1000)*1000,dir>0?Math.floor(price/500)*500:Math.ceil(price/500)*500,dir>0?Math.floor(price/250)*250:Math.ceil(price/250)*250]; const d=new Date(bars[N-1].ts); d.setUTCHours(0,0,0,0); const today=d.getTime(); let pdH=0,pdL=Infinity; for(let j=N-1;j>=0;j--){if(bars[j].ts>=today)continue; if(bars[j].ts<today-86400000)break; if(bars[j].h>pdH)pdH=bars[j].h; if(bars[j].l<pdL)pdL=bars[j].l} if(dir>0&&pdL<price&&pdL>0)c.push(pdL); if(dir<0&&pdH>price&&pdH>0)c.push(pdH); const v=c.filter(l=>dir>0?l<=price:l>=price); if(v.length===0)return dir>0?Math.floor(price/250)*250:Math.ceil(price/250)*250; return v.reduce((b,x)=>Math.abs(price-x)<Math.abs(price-b)?x:b)}
function stats(v:number[]){const n=v.length;if(n<3)return{n,mean:0,sd:0,t:0,wr:0};const mean=v.reduce((a,b)=>a+b,0)/n;const sd=Math.sqrt(v.reduce((s,x)=>s+(x-mean)**2,0)/(n-1));return{n,mean,sd,t:sd>0?mean/(sd/Math.sqrt(n)):0,wr:v.filter(x=>x>0).length/n}}
function sig(t:number){const a=Math.abs(t);return a>3.89?'****':a>3.29?'***':a>2.58?'**':a>1.96?'*':''}
function monthly(tr:Trade[],fixed=1500){const m:Record<string,number>={};for(const t of tr)m[t.month]=(m[t.month]??0)+fixed*t.net/10000;return m}
function actualMonthly(tr:Trade[],acct=10000,risk=0.03){const m:Record<string,number>={};for(const t of tr){const notional=acct*risk*(t.riskScale??1)/(t.sizingBps/10000);m[t.month]=(m[t.month]??0)+notional*t.net/10000}return m}
function ir(vals:number[]){if(vals.length<3)return 0;const s=stats(vals);return s.sd>0?s.mean/s.sd:0}
function moIR(tr:Trade[]){return ir(Object.values(monthly(tr)))}
function actualIR(tr:Trade[]){return ir(Object.values(actualMonthly(tr)))}
function maxDD(tr:Trade[],risk=0.03){let eq=500,pk=500,dd=0;for(const t of tr){eq+=eq*risk*(t.riskScale??1)/(t.sizingBps/10000)*t.net/10000;if(eq>pk)pk=eq;dd=Math.max(dd,(pk-eq)/pk)}return dd}
function pairedMaps(a:Record<string,number>,b:Record<string,number>){const months=[...new Set([...Object.keys(a),...Object.keys(b)])].sort();const diffs=months.map(m=>(b[m]??0)-(a[m]??0));const s=stats(diffs);return{n:diffs.length,imp:diffs.filter(x=>x>0).length,mean:s.mean,t:s.t,total:diffs.reduce((x,y)=>x+y,0)}}
function metric(label:string,tr:Trade[]){const s=stats(tr.map(t=>t.net)),mp=Object.values(monthly(tr));const sl=tr.filter(t=>t.isSl),hard=tr.filter(t=>t.isHardSl);return{label,n:s.n,mean:s.mean,t:s.t,wr:s.wr,moIR:moIR(tr),actIR:actualIR(tr),maxDD:maxDD(tr),mp:mp.filter(x=>x>0).length,mt:mp.length,slRate:tr.length?sl.length/tr.length:0,hardRate:sl.length?hard.length/sl.length:0}}
function reasonCounts(tr:Trade[]){const m=new Map<string,number>();for(const t of tr)m.set(t.reason??'',(m.get(t.reason??'')??0)+1);return[...m.entries()].sort((a,b)=>b[1]-a[1]).map(([k,v])=>`${k}:${v}`).join(' ')}
function withFee(tr:Trade[],fee:number){return tr.map(t=>({...t,net:t.gross-fee}))}
function fmt(m:ReturnType<typeof metric>,ref?:Trade[],tr?:Trade[]){let p=''; if(ref&&tr){const pa=pairedMaps(actualMonthly(ref),actualMonthly(tr));p=` actualΔ=$${pa.mean.toFixed(0)}(${pa.imp}/${pa.n},t=${pa.t.toFixed(2)}) total=$${Object.values(actualMonthly(tr)).reduce((a,b)=>a+b,0).toFixed(0)}`} return `${m.label.padEnd(18)} n=${String(m.n).padStart(4)} mean=${m.mean.toFixed(2).padStart(6)} t=${m.t.toFixed(2)}${sig(m.t).padEnd(4)} WR=${(m.wr*100).toFixed(1)}% moIR=${m.moIR.toFixed(2)} actIR=${m.actIR.toFixed(2)} maxDD=${(m.maxDD*100).toFixed(1)}% m+=${m.mp}/${m.mt} SL=${(m.slRate*100).toFixed(1)}% hard=${(m.hardRate*100).toFixed(1)}%${p}`}
function recoveryStats(tr:Trade[]){const stops=tr.filter(t=>t.isSl);const rec=stops.filter(t=>t.wouldRecover);return`stops=${stops.length} recover=${rec.length} (${stops.length?(rec.length/stops.length*100).toFixed(1):'0'}%) avgMFE=${(stats(stops.map(t=>t.mfeAfterStopBps??0)).mean).toFixed(1)} barsToRec=${(stats(rec.map(t=>t.barsToRecover??0)).mean).toFixed(1)}`}

type ReCfg = {
  label: string
  reclaim: 'none' | 'L2close' | 'L2touch' | 'L1close'
  windowBars?: number
  scoreMin?: number
  reRiskScale?: number
}

type ExitSpec = {
  dir: 1|-1
  side: 'long'|'short'
  entryPx: number
  entryBar: number
  slPx: number
  hardSlPx: number
  softSlBps: number
  hardSlBps: number
  sizingBps: number
  riskScale: number
  prefix?: string
}

function simulateExit(bars: Bar[], spec: ExitSpec): { trade: Trade; exitBar: number } {
  const {dir,side,entryPx,entryBar,slPx,hardSlPx,softSlBps,hardSlBps,sizingBps,riskScale,prefix=''} = spec
  const EXT=8, MIN_H=60, MAX_H=240, CAP=420, N=bars.length
  const startScore = computeScore(bars, entryBar)
  const hardCap = entryBar + CAP
  let deadline = entryBar + Math.max(MIN_H, Math.min(MAX_H, startScore.hold))
  let exitBar=-1, isSl=false, isHardSl=false, gross=0, reason='time'

  for (let j=entryBar+1; j<Math.min(N, hardCap+1); j++) {
    const hardTouched = dir>0 ? bars[j].l <= hardSlPx : bars[j].h >= hardSlPx
    const softClosed = dir>0 ? bars[j].c <= slPx : bars[j].c >= slPx
    if (hardTouched) { exitBar=j; isSl=true; isHardSl=true; reason='hard_sl'; gross=-hardSlBps; break }
    if (softClosed) { exitBar=j; isSl=true; reason='soft_sl'; gross=-softSlBps; break }
    if (j >= entryBar + MIN_H) {
      const es=computeScore(bars,j)
      if (es.score*dir >= EXT) {
        const proposed = Math.min(j + Math.max(MIN_H, Math.min(MAX_H, es.hold)), hardCap)
        if (proposed > deadline) deadline = proposed
      }
    }
    if (j >= deadline) { exitBar=j; reason='time'; gross=dir*(bars[j].c-entryPx)/entryPx*10000; break }
  }
  if (exitBar < 0) { exitBar=Math.min(N-2,hardCap); reason='cap'; gross=dir*(bars[exitBar].c-entryPx)/entryPx*10000 }
  const dt = new Date(bars[entryBar].ts)
  const trade: Trade = { gross, net:gross, reason: prefix+reason, isSl, isHardSl, month:dt.toISOString().slice(0,7), year:dt.getUTCFullYear(), side, bar:entryBar, softSlBps, sizingBps, isStructL2:true, isFallback:false, hour:dt.getUTCHours(), dow:dt.getUTCDay(), riskScale, isReentry: !!prefix }
  return {trade, exitBar}
}

function findReclaimBar(bars: Bar[], cfg: ReCfg, dir: 1|-1, side:'long'|'short', stopBar:number, target:number, slPx:number): {bar:number; px:number} | null {
  const end = Math.min(bars.length-2, stopBar + (cfg.windowBars ?? 60))
  const scoreMin = cfg.scoreMin ?? (side === 'long' ? 16 : 8)
  for (let j=stopBar+1; j<=end; j++) {
    const scoreInDir = computeScore(bars,j).score * dir
    if (scoreInDir < scoreMin) continue
    let ok=false
    if (cfg.reclaim === 'L2close') ok = dir>0 ? bars[j].c > slPx : bars[j].c < slPx
    else if (cfg.reclaim === 'L2touch') ok = dir>0 ? bars[j].h >= slPx : bars[j].l <= slPx
    else if (cfg.reclaim === 'L1close') ok = dir>0 ? bars[j].c > target : bars[j].c < target
    if (ok) return {bar:j, px: cfg.reclaim === 'L2touch' ? slPx : bars[j].c}
  }
  return null
}

function simulateReclaim(bars: Bar[], cfg: ReCfg): Trade[] {
  const trades: Trade[] = [], stopCounts = new Map<string,number>()
  let cooldown = 0
  const WAIT=30, CONFIRM=4, SL_CD=30, N=bars.length
  for (let i=20160; i<N-WAIT; i++) {
    if (i < cooldown) continue
    const {score}=computeScore(bars,i)
    if (score===0) continue
    const dir:1|-1 = score>0 ? 1 : -1
    const side = dir>0 ? 'long' : 'short'
    const thresh = dir>0 ? 16 : 8
    if (Math.abs(score) < thresh) continue
    let confirmed=true
    for (let k=1;k<CONFIRM;k++) { const sk=computeScore(bars,i-k).score; if (sk*dir<thresh) { confirmed=false; break } }
    if (!confirmed) continue
    if (dir*lbRet(bars,i,4320)<-600) continue
    if (dir*lbRet(bars,i,10080)<-700) continue
    if (dir*lbRet(bars,i,20160)<-800) continue
    const day = new Date(bars[i].ts).toISOString().slice(0,10), ck=`${day}_${side}`
    if ((stopCounts.get(ck)??0) >= 2) continue

    const target = compositeLiquidityLevel(bars,i,dir)
    const distBps = Math.abs(bars[i].c-target)/bars[i].c*10000
    let entryPx=bars[i].c, entryBar=i, filled=distBps<=10
    if (!filled) for (let j=i+1;j<=Math.min(N-1,i+WAIT);j++) {
      const hit = dir>0 ? bars[j].l<=target : bars[j].h>=target
      if (hit) { filled=true; entryBar=j; entryPx=target; break }
    }
    if (!filled) continue

    const slRef = dir>0 ? target-0.01 : target+0.01
    const slL2 = stopLiquidityLevel(bars,i,dir,slRef)
    const slBps = Math.abs(target-slL2)/target*10000
    const isStruct = slBps>=15 && slBps<=200
    const softSlBps = isStruct ? slBps : 100
    const slPx = isStruct ? slL2 : (dir>0 ? target*0.99 : target*1.01)
    const l3ref = dir>0 ? slPx-0.01 : slPx+0.01
    const slL3 = stopLiquidityLevel(bars,i,dir,l3ref)
    const l3Gap = Math.max(10, Math.min(100, Math.abs(slPx-slL3)/slPx*10000))
    const hardSlBps = isStruct ? softSlBps + l3Gap + 8 : 133
    const hardSlPx = isStruct ? (dir>0 ? slPx*(1-l3Gap/10000) : slPx*(1+l3Gap/10000)) : (dir>0 ? target*(1-1.25/100) : target*(1+1.25/100))
    const sizingBps = isStruct ? softSlBps + l3Gap : 125

    const first = simulateExit(bars, {dir,side,entryPx,entryBar,slPx,hardSlPx,softSlBps,hardSlBps,sizingBps,riskScale:1})
    trades.push({...first.trade, isStructL2:isStruct, isFallback:!isStruct})
    let finalExitBar = first.exitBar
    if (first.trade.isSl) {
      stopCounts.set(ck,(stopCounts.get(ck)??0)+1)
      if (cfg.reclaim !== 'none') {
        const rec = findReclaimBar(bars,cfg,dir,side,first.exitBar,target,slPx)
        if (rec) {
          const reSizingBps = Math.max(10, Math.abs(rec.px-hardSlPx)/rec.px*10000)
          const reSoftSlBps = Math.max(1, Math.abs(rec.px-slPx)/rec.px*10000)
          const reHardSlBps = Math.abs(rec.px-hardSlPx)/rec.px*10000 + 8
          const re = simulateExit(bars, {dir,side,entryPx:rec.px,entryBar:rec.bar,slPx,hardSlPx,softSlBps:reSoftSlBps,hardSlBps:reHardSlBps,sizingBps:reSizingBps,riskScale:cfg.reRiskScale??0.5,prefix:'re_'})
          trades.push({...re.trade, isStructL2:isStruct, isFallback:!isStruct})
          finalExitBar = re.exitBar
          if (re.trade.isSl) stopCounts.set(ck,(stopCounts.get(ck)??0)+1)
        }
      }
    }
    cooldown = finalExitBar + ((first.trade.isSl || trades.at(-1)?.isSl) ? SL_CD : 5)
  }
  return trades
}

function reStats(tr: Trade[]) { const re=tr.filter(t=>t.isReentry); const m=metric('re',re); return `reN=${re.length} reMean=${m.mean.toFixed(1)} reT=${m.t.toFixed(2)} reWR=${(m.wr*100).toFixed(1)}% reReasons=${reasonCounts(re)}` }

;(async()=>{
  console.log('Loading bars...')
  const bars=await loadBars(['data/klines/BTCUSDT-1m-2022-2025.jsonl','data/klines/BTCUSDT-1m-2022-2025b.jsonl','data/klines/BTCUSDT-1m.jsonl'])
  console.log(`${bars.length.toLocaleString()} bars`)
  const cfgs:ReCfg[]=[
    {label:'current',reclaim:'none'},
    {label:'L2close30 score8 r50',reclaim:'L2close',windowBars:30,scoreMin:8,reRiskScale:0.5},
    {label:'L2close60 score8 r50',reclaim:'L2close',windowBars:60,scoreMin:8,reRiskScale:0.5},
    {label:'L2close120 score8 r50',reclaim:'L2close',windowBars:120,scoreMin:8,reRiskScale:0.5},
    {label:'L2close60 thresh r50',reclaim:'L2close',windowBars:60,scoreMin:999,reRiskScale:0.5},
    {label:'L2close60 score8 r25',reclaim:'L2close',windowBars:60,scoreMin:8,reRiskScale:0.25},
    {label:'L2close60 score8 r100',reclaim:'L2close',windowBars:60,scoreMin:8,reRiskScale:1},
    {label:'L1close60 score8 r50',reclaim:'L1close',windowBars:60,scoreMin:8,reRiskScale:0.5},
    {label:'L2touch30 score8 r50',reclaim:'L2touch',windowBars:30,scoreMin:8,reRiskScale:0.5},
  ]
  // Fix threshold config dynamically: long threshold differs from short, so use 16 universally is conservative enough.
  cfgs.find(c=>c.label==='L2close60 thresh r50')!.scoreMin=16
  const sims=cfgs.map(cfg=>({cfg,trades:simulateReclaim(bars,cfg)}))
  for (const fee of [0,4]) {
    console.log(`\n================ RECLAIM RE-ENTRY feeRT=${fee} ================`)
    const ref=withFee(sims[0].trades,fee)
    for (const sim of sims) {
      const tr=withFee(sim.trades,fee)
      console.log(fmt(metric(sim.cfg.label,tr),sim.cfg.label==='current'?undefined:ref,sim.cfg.label==='current'?undefined:tr))
      console.log('  '+reasonCounts(tr)+' | '+reStats(tr))
    }
  }
})().catch(e=>{console.error(e);process.exit(1)})
