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:
parent
d0a31f3883
commit
e507f82508
1 changed files with 35 additions and 5 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue