/**
 * preemptive-entry-validation.ts
 *
 * Tests whether we can improve entries by placing maker orders BEFORE a signal
 * becomes active when the future signal is known from deterministic calendar/time
 * components (DOW + hour). No future price-derived inputs are used.
 *
 * Validity rules:
 *   - Only deterministic DOW/hour score may create a preempt trigger.
 *   - BUYP, RSI, USgap, 24h reversion are NOT projected.
 *   - The future known-only signal must be 4-bar confirmed at activation time.
 *   - If filled before activation, SL is live immediately.
 *   - At activation, the full actual score must confirm in the same direction;
 *     otherwise an early fill is exited immediately as a false-start.
 *   - If not filled by activation and actual confirmation fails, pending order is cancelled.
 *   - Pending maker order is rested at a single L1; no cancel/recreate at same price.
 *
 * This is a conservative/live-oriented model of "we know what calendar signal is
 * coming, so can we rest a better maker entry ahead of it?"
 */

import * as fs from 'fs'
import * as readline from 'readline'

interface Bar { ts: number; o: number; h: number; l: number; c: number; v: number }
interface ScoreResult { score: number; hold: number }
interface Trade {
  net: number; gross: number; isSl: boolean; isHardSl: boolean; hold: number
  month: string; year: number; bar: number; sizingBps: number; softSlBps: number
  preempt: boolean; falseStart: boolean; preStop: boolean; activationBar: number
}
type TradeSet = Trade[] & { meta?: Record<string, number> }
interface Pending {
  dir: 1 | -1; side: 'long' | 'short'; target: number; sizeBtc: number
  activationBar: number; expiresBar: number; holdBars: number
  softSlPx: number; hardSlPx: number; softSlBps: number; hardSlBps: number; sizingBps: number
  startedBar: number
}
interface Position {
  dir: 1 | -1; side: 'long' | 'short'; entryPx: number; entryBar: number
  activationBar: number; holdBars: number; preempt: boolean
  softSlPx: number; hardSlPx: number; softSlBps: number; hardSlBps: number; sizingBps: number
  activated: boolean; deadline: number; hardCap: number; peakFavBps: number
}
interface SimOpts {
  feeBpsRT: number
  preemptLeadBars: number
  preemptMarginBps: number
  allowBaselineFallback: boolean
  makerSlMode: 'simple' | 'maker'
  makerSlTimeoutBars: number
  timeoutExtraBps: 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 r = readline.createInterface({ input: fs.createReadStream(f) })
    for await (const line of r) {
      if (!line.trim()) continue
      const b = JSON.parse(line) as Bar
      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): 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 dd=bars[j].c-bars[j-1].c; if(dd>0)u+=dd; else d-=dd} return d===0?100:100-100/(1+u/d) }

