/**
 * orderflow-exit-validation.ts
 *
 * Research-only test: keep the current production score-based entry stack
 * (L16/S8 cap420 wait30 imm10), but add exits based on whether short-term
 * tape/orderflow still supports the open position.
 *
 * Uses the old archived tape logic as the source of the orderflow signal:
 *   - 10s bars with buy/sell notional
 *   - fast window = 5 x 10s bars
 *   - slow window = 60 x 10s bars
 *   - score = 0.45*deltaScore + 0.35*priceScore + burst/large-print terms omitted
 *
 * Historical scope is limited to directories under data/historical/<date>/,
 * because the 4yr kline dataset does not contain trade-side delta.
 */

import fs from 'node:fs'
import readline from 'node:readline'

interface Bar { ts: number; o: number; h: number; l: number; c: number; v: number }
interface FlowBar { ts: number; o: number; h: number; l: number; c: number; buyVol: number; sellVol: number; n: number }
interface FlowSig { score: number; delta: number; moveBps: number; fastVol: number; surge: number }

interface Trade {
  side: 'long' | 'short'
  net: number
  gross: number
  reason: string
  hold: number
  entryTs: number
  exitTs: number
  sizingBps: number
  session: string
}

interface ExitCfg {
  label: string
  minHoldBars: number
  confirmBars: number
  mode: 'none' | 'contraScore' | 'contraDelta' | 'lostScore' | 'deltaAndMove'
  threshold: number
  /** Optional post-flow-exit cooldown. Without this, persistent score windows
   * can immediately re-enter after a flow exit, creating churn. */
  flowCooldownBars?: number
}

const LONG_ENTRY_THRESH = 16
const SHORT_ENTRY_THRESH = 8
const EXT = 8
const WAIT = 30
const MIN_H = 60
const MAX_H = 240
const CAP = 420
const CONFIRM = 4
const SL_COOLDOWN_BARS = 30
const STOP_CAP_PER_DAY = 2
const IMMEDIATE_DIST_BPS = 10

const SWING_N = 5
const SWING_LOOKBACK = 480

function sig(t: number): string { const a=Math.abs(t); return a>3.89?'****':a>3.29?'***':a>2.58?'**':a>1.96?'*':'' }
function mean(xs: number[]): number { return xs.length ? xs.reduce((a,b)=>a+b,0)/xs.length : 0 }
function sd(xs: number[]): number { if(xs.length<2)return 0; const m=mean(xs); return Math.sqrt(xs.reduce((s,x)=>s+(x-m)**2,0)/(xs.length-1)) }
function stats(xs: number[]) { const n=xs.length, m=mean(xs), s=sd(xs), t=s>0?m/(s/Math.sqrt(n)):0, wr=n?xs.filter(x=>x>0).length/n:0; return {n,mean:m,sd:s,t,wr} }
function lbRet(bars: Bar[], i: number, n: number): 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): number { return b.h>b.l ? (b.c-b.l)/(b.h-b.l) : 0.5 }
function avgCP(bars: Bar[], i: number, n: number): 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): 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): { score: number; hold: number } {
  const d = new Date(bars[i].ts)
  const 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.00,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.00,240)
  if(h===22)add(+1, 2.86,120)
  if(h===21)add(+1,17.90,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.00,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.00,120) }
  if(i>=60){ const rr=rsi(bars,i,60); if(rr<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.0,240) }
  return { score:w, hold:tW>0?Math.max(MIN_H,Math.min(MAX_H,Math.round(hW/tW))):120 }
}

function compositeLiquidityLevel(bars: Bar[], i: number, dir: 1|-1, refPrice?: number): 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--){ const ts=bars[j].ts; if(ts>=today)continue; if(ts<today-86_400_000)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 dayStart=today, volMap=new Map<number,number>()
  for(let j=N-1;j>=0&&bars[j].ts>=dayStart;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): 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-86_400_000)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)
}

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)}); for await(const line of rl){ if(!line.trim())continue; const b=JSON.parse(line) as any; if(!seen.has(b.ts)){ seen.add(b.ts); all.push({ts:b.ts,o:b.o,h:b.h,l:b.l,c:b.c,v:b.v??b.usd??0}) } } }
  all.sort((a,b)=>a.ts-b.ts); return all
}

