/**
 * MEXC backtest — exhaustive parameter sweep.
 * Uses Bybit 1-min klines (price identical to MEXC within 0.05 bps).
 * Fee: MEXC maker 0 bps, taker 4 bps. We target maker-only execution.
 *
 * Sweeps: score threshold, hold time, lookback, risk %, and tests
 * each signal individually vs combined.
 */
import fs from 'node:fs'
import readline from 'node:readline'

interface K { ts:number; o:number; h:number; l:number; c:number; v:number; usd:number }

async function load(): Promise<K[]> {
  const bars: K[] = []
  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 tt(v: number[]): { mean:number; t:number; n:number; std:number } {
  const n=v.length; if(n<5) return {mean:0,t:0,n,std:0}
  const m=v.reduce((a,b)=>a+b,0)/n
  const var_=v.reduce((s,x)=>s+(x-m)**2,0)/(n-1)
  const se=Math.sqrt(var_/n)
  return {mean:m,t:se>0?m/se:0,n,std:Math.sqrt(var_)}
}
function sig(t:number):string{const a=Math.abs(t);return a>3.89?'****':a>3.29?'***':a>2.58?'**':a>1.96?'*':''}
function pct(a:number[],p:number){const s=[...a].sort((x,y)=>x-y);return s[Math.min(s.length-1,Math.floor(s.length*p))]??0}
function money(n:number):string{if(n>=1e6)return'$'+(n/1e6).toFixed(2)+'M';if(n>=1e3)return'$'+(n/1e3).toFixed(1)+'k';return'$'+n.toFixed(0)}

// ── Signals ──
function lbRet(b:K[],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:K){return b.h>b.l?(b.c-b.l)/(b.h-b.l):0.5}
function avgCP(b:K[],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:K[],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:K[],i:number):{score:number;hold: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)
  if(i>=60){const bp=avgCP(bars,i,60);if(bp>0.58)add(1,15.49,240);if(bp<0.42)add(-1,15.49,240)}
  // OVERNIGHT_REV removed (negative edge)
  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)}
  if(i>=60){const r=rsi(bars,i,60);if(r<30)add(1,4.63,120);if(r>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,hold:tW>0?Math.max(60,Math.min(240,Math.round(hW/tW))):120}
}

interface Trade { gross:number; net:number; month:string; holdBars:number }

function simulate(bars:K[],minScore:number,feeBps:number,minHold:number,maxHold:number,stopBps:number):Trade[]{
  const trades:Trade[]=[];let cd=0
  for(let i=1440;i<bars.length;i++){
    if(i<cd)continue
    const{score,hold:sigHold}=getScore(bars,i)
    if(Math.abs(score)<minScore)continue
    const hold=Math.max(minHold,Math.min(maxHold,sigHold))
    if(i+hold>=bars.length)continue
    const dir=score>0?1:-1, entry=bars[i].c
    // Walk with stop
    let exitBar=i+hold, gross=0, stopped=false
    for(let j=i+1;j<=i+hold&&j<bars.length;j++){
      const pnl=dir*(bars[j].c-entry)/entry*10000
      if(stopBps>0&&pnl<=-stopBps){gross=-stopBps;exitBar=j;stopped=true;break}
    }
    if(!stopped){if(exitBar>=bars.length)exitBar=bars.length-1;gross=dir*(bars[exitBar].c-entry)/entry*10000}
    const net=gross-2*feeBps
    trades.push({gross,net,month:new Date(bars[i].ts).toISOString().slice(0,7),holdBars:exitBar-i})
    cd=exitBar+5
  }
  return trades
}

function equityCurve(trades:Trade[],startEq:number,riskPct:number):{final:number;maxDdPct:number}{
  let eq=startEq,peak=eq,maxDd=0
  for(const t of trades){
    const notional=Math.min(eq*riskPct/0.01,eq*100)
    eq=Math.max(0,eq+notional*(t.net/10000))
    if(eq>peak)peak=eq
    const dd=peak>0?(peak-eq)/peak:0
    if(dd>maxDd)maxDd=dd
  }
  return{final:eq,maxDdPct:maxDd}
}

