Quick backtest mode for image builds (identical published numbers)

BACKTEST_QUICK=1 skips the hyperparameter grid and scores only the
shipped + previous configs — same test metrics, a fraction of the
build time on the 1-CPU VPS. The full grid search stays a local
activity. Verified byte-level: quick output matches the grid run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:33:42 +02:00
parent dc3c375081
commit 716dab2f23
2 changed files with 53 additions and 31 deletions
+4 -1
View File
@@ -4,7 +4,10 @@ WORKDIR /app
COPY package.json bun.lock* bun.lockb* ./ COPY package.json bun.lock* bun.lockb* ./
RUN bun install RUN bun install
COPY . . COPY . .
RUN bun run data:build # icons + fixtures.json + ratings.json → public/ # Quick backtest in image builds: score the shipped config only — the published
# numbers are identical; the hyperparam grid search is a local-dev activity.
ENV BACKTEST_QUICK=1
RUN bun run data:build # icons + fixtures.json + ratings.json + backtest → public/
RUN bun run build # tsc -b && vite build → /app/dist RUN bun run build # tsc -b && vite build → /app/dist
RUN bun run build:server # esbuild → /app/server/dist/index.js RUN bun run build:server # esbuild → /app/server/dist/index.js
+21 -2
View File
@@ -40,6 +40,12 @@ const FORM_WINDOW = 10; // last N non-friendly matches feeding the form signal
const MIN_VAL_GAIN = 0.0005; // candidate must beat the previous config by this much const MIN_VAL_GAIN = 0.0005; // candidate must beat the previous config by this much
/** the previously shipped v3 config — the bar every candidate has to clear */ /** the previously shipped v3 config — the bar every candidate has to clear */
const PREV = { xi: 0.5, k: 5, w: 0.75, m: 2, beta: 0, gamma: 0 }; const PREV = { xi: 0.5, k: 5, w: 0.75, m: 2, beta: 0, gamma: 0 };
/** the v4 winner (from the full grid run) — keep in sync with buildRatings.ts */
const SHIPPED = { xi: 0.25, k: 3, w: 0.45, m: 3, beta: 0.3, gamma: 0 };
/** BACKTEST_QUICK=1 (Docker builds): skip the grid search and just score the
* shipped + previous configs — the published test numbers are identical, the
* hyperparam hunt only happens locally. */
const QUICK = process.env.BACKTEST_QUICK === '1';
interface Row { date: string; home: string; away: string; hs: number; as: number; tournament: string; neutral: boolean; t: number } interface Row { date: string; home: string; away: string; hs: number; as: number; tournament: string; neutral: boolean; t: number }
type Probs = { h: number; d: number; a: number }; type Probs = { h: number; d: number; a: number };
@@ -241,11 +247,23 @@ function main(): void {
return W_STEPS.map((w, wi) => ({ w, rps: acc[wi]! / preds.length })); return W_STEPS.map((w, wi) => ({ w, rps: acc[wi]! / preds.length }));
}; };
let best = { xi: PREV.xi, k: PREV.k, w: PREV.w, m: PREV.m, beta: 0, gamma: 0, rps: Infinity };
let prevValRps = Infinity; // validation RPS of the previously shipped config
if (QUICK) {
// ---------- quick mode: score only shipped + previous on validation ----------
console.log('quick mode — scoring shipped + previous configs on validation…');
const predsShipped = walkTeamDc(SHIPPED.xi, SHIPPED.k, tValFrom, tTestFrom);
const predsPrev = SHIPPED.xi === PREV.xi && SHIPPED.k === PREV.k
? predsShipped
: walkTeamDc(PREV.xi, PREV.k, tValFrom, tTestFrom);
const shippedRps = sweepW(predsShipped, SHIPPED.beta, SHIPPED.m, SHIPPED.gamma).find((x) => x.w === SHIPPED.w)!.rps;
prevValRps = sweepW(predsPrev, PREV.beta, PREV.m, PREV.gamma).find((x) => x.w === PREV.w)!.rps;
best = { ...SHIPPED, rps: shippedRps };
} else {
// ---------- validation stage 1: tune ξ, k, w (β=0, γ=0) ---------- // ---------- validation stage 1: tune ξ, k, w (β=0, γ=0) ----------
console.log('validation stage 1 — ξ/k/w grid (β=0, γ=0)…'); console.log('validation stage 1 — ξ/k/w grid (β=0, γ=0)…');
const valWalks = new Map<string, Pred[]>(); const valWalks = new Map<string, Pred[]>();
let best = { xi: PREV.xi, k: PREV.k, w: PREV.w, m: PREV.m, beta: 0, gamma: 0, rps: Infinity };
let prevValRps = Infinity; // validation RPS of the previously shipped config
for (const xi of XI_GRID) { for (const xi of XI_GRID) {
for (const k of K_GRID) { for (const k of K_GRID) {
const preds = walkTeamDc(xi, k, tValFrom, tTestFrom); const preds = walkTeamDc(xi, k, tValFrom, tTestFrom);
@@ -279,6 +297,7 @@ function main(): void {
if (r < best.rps) best = { ...best, gamma, w, rps: r }; if (r < best.rps) best = { ...best, gamma, w, rps: r };
} }
} }
}
console.log(` champion: ξ=${best.xi} k=${best.k} w=${best.w} β=${best.beta} m=${best.m} γ=${best.gamma} rps=${best.rps.toFixed(4)}`); console.log(` champion: ξ=${best.xi} k=${best.k} w=${best.w} β=${best.beta} m=${best.m} γ=${best.gamma} rps=${best.rps.toFixed(4)}`);
// ---------- decision rule: ship only a validated improvement ---------- // ---------- decision rule: ship only a validated improvement ----------