import { simulate, type Observation, type Regime } from './simulation.ts'; export type BacktestOptions = { lookback: number; block: number; paths: number; seed: number; horizonWeeks: number; regime: Regime }; export function backtest(weekly: Observation[], options: BacktestOptions) { const { lookback, horizonWeeks } = options; if (![lookback, horizonWeeks, options.block, options.paths].every(Number.isInteger) || lookback < 2 || horizonWeeks < 1 || options.block < 1 || options.block > lookback || options.paths < 1) throw new Error('Invalid backtest options'); if (weekly.some((row, i) => !Number.isFinite(row.value) || row.value <= 0 || !Number.isFinite(row.ts) || row.date !== new Date(row.ts * 1000).toISOString().slice(0, 10) || (i > 0 && row.ts - weekly[i - 1]!.ts !== 604800))) throw new Error('Invalid weekly history'); const rows = []; for (let origin = lookback; origin + horizonWeeks < weekly.length; origin++) { const training = weekly.slice(0, origin + 1); const prediction = simulate(training, options).forecast.at(-1)!; const target = weekly[origin + horizonWeeks]!; const baseline = weekly[origin]!.value; const error = Math.abs(target.value - prediction.p50); const baselineError = Math.abs(target.value - baseline); const width = prediction.p90 - prediction.p10; // Proper interval score for an 80% interval: width plus 2/alpha miss penalty. const intervalScore = width + 10 * Math.max(prediction.p10 - target.value, target.value - prediction.p90, 0); rows.push({ origin: weekly[origin]!.date, target: target.date, actual: target.value, baseline, p10: prediction.p10, p50: prediction.p50, p90: prediction.p90, error, baselineError, covered: target.value >= prediction.p10 && target.value <= prediction.p90, intervalScore }); } const n = rows.length; const average = (values: number[]) => values.reduce((sum, v) => sum + v, 0) / n; const summary = n === 0 ? null : { mae: average(rows.map(r => r.error)), baselineMae: average(rows.map(r => r.baselineError)), coverage: average(rows.map(r => Number(r.covered))), intervalScore: average(rows.map(r => r.intervalScore)), }; return { options, n, summary, rows }; }