Blend standings-based strength into F1/IndyCar EV simulation

Championship futures odds rate mid-season drivers by likelihood to win the
title, which understates a driver like Hamilton (2nd in standings) because
the odds reflect the difficulty of closing a gap — not where he'll likely
finish. The simulator was using those odds as-is for all remaining race
win probabilities, making the current standings irrelevant to EV.

Fix: blend each driver's odds-derived strength with their share of total
current championship points, weighted by season progress (completedRaces /
totalRaces). Pre-season remains pure futures-odds; mid-season is a 50/50
blend; late-season standings dominate. Additionally scale PARTICIPANT_VOLATILITY
down by up to 70% as the season progresses to prevent unrealistic swings
when few races remain.

Applies equally to F1 and IndyCar via AutoRacingSimulator.

https://claude.ai/code/session_01FyG28zxsjSrgr8aKYuy6xY
This commit is contained in:
Claude 2026-06-15 01:02:46 +00:00
parent d0a31f3883
commit e507f82508
No known key found for this signature in database

View file

@ -117,13 +117,19 @@ export class AutoRacingSimulator implements Simulator {
seasonResults.map((r) => [r.participant.id, parseFloat(r.currentPoints ?? "0")])
);
// 3. Count remaining races: incomplete scoring events, excluding schedule_event entries
// 3. Count remaining and completed races (exclude schedule_event entries)
const allEvents = await db.query.scoringEvents.findMany({
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
});
const remainingRaces = allEvents.filter(
(e) => !e.isComplete && e.eventType !== "schedule_event"
).length;
const completedRaces = allEvents.filter(
(e) => e.isComplete && e.eventType !== "schedule_event"
).length;
// 0.0 = pre-season, 1.0 = all races done
const totalRaces = completedRaces + remainingRaces;
const seasonProgress = totalRaces > 0 ? completedRaces / totalRaces : 0;
// 4. Load EV data for championship win probabilities
const evs = await getAllParticipantEVsForSeason(sportsSeasonId);
@ -160,7 +166,28 @@ export class AutoRacingSimulator implements Simulator {
}
}
// 7. Accumulate finish counts across simulations
// 7. Build blended probability weights that combine futures-odds strength
// with standings-based strength, weighted by season progress.
// - Pre-season (seasonProgress=0): pure futures odds
// - Mid/late season: standings dominate, reducing the distortion from
// championship futures (which penalize 2nd-place drivers whose odds of
// *winning* the title are weak, even though they'll likely finish top 3)
const totalCurrentPoints = [...currentPointsMap.values()].reduce((a, b) => a + b, 0);
const blendedProbs = new Map<string, number>();
for (const id of ids) {
const oddsW = baseProbs.get(id) ?? fallbackProb;
let standingsW: number;
if (totalCurrentPoints > 0) {
const pts = currentPointsMap.get(id) ?? 0;
// Drivers with 0 pts (new entry, early DNF) fall back to odds strength
standingsW = pts > 0 ? pts / totalCurrentPoints : oddsW;
} else {
standingsW = oddsW;
}
blendedProbs.set(id, (1 - seasonProgress) * oddsW + seasonProgress * standingsW);
}
// Accumulate finish counts across simulations
// rankCounts[id][0..7] = number of times driver finished 1st..8th
const rankCounts = new Map<string, number[]>();
for (const id of ids) {
@ -180,14 +207,17 @@ export class AutoRacingSimulator implements Simulator {
}
} else {
// In-season: simulate remaining races from current standings.
// Volatility shrinks as the season progresses — late-season standings are
// much more predictive than early-season odds.
const effectiveVolatility = PARTICIPANT_VOLATILITY * (1 - seasonProgress * 0.7);
for (let sim = 0; sim < NUM_SIMULATIONS; sim++) {
// 7a. Season-long performance multiplier per driver
// 7a. Season-long performance multiplier per driver (uses blended strength)
const seasonWeights = new Map<string, number>();
for (const id of ids) {
const base = baseProbs.get(id) ?? fallbackProb;
const base = blendedProbs.get(id) ?? fallbackProb;
const mult = Math.max(
0.05,
1 - PARTICIPANT_VOLATILITY + Math.random() * PARTICIPANT_VOLATILITY * 2
1 - effectiveVolatility + Math.random() * effectiveVolatility * 2
);
seasonWeights.set(id, base * mult);
}