From e507f82508372ce23999462251e8b58be399bd38 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 01:02:46 +0000 Subject: [PATCH] Blend standings-based strength into F1/IndyCar EV simulation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../simulations/auto-racing-simulator.ts | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/app/services/simulations/auto-racing-simulator.ts b/app/services/simulations/auto-racing-simulator.ts index 64072ee..8120702 100644 --- a/app/services/simulations/auto-racing-simulator.ts +++ b/app/services/simulations/auto-racing-simulator.ts @@ -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(); + 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(); 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(); 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); }