async function loadFlowSession(dir: string): Promise<{ label:string; bars: FlowBar[]; startTs:number; endTs:number }> {
  const map=new Map<number,FlowBar>()
  const file=`${dir}/trades.jsonl`
  const rl=readline.createInterface({input:fs.createReadStream(file)})
  for await(const line of rl){
    if(!line.trim())continue
    const row=JSON.parse(line)
    const ts=Number(row.ts ?? row.data?.timestamp)
    const d=row.data ?? row
    const price=Number(d.price), notional=Number(d.notionalUsd ?? (d.size && d.price ? d.size*d.price : 0))
    const side=String(d.side).toLowerCase()==='buy'?'buy':'sell'
    if(!Number.isFinite(ts)||!Number.isFinite(price)||!Number.isFinite(notional)||notional<=0)continue
    const key=Math.floor(ts/10_000)*10_000
    let b=map.get(key)
    if(!b){ b={ts:key,o:price,h:price,l:price,c:price,buyVol:0,sellVol:0,n:0}; map.set(key,b) }
    b.h=Math.max(b.h,price); b.l=Math.min(b.l,price); b.c=price; b.n++
    if(side==='buy')b.buyVol+=notional; else b.sellVol+=notional
  }
  const arr=[...map.values()].sort((a,b)=>a.ts-b.ts)
  return { label: dir.split('/').pop() ?? dir, bars: arr, startTs: arr[0]?.ts ?? 0, endTs: (arr.at(-1)?.ts ?? 0)+10_000 }
}

function flowSignal(flow: FlowBar[], idx: number): FlowSig | null {
  const fastN=5, slowN=60
  if(idx<Math.max(fastN,10)-1)return null
  const fast=flow.slice(Math.max(0,idx-fastN+1),idx+1)
  const slow=flow.slice(Math.max(0,idx-slowN+1),idx+1)
  if(fast.length<3||slow.length<10)return null
  const fBuy=fast.reduce((s,b)=>s+b.buyVol,0), fSell=fast.reduce((s,b)=>s+b.sellVol,0), fTot=fBuy+fSell
  const delta=fTot>0?(fBuy-fSell)/fTot:0
  const move=fast[0].o>0?(fast[fast.length-1].c-fast[0].o)/fast[0].o*10000:0
  const prev=flow.slice(Math.max(0,idx-20),idx)
  const avgVol=prev.reduce((s,b)=>s+b.buyVol+b.sellVol,0)/Math.max(1,prev.length)
  const surge=avgVol>0?(fTot/fastN)/avgVol:1
  const deltaScore=Math.max(-1,Math.min(1,delta/0.35))
  const priceScore=Math.max(-1,Math.min(1,move/12))
  const score=deltaScore*0.45+priceScore*0.35
  return {score,delta,moveBps:move,fastVol:fTot,surge}
}

function shouldFlowExit(cfg: ExitCfg, dir: 1|-1, sig: FlowSig | null): boolean {
  if(cfg.mode==='none'||!sig)return false
  const supportScore=dir*sig.score
  const supportDelta=dir*sig.delta
  const supportMove=dir*sig.moveBps
  if(cfg.mode==='contraScore') return supportScore <= -cfg.threshold
  if(cfg.mode==='contraDelta') return supportDelta <= -cfg.threshold
  if(cfg.mode==='lostScore') return supportScore < cfg.threshold
  if(cfg.mode==='deltaAndMove') return supportDelta <= -cfg.threshold && supportMove < 0
  return false
}