function report(name:string,trades:Trade[],detail=false){
  if(trades.length<10){console.log(`  ${name.padEnd(55)} n=${trades.length} (too few)`);return}
  const nets=trades.map(t=>t.net),t=tt(nets)
  const wr=nets.filter(n=>n>0).length/nets.length
  let cum=0,maxDd=0,peak=0
  for(const r of nets){cum+=r;if(cum>peak)peak=cum;maxDd=Math.max(maxDd,peak-cum)}
  const stops=trades.filter(t=>t.gross===-100).length
  const avgHold=trades.reduce((s,t)=>s+t.holdBars,0)/trades.length
  console.log(`  ${name.padEnd(55)} n=${String(t.n).padStart(5)} WR=${(wr*100).toFixed(0).padStart(2)}% mean=${t.mean.toFixed(2).padStart(6)}bps t=${t.t.toFixed(2).padStart(5)} ${sig(t.t).padEnd(4)} cum=${cum.toFixed(0).padStart(6)} hold=${(avgHold/6).toFixed(0).padStart(3)}h SL=${stops}`)
  if(detail){
    const byM=new Map<string,number[]>()
    trades.forEach(t=>{if(!byM.has(t.month))byM.set(t.month,[]);byM.get(t.month)!.push(t.net)})
    let pos=0
    for(const[m,ns]of [...byM.entries()].sort()){
      const mt=tt(ns);if(mt.mean>0)pos++
      console.log(`    ${m} n=${String(ns.length).padStart(4)} WR=${(ns.filter(n=>n>0).length/ns.length*100).toFixed(0).padStart(2)}% mean=${mt.mean.toFixed(2).padStart(7)} t=${mt.t.toFixed(2).padStart(6)} ${sig(mt.t)}`)
    }
    console.log(`    Positive months: ${pos}/${byM.size}`)
  }
}

