// @ts-nocheck
/**
 * Formal long-threshold validation.
 *
 * Validates the candidate from exploratory research:
 *   LONG_ENTRY_THRESH = 16, SHORT_ENTRY_THRESH = 8
 *
 * This script keeps the current canonical mechanics and sweeps only the long
 * threshold, then conditionally sweeps hard-cap bars around the best candidate.
 * It reports full-period, yearly, half-split, leave-one-year-out, paired monthly
 * fixed-notional and actual L3-sized comparisons, and simple walk-forward
 * threshold selection.
 */

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
  isSl: boolean
  isHardSl: boolean
}

type SimCfg = {
  label: string
  longT: number
  shortT: number
  hardCapBars?: number
}

async function loadBars(files: string[]): Promise<Bar[]> {
  const seen = new Set<number>()
  const 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)
  candidates.push(dir > 0 ? Math.floor(price / 500) * 500 : Math.ceil(price / 500) * 500)
  candidates.push(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--) {
    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 simulate(bars: Bar[], cfg: SimCfg): Trade[] {
  const trades: Trade[] = [], stopCounts = new Map<string, number>()
  let cooldown = 0
  const EXT = 8, WAIT = 20, MIN_H = 60, MAX_H = 240, CAP = cfg.hardCapBars ?? 360, CONFIRM = 4, SL_CD = 30, N = bars.length
  for (let i = 20160; i < N - WAIT; i++) {
    if (i < cooldown) continue
    const { score, hold } = 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 ? cfg.longT : cfg.shortT
    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 hardCap = i + CAP
    let deadline = i + Math.max(MIN_H, Math.min(MAX_H, hold)), exitBar = -1, isSl = false, isHardSl = false
    for (let j = entryBar + 1; j < Math.min(N, hardCap + 1); j++) {
      if (dir > 0 ? bars[j].c <= slPx : bars[j].c >= slPx) { exitBar = j; isSl = true; isHardSl = dir > 0 ? bars[j].l <= hardSlPx : bars[j].h >= hardSlPx; break }
      if (j >= i + 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; break }
    }
    if (exitBar < 0) exitBar = Math.min(N - 2, hardCap)
    const gross = isSl ? (isHardSl ? -hardSlBps : -softSlBps) : dir * (bars[exitBar].c - entryPx) / entryPx * 10000
    trades.push({ gross, net: gross, isSl, isHardSl, month: new Date(bars[i].ts).toISOString().slice(0, 7), year: new Date(bars[i].ts).getUTCFullYear(), side, bar: i, softSlBps, sizingBps })
    if (isSl) stopCounts.set(ck, (stopCounts.get(ck) ?? 0) + 1)
    cooldown = exitBar + (isSl ? SL_CD : 5)
  }
  return trades
}

function withFee(tr: Trade[], fee: number): Trade[] { return tr.map(t => ({ ...t, net: t.gross - fee })) }
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.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.sizingBps / 10000) * t.net / 10000; if (eq > pk) pk = eq; dd = Math.max(dd, (pk - eq) / pk) } return dd }
function metric(label: string, tr: Trade[]) { const s = stats(tr.map(t => t.net)); const 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 fmt(m: ReturnType<typeof metric>) { return `${m.label.padEnd(10)} 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}` }
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 subset(tr: Trade[], pred: (t: Trade) => boolean) { return tr.filter(pred) }
function yearsOf(tr: Trade[]) { return [...new Set(tr.map(t => t.year))].sort() }
function minLeaveOneOutMoIR(tr: Trade[]) { let mn = Infinity; for (const y of yearsOf(tr)) mn = Math.min(mn, moIR(tr.filter(t => t.year !== y))); return mn }
function halfMetrics(tr: Trade[], bars: Bar[]) { const cut = Date.parse('2024-04-01T00:00:00Z'); return [metric('pre', tr.filter(t => bars[t.bar].ts < cut)), metric('post', tr.filter(t => bars[t.bar].ts >= cut))] }
function yearlyRows(label: string, tr: Trade[]) { console.log(`\n${label}`); for (const y of yearsOf(tr)) console.log(`  ${y} ${fmt(metric('', tr.filter(t => t.year === y)))}`) }

function addMonths(ms: number, n: number) { const d = new Date(ms); d.setUTCMonth(d.getUTCMonth() + n); return d.getTime() }
function monthStr(ms: number) { return new Date(ms).toISOString().slice(0,7) }
function tradeTs(bars: Bar[], t: Trade) { return bars[t.bar].ts }
function inWindow(bars: Bar[], t: Trade, start: number, end: number) { const ts = tradeTs(bars,t); return ts >= start && ts < end }
function rscore(m: ReturnType<typeof metric>) { return m.moIR + 0.25*m.actIR - 0.50*m.maxDD }
function selectByObjective(rows: {thr:number; tr:Trade[]; m:ReturnType<typeof metric>}[], objective: 'moIR'|'actIR'|'score') {
  const viable = rows.filter(r => r.m.n >= 120 && r.m.mt >= 6)
  const pool = viable.length ? viable : rows
  return pool.slice().sort((a,b) => {
    const av = objective === 'score' ? rscore(a.m) : a.m[objective]
    const bv = objective === 'score' ? rscore(b.m) : b.m[objective]
    if (bv !== av) return bv - av
    if (a.m.maxDD !== b.m.maxDD) return a.m.maxDD - b.m.maxDD
    return a.thr - b.thr
  })[0]
}
function mergeTrades(parts: Trade[][]) { return parts.flat().sort((a,b)=>a.bar-b.bar) }
function foldStats(thrs: number[]) { const s=stats(thrs); const counts: Record<string,number>={}; for(const t of thrs) counts[t]=(counts[t]??0)+1; return {mean:s.mean, counts} }
function actualTotal(tr: Trade[]) { return Object.values(actualMonthly(tr)).reduce((a,b)=>a+b,0) }
function wfRun(args: {bars:Bar[]; sims: {thr:number; trades:Trade[]}[]; fee:number; lookbackMonths:number|'expanding'; testMonths:number; objective:'moIR'|'actIR'|'score'; startMonth:string; endMonth:string}) {
  const {bars,sims,fee,lookbackMonths,testMonths,objective}=args
  const start = Date.parse(args.startMonth+'-01T00:00:00Z')
  const end = addMonths(Date.parse(args.endMonth+'-01T00:00:00Z'), 1)
  let testStart = start
  const selectedParts: Trade[][] = [], baseParts: Trade[][] = [], l16Parts: Trade[][] = [], l19Parts: Trade[][] = []
  const folds: any[] = []
  while (addMonths(testStart,testMonths) <= end) {
    const testEnd = addMonths(testStart,testMonths)
    const trainEnd = testStart
    const trainStart = lookbackMonths === 'expanding' ? Date.parse('2022-04-01T00:00:00Z') : addMonths(trainEnd, -lookbackMonths)
    if (trainStart < Date.parse('2022-04-01T00:00:00Z')) { testStart = addMonths(testStart, testMonths); continue }
    const rows = sims.map(s => {
      const tr = withFee(s.trades.filter(t=>inWindow(bars,t,trainStart,trainEnd)), fee)
      return {thr:s.thr, tr, m:metric('',tr)}
    })
    const sel = selectByObjective(rows, objective)
    const selTest = withFee(sims.find(s=>s.thr===sel.thr)!.trades.filter(t=>inWindow(bars,t,testStart,testEnd)), fee)
    const baseTest = withFee(sims.find(s=>s.thr===10)!.trades.filter(t=>inWindow(bars,t,testStart,testEnd)), fee)
    const l16Test = withFee(sims.find(s=>s.thr===16)!.trades.filter(t=>inWindow(bars,t,testStart,testEnd)), fee)
    const l19Test = withFee(sims.find(s=>s.thr===19)!.trades.filter(t=>inWindow(bars,t,testStart,testEnd)), fee)
    selectedParts.push(selTest); baseParts.push(baseTest); l16Parts.push(l16Test); l19Parts.push(l19Test)
    folds.push({test: monthStr(testStart)+'..'+monthStr(addMonths(testEnd,-1)), train: monthStr(trainStart)+'..'+monthStr(addMonths(trainEnd,-1)), thr: sel.thr, trainM: sel.m, selTest: metric('', selTest), baseTest: metric('',baseTest), l16Test: metric('',l16Test), l19Test: metric('',l19Test)})
    testStart = addMonths(testStart, testMonths)
  }
  const selected = mergeTrades(selectedParts), base = mergeTrades(baseParts), l16=mergeTrades(l16Parts), l19=mergeTrades(l19Parts)
  return {folds, selected, base, l16, l19, chosen: foldStats(folds.map(f=>f.thr))}
}
function compareLine(name:string, tr:Trade[], base:Trade[]) {
  const m=metric(name,tr), pf=pairedMaps(monthly(base), monthly(tr)), pa=pairedMaps(actualMonthly(base), actualMonthly(tr))
  return fmt(m)+' fixedDelta=$'+pf.mean.toFixed(1)+'('+pf.imp+'/'+pf.n+',t='+pf.t.toFixed(2)+') actualDelta=$'+pa.mean.toFixed(0)+'('+pa.imp+'/'+pa.n+',t='+pa.t.toFixed(2)+') actualTotal=$'+actualTotal(tr).toFixed(0)
}
function printWfSummary(label:string, res: ReturnType<typeof wfRun>) {
  console.log('\n'+label)
  console.log('folds='+res.folds.length+' chosenMean='+res.chosen.mean.toFixed(1)+' chosenCounts='+JSON.stringify(res.chosen.counts))
  console.log(compareLine('WF-select',res.selected,res.base))
  console.log(compareLine('BASE OOS',res.base,res.base))
  console.log(compareLine('Fixed L16',res.l16,res.base))
  console.log(compareLine('Fixed L19',res.l19,res.base))
  // Per-fold moIR is not meaningful for 1-month folds (single monthly return => 0),
  // so use actual-sized P&L wins as the generic fold-level win counter.
  const wins=res.folds.filter(f=>actualTotal(mergeTrades([res.selected.filter(t=>t.month>=f.test.slice(0,7)&&t.month<=f.test.slice(-7))]))>actualTotal(mergeTrades([res.base.filter(t=>t.month>=f.test.slice(0,7)&&t.month<=f.test.slice(-7))]))).length
  console.log('fold actual-PnL wins WF vs BASE='+wins+'/'+res.folds.length)
  console.log('first folds:')
  for (const f of res.folds.slice(0,4)) console.log('  '+f.test+' train='+f.train+' L'+f.thr+' trainMoIR='+f.trainM.moIR.toFixed(2)+' testMoIR='+f.selTest.moIR.toFixed(2)+' baseMoIR='+f.baseTest.moIR.toFixed(2)+' n='+f.selTest.n)
  console.log('last folds:')
  for (const f of res.folds.slice(-4)) console.log('  '+f.test+' train='+f.train+' L'+f.thr+' trainMoIR='+f.trainM.moIR.toFixed(2)+' testMoIR='+f.selTest.moIR.toFixed(2)+' baseMoIR='+f.baseTest.moIR.toFixed(2)+' n='+f.selTest.n)
}

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)})`)
  const thresholds = Array.from({ length: 13 }, (_, i) => 10 + i)
  const sims = thresholds.map(thr => ({ thr, trades: simulate(bars, { label: `L${thr}/S8`, longT: thr, shortT: 8 }) }))
  console.log('simulated thresholds '+thresholds.join(','))

  for (const fee of [0,4]) {
    console.log('\n================ WALK-FORWARD/OOS feeRT='+fee+' ================')
    for (const objective of ['moIR','actIR','score'] as const) {
      for (const lookbackMonths of [12,18,24,'expanding'] as const) {
        for (const testMonths of [1,3]) {
          const startMonth = lookbackMonths === 'expanding' ? '2023-01' : monthStr(addMonths(Date.parse('2022-04-01T00:00:00Z'), lookbackMonths))
          const res = wfRun({bars,sims,fee,lookbackMonths,testMonths,objective,startMonth,endMonth:'2026-03'})
          printWfSummary('objective='+objective+' train='+lookbackMonths+' test='+testMonths+'m', res)
        }
      }
    }
  }
}

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