function addKnown(d: Date): Array<[number, number, number]> {
  const h=d.getUTCHours(), dow=d.getUTCDay()
  const out: Array<[number, number, number]> = []
  if (dow===4) out.push([-1,20.00,240])
  if (dow===3) out.push([+1,19.05,240])
  if (dow===0) out.push([+1,15.05,240])
  if (dow===1) out.push([+1,10.61,240])
  if (dow===5) out.push([-1, 6.00,240])
  if (h===22)  out.push([+1, 2.86,120])
  if (h===21)  out.push([+1,17.90,120])
  if (h===20)  out.push([+1, 9.49, 60])
  if (h===23)  out.push([-1, 8.94, 30])
  return out
}
function knownScoreAt(ts: number): ScoreResult {
  let w=0,hW=0,tW=0
  for (const [dir, wt, hold] of addKnown(new Date(ts))) { w += dir*wt; hW += hold*wt; tW += wt }
  return { score: w, hold: tW>0 ? Math.max(60, Math.min(240, Math.round(hW/tW))) : 120 }
}
function computeScore(bars: Bar[], i: number): ScoreResult {
  const d = new Date(bars[i].ts); const h=d.getUTCHours(), m=d.getUTCMinutes()
  let w=0,hW=0,tW=0
  const add=(dir:number,wt:number,hold:number):void=>{w+=dir*wt;hW+=hold*wt;tW+=wt}
  for (const [dir, wt, hold] of addKnown(d)) add(dir, wt, hold)
  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: number, 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 isSwing=true
    for(let k=1;k<=SWING_N&&isSwing;k++){
      if(dir>0){ if((bars[j-k]?.l??Infinity)<=bars[j].l)isSwing=false; if((bars[j+k]?.l??Infinity)<=bars[j].l)isSwing=false }
      else { if((bars[j-k]?.h??0)>=bars[j].h)isSwing=false; if((bars[j+k]?.h??0)>=bars[j].h)isSwing=false }
    }
    if(!isSwing)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-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 d2=new Date(bars[N-1].ts); d2.setUTCHours(0,0,0,0); const dayStart=d2.getTime(); const 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: number, refPrice: number): number {
  const N=i+1, price=refPrice, c:number[]=[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 fullConfirmed(bars: Bar[], i: number, dir: number): boolean {
  const thresh=dir>0?10:8
  for(let k=0;k<4;k++){ if(i-k<0)return false; const s=computeScore(bars,i-k).score; if(s*dir<thresh)return false }
  return true
}
function knownConfirmedAt(bars: Bar[], i: number, dir: number, margin: number): boolean {
  const thresh=(dir>0?10:8)+margin
  for(let k=0;k<4;k++){ if(i-k<0)return false; const s=knownScoreAt(bars[i-k].ts).score; if(s*dir<thresh)return false }
  return true
}
function findKnownActivation(bars: Bar[], i: number, lead: number, margin: number): { bar: number; dir: 1|-1; hold: number } | null {
  const end=Math.min(bars.length-1,i+lead)
  for(let a=i+1;a<=end;a++){
    const ks=knownScoreAt(bars[a].ts); if(ks.score===0)continue
    const dir=(ks.score>0?1:-1) as 1|-1
    if(!knownConfirmedAt(bars,a,dir,margin))continue
    // Transition: previous confirmed bar in same dir was false, so this is first actionable confirmed known regime.
    if(a>0 && knownConfirmedAt(bars,a-1,dir,margin))continue
    return { bar:a, dir, hold:ks.hold }
  }
  return null
}
function makeLevels(bars: Bar[], i: number, dir: 1|-1): Pending {
  const target=compositeLiquidityLevel(bars,i,dir)
  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 softSlPx=isStruct?slL2:(dir>0?target*0.99:target*1.01)
  const l3ref=dir>0?softSlPx-0.01:softSlPx+0.01
  const slL3=stopLiquidityLevel(bars,i,dir,l3ref)
  const l3Gap=Math.max(10,Math.min(100,Math.abs(softSlPx-slL3)/softSlPx*10000))
  const sizingBps=isStruct?softSlBps+l3Gap:125
  const hardSlBps=isStruct?softSlBps+l3Gap+8:133
  const hardSlPx=isStruct?(dir>0?softSlPx*(1-l3Gap/10000):softSlPx*(1+l3Gap/10000)):(dir>0?target*(1-1.25/100):target*(1+1.25/100))
  return { dir, side:dir>0?'long':'short', target, sizeBtc:0, activationBar:i, expiresBar:i, holdBars:120, softSlPx, hardSlPx, softSlBps, hardSlBps, sizingBps, startedBar:i }
}
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 monthlyPnl(trades:TradeSet,fixedNotional=1500){const out:Record<string,number>={};for(const t of trades)out[t.month]=(out[t.month]??0)+fixedNotional*t.net/10000;return out}
function moIR(trades:TradeSet){const mp=Object.values(monthlyPnl(trades)); if(mp.length<3)return 0; const m=mp.reduce((a,b)=>a+b,0)/mp.length; const sd=Math.sqrt(mp.reduce((s,x)=>s+(x-m)**2,0)/(mp.length-1)); return sd>0?m/sd:0}
function actualSizedMoIR(trades:TradeSet,fixedAccount=10000,risk=0.03){const monthly:Record<string,number>={}; for(const t of trades){const notional=fixedAccount*risk/(t.sizingBps/10000); monthly[t.month]=(monthly[t.month]??0)+notional*t.net/10000} const mp=Object.values(monthly); if(mp.length<3)return 0; const m=mp.reduce((a,b)=>a+b,0)/mp.length; const sd=Math.sqrt(mp.reduce((s,x)=>s+(x-m)**2,0)/(mp.length-1)); return sd>0?m/sd:0}
function maxDD(trades:TradeSet,risk=0.03){let eq=500,pk=500,dd=0; for(const t of trades){eq+=eq*risk/(t.sizingBps/10000)*t.net/10000; if(eq>pk)pk=eq; dd=Math.max(dd,(pk-eq)/pk)} return dd}
function monthsPos(trades:TradeSet):[number,number]{const vals=Object.values(monthlyPnl(trades));return[vals.filter(x=>x>0).length,vals.length]}

function simulateBaseline(bars: Bar[], feeBpsRT: number, makerSlMode: 'simple'|'maker'): TradeSet {
  return simulatePreempt(bars,{feeBpsRT,preemptLeadBars:0,preemptMarginBps:999,allowBaselineFallback:true,makerSlMode,makerSlTimeoutBars:30,timeoutExtraBps:3})
}

function closePosition(bars: Bar[], pos: Position, exitBar: number, gross: number, isSl: boolean, isHardSl: boolean, opts: SimOpts, falseStart=false, preStop=false): Trade {
  const extra = 0
  const net = gross - opts.feeBpsRT - extra
  return { net, gross, isSl, isHardSl, hold: exitBar-pos.entryBar, month:new Date(bars[pos.entryBar].ts).toISOString().slice(0,7), year:new Date(bars[pos.entryBar].ts).getUTCFullYear(), bar:pos.entryBar, sizingBps:pos.sizingBps, softSlBps:pos.softSlBps, preempt:pos.preempt, falseStart, preStop, activationBar:pos.activationBar }
}

function simulatePreempt(bars: Bar[], opts: SimOpts): TradeSet {
  const trades=[] as TradeSet
  const meta:Record<string,number>={pendingStarted:0,pendingFilled:0,falseStarts:0,preStops:0,baselineTrades:0,preemptTrades:0}
  let cooldown=0, pending:Pending|null=null, pos:Position|null=null
  const stopCounts=new Map<string,number>()
  const WAIT=20, CAP=360, MIN_H=60, MAX_H=240, EXT=8, SL_COOLDOWN=30

  for(let i=20160;i<bars.length-40;i++){
    // Position management
    if(pos){
      if(!pos.activated && i>=pos.activationBar){
        if(!fullConfirmed(bars,pos.activationBar,pos.dir)){
          const gross=pos.dir*(bars[i].c-pos.entryPx)/pos.entryPx*10000
          trades.push(closePosition(bars,pos,i,gross,false,false,opts,true,false)); meta.falseStarts=(meta.falseStarts??0)+1; pos=null; cooldown=i+5; continue
        }
        pos.activated=true
      }
      // SL
      const slCross=pos.dir>0?bars[i].c<=pos.softSlPx:bars[i].c>=pos.softSlPx
      if(slCross){
        let exitBar=i,isHard=false,gross=-pos.softSlBps, extra=0
        if(opts.makerSlMode==='maker'){
          const makerDeadline=Math.min(bars.length-2,pos.hardCap,i+opts.makerSlTimeoutBars)
          let done=false
          for(let k=i;k<=makerDeadline;k++){
            const hard=pos.dir>0?bars[k].l<=pos.hardSlPx:bars[k].h>=pos.hardSlPx
            if(hard){exitBar=k;isHard=true;gross=-pos.hardSlBps;done=true;break}
            if(k>i){const retouch=pos.dir>0?bars[k].h>=pos.softSlPx:bars[k].l<=pos.softSlPx; if(retouch){exitBar=k;gross=-pos.softSlBps;done=true;break}}
          }
          if(!done){exitBar=makerDeadline; gross=pos.dir*(bars[exitBar].c-pos.entryPx)/pos.entryPx*10000; extra=opts.timeoutExtraBps}
        }else{
          isHard=pos.dir>0?bars[i].l<=pos.hardSlPx:bars[i].h>=pos.hardSlPx
          gross=isHard?-pos.hardSlBps:-pos.softSlBps
        }
        const net=gross-opts.feeBpsRT-extra
        trades.push({net,gross,isSl:true,isHardSl:isHard,hold:exitBar-pos.entryBar,month:new Date(bars[pos.entryBar].ts).toISOString().slice(0,7),year:new Date(bars[pos.entryBar].ts).getUTCFullYear(),bar:pos.entryBar,sizingBps:pos.sizingBps,softSlBps:pos.softSlBps,preempt:pos.preempt,falseStart:false,preStop:!pos.activated,activationBar:pos.activationBar})
        if(pos.preempt)meta.preemptTrades=(meta.preemptTrades??0)+1; else meta.baselineTrades=(meta.baselineTrades??0)+1
        if(!pos.activated)meta.preStops=(meta.preStops??0)+1
        const key=`${new Date(bars[exitBar].ts).toISOString().slice(0,10)}_${pos.side}`; stopCounts.set(key,(stopCounts.get(key)??0)+1)
        pos=null; cooldown=exitBar+SL_COOLDOWN; i=exitBar; continue
      }
      // extension/time exit only after activation
      if(pos.activated){
        if(i>=pos.activationBar+MIN_H){const es=computeScore(bars,i); if(es.score*pos.dir>=EXT){const p=Math.min(i+Math.max(MIN_H,Math.min(MAX_H,es.hold)),pos.hardCap); if(p>pos.deadline)pos.deadline=p}}
        if(i>=pos.deadline||i>=pos.hardCap){const gross=pos.dir*(bars[i].c-pos.entryPx)/pos.entryPx*10000; trades.push(closePosition(bars,pos,i,gross,false,false,opts)); if(pos.preempt)meta.preemptTrades=(meta.preemptTrades??0)+1; else meta.baselineTrades=(meta.baselineTrades??0)+1; pos=null; cooldown=i+5; continue}
      }
      continue
    }

    // Pending preempt management
    if(pending){
      const hit=pending.dir>0?bars[i].l<=pending.target:bars[i].h>=pending.target
      if(hit){
        pos={dir:pending.dir,side:pending.side,entryPx:pending.target,entryBar:i,activationBar:pending.activationBar,holdBars:pending.holdBars,preempt:true,softSlPx:pending.softSlPx,hardSlPx:pending.hardSlPx,softSlBps:pending.softSlBps,hardSlBps:pending.hardSlBps,sizingBps:pending.sizingBps,activated:i>=pending.activationBar,deadline:pending.activationBar+pending.holdBars,hardCap:pending.activationBar+CAP,peakFavBps:0}
        meta.pendingFilled++
        pending=null
        continue
      }
      if(i>=pending.activationBar && !fullConfirmed(bars,pending.activationBar,pending.dir)){pending=null; continue}
      if(i>=pending.expiresBar){pending=null; continue}
    }

    if(i<cooldown || pending)continue

    // Baseline actual signal (priority if already active now)
    const sc=computeScore(bars,i), rawDir=(sc.score>0?1:-1) as 1|-1
    if(sc.score!==0 && fullConfirmed(bars,i,rawDir)){
      if(rawDir*lbRet(bars,i,4320)>=-600 && rawDir*lbRet(bars,i,10080)>=-700 && rawDir*lbRet(bars,i,20160)>=-800){
        const side=rawDir>0?'long':'short'; const day=new Date(bars[i].ts).toISOString().slice(0,10); if((stopCounts.get(`${day}_${side}`)??0)<2){
          const p=makeLevels(bars,i,rawDir); const dist=Math.abs(bars[i].c-p.target)/bars[i].c*10000
          let filled=dist<=10, entryBar=i, entryPx=bars[i].c
          if(!filled){for(let j=i+1;j<=i+WAIT;j++){const h=rawDir>0?bars[j].l<=p.target:bars[j].h>=p.target;if(h){filled=true;entryBar=j;entryPx=p.target;break}}}
          if(filled){pos={dir:rawDir,side:side as 'long'|'short',entryPx,entryBar,activationBar:i,holdBars:sc.hold,preempt:false,softSlPx:p.softSlPx,hardSlPx:p.hardSlPx,softSlBps:p.softSlBps,hardSlBps:p.hardSlBps,sizingBps:p.sizingBps,activated:true,deadline:i+sc.hold,hardCap:i+CAP,peakFavBps:0}; continue}
        }
      }
    }

    // Preempt pending known activation
    if(opts.preemptLeadBars>0){
      const act=findKnownActivation(bars,i,opts.preemptLeadBars,opts.preemptMarginBps)
      if(act){
        const p=makeLevels(bars,i,act.dir)
        p.activationBar=act.bar; p.expiresBar=act.bar+WAIT; p.holdBars=act.hold; p.startedBar=i
        pending=p; meta.pendingStarted++
      }
    }
  }
  trades.meta=meta
  return trades
}

function metrics(label:string,trades:TradeSet){const s=stats(trades.map(t=>t.net));const mp=Object.values(monthlyPnl(trades));const mpos=mp.filter(x=>x>0).length;const pt=trades.filter(t=>t.preempt);const ps=stats(pt.map(t=>t.net));return{label,n:s.n,mean:s.mean,t:s.t,wr:s.wr,moIR:moIR(trades),actIR:actualSizedMoIR(trades),maxDD:maxDD(trades),mp:mpos,mt:mp.length,preN:pt.length,preMean:ps.mean,preT:ps.t,falseStarts:trades.filter(t=>t.falseStart).length,preStops:trades.filter(t=>t.preStop).length,meta:trades.meta}}
function fmt(m:ReturnType<typeof metrics>){return`${m.label.padEnd(32)} n=${String(m.n).padStart(5)} mean=${m.mean.toFixed(2).padStart(6)} t=${m.t.toFixed(2)}${sig(m.t).padEnd(4)} moIR=${m.moIR.toFixed(2)} actIR=${m.actIR.toFixed(2)} maxDD=${(m.maxDD*100).toFixed(1)}% m+=${m.mp}/${m.mt} preN=${String(m.preN).padStart(4)} preMean=${m.preN?m.preMean.toFixed(2):'n/a'} preT=${m.preN?m.preT.toFixed(2)+sig(m.preT):'n/a'} false=${m.falseStarts} preStop=${m.preStops}`}

async function main(){
  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 (${new Date(bars[0].ts).toISOString().slice(0,10)} → ${new Date(bars[bars.length-1].ts).toISOString().slice(0,10)})`)
  for(const feeBpsRT of [4,0]){
    for(const makerSlMode of ['simple','maker'] as const){
      console.log(`\n=== feeRT=${feeBpsRT} ${makerSlMode} SL ===`)
      const base=metrics('BASELINE',simulateBaseline(bars,feeBpsRT,makerSlMode)); console.log(fmt(base))
      for(const lead of [15,30,60,120,240]){
        for(const margin of [0,5,10,20]){
          const tr=simulatePreempt(bars,{feeBpsRT,preemptLeadBars:lead,preemptMarginBps:margin,allowBaselineFallback:true,makerSlMode,makerSlTimeoutBars:30,timeoutExtraBps:3})
          const m=metrics(`lead=${lead} margin=${margin}`,tr)
          console.log(fmt(m))
        }
      }
    }
  }
}
main().catch(e=>{console.error(e);process.exit(1)})