async function main(){
  process.stderr.write('Loading...')
  const bars=await load()
  process.stderr.write(` ${bars.length}\n\n`)

  const MEXC_FEE=0  // 0 bps maker

  console.log('═'.repeat(100))
  console.log('MEXC BACKTEST — EXHAUSTIVE PARAMETER SWEEP')
  console.log('Data: 525,075 Bybit 1-min klines (BTC price identical to MEXC within 0.05 bps)')
  console.log('Fee: MEXC maker 0 bps (maker-only execution)')
  console.log('═'.repeat(100))
  console.log()

  // ── 1. Score threshold sweep ──
  console.log('1. SCORE THRESHOLD (hold=60-240m, stop=100bps)')
  for(const ms of [0,5,10,15,20,25,30,40,50]){
    report(`score>=${ms}`,simulate(bars,ms,MEXC_FEE,60,240,100))
  }

  // ── 2. Hold time sweep ──
  console.log('\n2. HOLD TIME (score>=10, stop=100bps)')
  for(const[minH,maxH]of [[30,60],[30,120],[60,120],[60,180],[60,240],[60,360],[60,480],[120,240],[120,480],[180,360]] as [number,number][]){
    report(`hold=${minH}-${maxH}m`,simulate(bars,10,MEXC_FEE,minH,maxH,100))
  }

  // ── 3. Stop loss sweep ──
  console.log('\n3. STOP LOSS (score>=10, hold=60-240m)')
  for(const stop of [0,30,50,75,100,150,200,300]){
    report(`stop=${stop}bps`,simulate(bars,10,MEXC_FEE,60,240,stop))
  }

  // ── 4. BUYP threshold sweep ──
  console.log('\n4. BUY PRESSURE THRESHOLD (only BUYP signal)')
  for(const thresh of [0.52,0.55,0.58,0.60,0.62,0.65]){
    const trades:Trade[]=[];let cd=0
    for(let i=1500;i<bars.length;i++){
      if(i<cd)continue
      const bp=avgCP(bars,i,60)
      if(Math.abs(bp-0.5)<(thresh-0.5))continue
      const dir=bp>thresh?1:bp<(1-thresh)?-1:0
      if(!dir)continue
      const hold=240
      if(i+hold>=bars.length)continue
      const gross=dir*(bars[i+hold].c-bars[i].c)/bars[i].c*10000
      trades.push({gross,net:gross,month:new Date(bars[i].ts).toISOString().slice(0,7),holdBars:hold})
      cd=i+hold+5
    }
    report(`BUYP thresh=${thresh}`,trades)
  }

  // ── 5. RSI period sweep ──
  console.log('\n5. RSI PERIOD (hold=120m)')
  for(const period of [14,30,45,60,90,120]){
    for(const[lo,hi]of [[25,75],[30,70],[20,80]] as [number,number][]){
      const trades:Trade[]=[];let cd=0
      for(let i=1500;i<bars.length;i++){
        if(i<cd)continue
        const r=rsi(bars,i,period)
        let dir=0
        if(r<lo)dir=1;else if(r>hi)dir=-1;else continue
        const hold=120
        if(i+hold>=bars.length)continue
        const gross=dir*(bars[i+hold].c-bars[i].c)/bars[i].c*10000
        trades.push({gross,net:gross,month:new Date(bars[i].ts).toISOString().slice(0,7),holdBars:hold})
        cd=i+hold+5
      }
      report(`RSI(${period}) <${lo}/>${hi}`,trades)
    }
  }

  // ── 6. DOW individual day performance ──
  console.log('\n6. DAY-OF-WEEK (4h hold each)')
  for(const[dow,name,dir]of [[0,'Sun',1],[1,'Mon',1],[2,'Tue',0],[3,'Wed',1],[4,'Thu',-1],[5,'Fri',-1],[6,'Sat',0]] as [number,string,number][]){
    if(dir===0)continue
    const trades:Trade[]=[];let cd=0
    for(let i=1500;i<bars.length;i++){
      if(i<cd)continue
      if(new Date(bars[i].ts).getUTCDay()!==dow)continue
      const hold=240;if(i+hold>=bars.length)continue
      const gross=dir*(bars[i+hold].c-bars[i].c)/bars[i].c*10000
      trades.push({gross,net:gross,month:new Date(bars[i].ts).toISOString().slice(0,7),holdBars:hold})
      cd=i+hold+5
    }
    report(`${name} ${dir>0?'LONG':'SHORT'}`,trades)
  }

  // ── 7. Hour-of-day sweep ──
  console.log('\n7. HOUR BIAS (1h hold, top hours only)')
  for(const[hour,dir]of [[20,1],[21,1],[22,1],[23,-1],[0,1],[1,1],[13,-1],[14,-1]] as [number,number][]){
    const trades:Trade[]=[];let cd=0
    for(let i=1500;i<bars.length;i++){
      if(i<cd)continue
      if(new Date(bars[i].ts).getUTCHours()!==hour)continue
      const hold=60;if(i+hold>=bars.length)continue
      const gross=dir*(bars[i+hold].c-bars[i].c)/bars[i].c*10000
      trades.push({gross,net:gross,month:new Date(bars[i].ts).toISOString().slice(0,7),holdBars:hold})
      cd=i+hold+5
    }
    report(`H${hour} ${dir>0?'LONG':'SHORT'}`,trades)
  }

  // ── 8. US gap fade parameters ──
  console.log('\n8. US GAP FADE (sweep gap threshold and hold)')
  for(const gapThresh of [3,5,7,10,15]){
    for(const hold of [60,120,180,240]){
      const trades:Trade[]=[];let cd=0
      for(let i=1500;i<bars.length;i++){
        if(i<cd)continue
        const d=new Date(bars[i].ts),h=d.getUTCHours(),m=d.getUTCMinutes()
        if(!((h===13||(h===14&&m<=15))&&m<=15))continue
        if(i<60)continue
        const gap=lbRet(bars,i,60)
        if(Math.abs(gap)<gapThresh)continue
        const dir=gap>0?-1:1
        if(i+hold>=bars.length)continue
        const gross=dir*(bars[i+hold].c-bars[i].c)/bars[i].c*10000
        trades.push({gross,net:gross,month:new Date(bars[i].ts).toISOString().slice(0,7),holdBars:hold})
        cd=i+hold+5
      }
      report(`USgap>${gapThresh} hold=${hold}m`,trades)
    }
  }

  // ── 9. 24h reversion sweep ──
  console.log('\n9. 24h REVERSION (sweep threshold and hold)')
  for(const thresh of [20,30,40,50,75]){
    for(const hold of [120,240,360,480]){
      const trades:Trade[]=[];let cd=0
      for(let i=1500;i<bars.length;i++){
        if(i<cd||i<1440)continue
        const dayRet=lbRet(bars,i,1440)
        if(Math.abs(dayRet)<thresh)continue
        const dir=dayRet>0?-1:1
        if(i+hold>=bars.length)continue
        const gross=dir*(bars[i+hold].c-bars[i].c)/bars[i].c*10000
        trades.push({gross,net:gross,month:new Date(bars[i].ts).toISOString().slice(0,7),holdBars:hold})
        cd=i+hold+5
      }
      report(`REV24h>${thresh} hold=${hold/60}h`,trades)
    }
  }

  // ── 10. Best combined configs — full detail ──
  console.log('\n' + '═'.repeat(100))
  console.log('10. BEST CONFIGS — MONTHLY DETAIL + EQUITY')
  console.log('═'.repeat(100))

  const configs:[string,number,number,number,number][]=[
    ['Combined s>=10 h60-240 sl100', 10,60,240,100],
    ['Combined s>=15 h60-240 sl100', 15,60,240,100],
    ['Combined s>=20 h60-240 sl100', 20,60,240,100],
    ['Combined s>=10 h60-240 noSL', 10,60,240,0],
    ['Combined s>=10 h60-360 sl100', 10,60,360,100],
    ['Combined s>=10 h120-240 sl100',10,120,240,100],
  ]

  for(const[name,ms,minH,maxH,stop]of configs){
    const trades=simulate(bars,ms,MEXC_FEE,minH,maxH,stop)
    report(name,trades,true)

    // Equity curves
    for(const riskPct of [0.02,0.05,0.10]){
      const{final,maxDdPct}=equityCurve(trades,500,riskPct)
      const ret=((final-500)/500*100)
      console.log(`    risk=${(riskPct*100).toFixed(0)}%: $500→${money(final)} (${ret>=0?'+':''}${ret.toFixed(0)}%) maxDD=${(maxDdPct*100).toFixed(0)}%`)
    }
    console.log()
  }

  // ── 11. Walk-forward on best config ──
  console.log('═'.repeat(100))
  console.log('11. WALK-FORWARD (train first 6 months, test last 6)')
  console.log('═'.repeat(100))
  console.log()
  const mid=Math.floor(bars.length/2)
  const train=bars.slice(0,mid+1440),test=bars.slice(mid)
  for(const[name,ms,minH,maxH,stop]of configs){
    const trainTr=simulate(train,ms,MEXC_FEE,minH,maxH,stop)
    const testTr=simulate(test,ms,MEXC_FEE,minH,maxH,stop)
    const trainT=tt(trainTr.map(t=>t.net)),testT=tt(testTr.map(t=>t.net))
    const oos=testT.mean>0&&testT.t>1.5?'✓ OOS':'~'
    console.log(`  ${name.padEnd(38)} TRAIN: mean=${trainT.mean.toFixed(2).padStart(6)} t=${trainT.t.toFixed(2).padStart(5)} n=${trainTr.length}  |  TEST: mean=${testT.mean.toFixed(2).padStart(6)} t=${testT.t.toFixed(2).padStart(5)} n=${testTr.length} ${sig(testT.t)} ${oos}`)
  }

  // ── 12. Monte Carlo on best ──
  console.log('\n' + '═'.repeat(100))
  console.log('12. MONTE CARLO PROJECTION — BEST CONFIG, MEXC 0% MAKER')
  console.log('═'.repeat(100))
  console.log()

  const bestTrades=simulate(bars,10,MEXC_FEE,60,240,100)
  const grossPool=bestTrades.map(t=>t.gross)
  const tpd=grossPool.length/365
  const DAYS=[30,60,120,240]
  const cpT=DAYS.map(d=>Math.round(d*tpd))
  const ITERS=10000
  const snaps:number[][]=DAYS.map(()=>[])
  const dds:number[]=[];let ruins=0

  process.stderr.write('MC 10k paths...')
  for(let it=0;it<ITERS;it++){
    if(it%2000===0)process.stderr.write(` ${it}`)
    let eq=500,peak=500,maxDd=0
    for(let t=0;t<cpT[cpT.length-1];t++){
      const g=grossPool[Math.floor(Math.random()*grossPool.length)]
      const notional=Math.min(eq*0.05/0.01,eq*100)
      eq=Math.max(0,eq+notional*(g/10000))  // 0 fee
      if(eq>peak)peak=eq;const dd=peak>0?(peak-eq)/peak:0;if(dd>maxDd)maxDd=dd
      for(let ci=0;ci<cpT.length;ci++)if(t+1===cpT[ci])snaps[ci].push(eq)
    }
    dds.push(maxDd);if(eq<100)ruins++
  }
  process.stderr.write(' done\n\n')

  console.log(`  Trades/year: ${grossPool.length}  Trades/day: ${tpd.toFixed(1)}  Risk: 5%  Start: $500  Fee: 0bps`)
  console.log()
  for(let di=0;di<DAYS.length;di++){
    const s=snaps[di]
    console.log(`  Day ${String(DAYS[di]).padStart(3)}:  p10=${money(pct(s,0.10)).padStart(9)}  p25=${money(pct(s,0.25)).padStart(9)}  median=${money(pct(s,0.50)).padStart(9)}  p75=${money(pct(s,0.75)).padStart(9)}  p90=${money(pct(s,0.90)).padStart(9)}`)
  }
  console.log()
  console.log(`  Max DD p50=${(pct(dds,0.50)*100).toFixed(0)}%  p90=${(pct(dds,0.90)*100).toFixed(0)}%  p99=${(pct(dds,0.99)*100).toFixed(0)}%`)
  console.log(`  Ruin (<$100): ${(ruins/ITERS*100).toFixed(2)}%`)
  console.log(`  Prob losing money after 240d: ${(snaps[3].filter(e=>e<500).length/snaps[3].length*100).toFixed(1)}%`)
}

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