function simulateSession(baseBars: Bar[], startIdx: number, endIdx: number, flow: FlowBar[], session: string, cfg: ExitCfg): Trade[] {
  const trades: Trade[]=[]
  const stopCounts=new Map<string,number>()
  let cooldown=0
  let flowIdx=0
  const flowAt=(ts:number): FlowSig|null => {
    while(flowIdx<flow.length-1 && flow[flowIdx+1].ts<=ts)flowIdx++
    if(!flow[flowIdx] || flow[flowIdx].ts < ts-90_000)return null
    return flowSignal(flow,flowIdx)
  }

  for(let i=Math.max(startIdx,20160); i<endIdx-WAIT; i++){
    if(i<cooldown)continue
    const {score,hold}=computeScore(baseBars,i)
    if(score===0)continue
    const dir=(score>0?1:-1) as 1|-1
    const side=dir>0?'long':'short'
    const thresh=dir>0?LONG_ENTRY_THRESH:SHORT_ENTRY_THRESH
    if(Math.abs(score)<thresh)continue
    let confirmed=true
    for(let k=1;k<CONFIRM;k++){ const sk=computeScore(baseBars,i-k).score; if(sk*dir<thresh){confirmed=false;break} }
    if(!confirmed)continue
    if(dir*lbRet(baseBars,i,4320)<-600)continue
    if(dir*lbRet(baseBars,i,10080)<-700)continue
    if(dir*lbRet(baseBars,i,20160)<-800)continue
    const day=new Date(baseBars[i].ts).toISOString().slice(0,10), ck=`${day}_${side}`
    if((stopCounts.get(ck)??0)>=STOP_CAP_PER_DAY)continue

    const target=compositeLiquidityLevel(baseBars,i,dir)
    const distBps=Math.abs(baseBars[i].c-target)/baseBars[i].c*10000
    let entryPx=baseBars[i].c, entryBar=i, filled=distBps<=IMMEDIATE_DIST_BPS
    if(!filled){ for(let j=i+1;j<=Math.min(endIdx-1,i+WAIT);j++){ const hit=dir>0?baseBars[j].l<=target:baseBars[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(baseBars,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*(1-0.01):target*(1+0.01))
    const l3ref=dir>0?slPx-0.01:slPx+0.01
    const slL3=stopLiquidityLevel(baseBars,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 hardCap=i+CAP
    let deadline=i+Math.max(MIN_H,Math.min(MAX_H,hold))
    let exitBar=-1, reason='time', gross=0, adverseFlowCount=0
    for(let j=entryBar+1;j<Math.min(endIdx,hardCap+1);j++){
      if(dir>0?baseBars[j].c<=slPx:baseBars[j].c>=slPx){
        const hard=dir>0?baseBars[j].l<=hardSlPx:baseBars[j].h>=hardSlPx
        exitBar=j; reason=hard?'hard_sl':'soft_sl'; gross=hard?-hardSlBps:-softSlBps; break
      }

      if(cfg.mode!=='none' && j>=entryBar+cfg.minHoldBars){
        const fsig=flowAt(baseBars[j].ts)
        if(shouldFlowExit(cfg,dir,fsig)) adverseFlowCount++
        else adverseFlowCount=0
        if(adverseFlowCount>=cfg.confirmBars){
          exitBar=j; reason='flow_exit'; gross=dir*(baseBars[j].c-entryPx)/entryPx*10000; break
        }
      }

      if(j>=i+MIN_H){
        const {score:es,hold:eh}=computeScore(baseBars,j)
        if(es*dir>=EXT){ const proposed=Math.min(j+Math.max(MIN_H,Math.min(MAX_H,eh)),hardCap); if(proposed>deadline)deadline=proposed }
      }
      if(j>=deadline){ exitBar=j; reason='time'; gross=dir*(baseBars[j].c-entryPx)/entryPx*10000; break }
    }
    if(exitBar<0){ exitBar=Math.min(endIdx-2,hardCap); gross=dir*(baseBars[exitBar].c-entryPx)/entryPx*10000; reason='eod' }

    trades.push({side,net:gross,gross,reason,hold:exitBar-entryBar,entryTs:baseBars[i].ts,exitTs:baseBars[exitBar].ts,sizingBps,session})
    if(reason==='soft_sl'||reason==='hard_sl'){ stopCounts.set(ck,(stopCounts.get(ck)??0)+1) }
    cooldown=exitBar + ((reason==='soft_sl'||reason==='hard_sl')?SL_COOLDOWN_BARS:(reason==='flow_exit'?(cfg.flowCooldownBars??5):5))
  }
  return trades
}

function actualEval(trades: Trade[], feeRtBps: number) {
  let eq=500, pk=500, dd=0
  const daily=new Map<string,number>()
  const vals:number[]=[]
  for(const t of trades){
    const net=t.net-feeRtBps
    vals.push(net)
    const pnl=eq*0.03/(t.sizingBps/10000)*net/10000
    eq+=pnl
    if(eq>pk)pk=eq
    dd=Math.max(dd,(pk-eq)/pk)
    const d=new Date(t.entryTs).toISOString().slice(0,10)
    daily.set(d,(daily.get(d)??0)+pnl)
  }
  const ds=[...daily.values()]
  const dMean=mean(ds), dSd=sd(ds)
  return {eq,ret:eq-500,maxDD:dd,dailyIR:dSd>0?dMean/dSd:0, metric:stats(vals)}
}

function report(label: string, trades: Trade[], ref: Trade[] | null, feeRtBps: number) {
  const ev=actualEval(trades,feeRtBps), s=ev.metric
  const reasons=[...trades.reduce((m,t)=>m.set(t.reason,(m.get(t.reason)??0)+1),new Map<string,number>()).entries()].map(([k,v])=>`${k}:${v}`).join(' ')
  const avgHold=mean(trades.map(t=>t.hold))
  const delta=ref?ev.ret-actualEval(ref,feeRtBps).ret:0
  console.log(`${label.padEnd(24)} n=${String(s.n).padStart(3)} mean=${s.mean.toFixed(2).padStart(7)} t=${s.t.toFixed(2).padStart(5)}${sig(s.t).padEnd(4)} WR=${(s.wr*100).toFixed(1).padStart(5)}% dayIR=${ev.dailyIR.toFixed(2).padStart(5)} maxDD=${(ev.maxDD*100).toFixed(1).padStart(5)}% ret=$${ev.ret.toFixed(2).padStart(8)} Δ=$${delta.toFixed(2).padStart(8)} hold=${avgHold.toFixed(0).padStart(4)} reasons=${reasons}`)
}

async function main() {
  console.log('Loading 1m bars...')
  const bars=await loadBars(['data/klines/BTCUSDT-1m-2022-2025.jsonl','data/klines/BTCUSDT-1m-2022-2025b.jsonl','data/klines/BTCUSDT-1m.jsonl'])
  const tsToIdx=new Map<number,number>(); bars.forEach((b,i)=>tsToIdx.set(b.ts,i))
  console.log(`${bars.length.toLocaleString()} bars loaded`)

  const dirs=fs.readdirSync('data/historical').map(d=>`data/historical/${d}`).filter(d=>fs.existsSync(`${d}/trades.jsonl`)).sort()
  const cfgs: ExitCfg[] = [
    {label:'baseline',mode:'none',minHoldBars:0,confirmBars:1,threshold:0},
    {label:'score<-0.25 m15 c1',mode:'contraScore',minHoldBars:15,confirmBars:1,threshold:0.25},
    {label:'score<-0.35 m15 c1',mode:'contraScore',minHoldBars:15,confirmBars:1,threshold:0.35},
    {label:'score<-0.25 m30 c2',mode:'contraScore',minHoldBars:30,confirmBars:2,threshold:0.25},
    {label:'delta<-0.20 m15 c1',mode:'contraDelta',minHoldBars:15,confirmBars:1,threshold:0.20},
    {label:'delta<-0.30 m15 c1',mode:'contraDelta',minHoldBars:15,confirmBars:1,threshold:0.30},
    {label:'delta<-0.20 m30 c2',mode:'contraDelta',minHoldBars:30,confirmBars:2,threshold:0.20},
    {label:'lost score<0 m30 c3',mode:'lostScore',minHoldBars:30,confirmBars:3,threshold:0},
    {label:'delta+move<-0.2 m15',mode:'deltaAndMove',minHoldBars:15,confirmBars:1,threshold:0.20},
    {label:'delta<-0.20 m15 cd60',mode:'contraDelta',minHoldBars:15,confirmBars:1,threshold:0.20,flowCooldownBars:60},
    {label:'delta<-0.20 m30 c2 cd60',mode:'contraDelta',minHoldBars:30,confirmBars:2,threshold:0.20,flowCooldownBars:60},
    {label:'score<-0.25 m30 c2 cd60',mode:'contraScore',minHoldBars:30,confirmBars:2,threshold:0.25,flowCooldownBars:60},
    {label:'delta<-0.20 m60 c2 cd60',mode:'contraDelta',minHoldBars:60,confirmBars:2,threshold:0.20,flowCooldownBars:60},
    {label:'delta<-0.20 m60 c3 cd120',mode:'contraDelta',minHoldBars:60,confirmBars:3,threshold:0.20,flowCooldownBars:120},
  ]
  const all: Record<string, Trade[]> = Object.fromEntries(cfgs.map(c=>[c.label,[]]))

  for(const dir of dirs){
    process.stderr.write(`Loading flow ${dir}...`)
    const sess=await loadFlowSession(dir)
    process.stderr.write(` 10s=${sess.bars.length}\n`)
    if(sess.bars.length<120)continue
    const startMinute=Math.floor(sess.startTs/60_000)*60_000
    const endMinute=Math.floor(sess.endTs/60_000)*60_000
    const startIdx=tsToIdx.get(startMinute) ?? bars.findIndex(b=>b.ts>=startMinute)
    let endIdx=tsToIdx.get(endMinute) ?? bars.findIndex(b=>b.ts>=endMinute)
    if(endIdx<0)endIdx=bars.length-1
    if(startIdx<0||endIdx<=startIdx+120)continue
    for(const cfg of cfgs){
      const tr=simulateSession(bars,startIdx,endIdx,sess.bars,sess.label,cfg)
      all[cfg.label].push(...tr)
    }
  }

  for(const fee of [0,4]){
    console.log(`\n================ ORDERFLOW EXIT VALIDATION feeRT=${fee} ================`)
    const ref=all['baseline']
    for(const cfg of cfgs) report(cfg.label,all[cfg.label],cfg.label==='baseline'?null:ref,fee)
  }

  console.log('\nSessions:', dirs.map(d=>d.split('/').pop()).join(', '))
  console.log('Note: this is limited to historical trade-tape days only; full 4yr validation is impossible without side-stamped trade data.')
}

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