/** * Bracket Simulator * * Wraps the existing Monte Carlo bracket simulator (app/services/bracket-simulator.ts) * to conform to the Simulator interface. Loads current participant Elo ratings * from participantExpectedValues and runs 100k simulations. * * The existing bracket simulator uses Elo ratings derived from futures odds * (via the probability engine pipeline). These should be imported via the * admin "Futures Odds" page before running simulation. */ import { database } from "~/database/context"; import { participantExpectedValues, participants } from "~/database/schema"; import { eq, and, isNotNull } from "drizzle-orm"; import { simulateBracket } from "~/services/bracket-simulator"; import { convertFuturesToElo } from "~/services/probability-engine"; import type { Simulator, SimulationResult } from "./types"; export class BracketSimulator implements Simulator { async simulate(sportsSeasonId: string): Promise { const db = database(); // Load all participants with their current EVs (which hold probability distributions) const evRows = await db .select({ participantId: participantExpectedValues.participantId, probFirst: participantExpectedValues.probFirst, sourceOdds: participantExpectedValues.sourceOdds, }) .from(participantExpectedValues) .where(eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)); if (evRows.length === 0) { throw new Error( `No participant EVs found for sports season ${sportsSeasonId}. ` + `Import futures odds first via Admin → Futures Odds.` ); } // Build Elo ratings from source odds (American odds format) if available, // otherwise fall back to using probFirst as a proxy for win probability. let eloMap: Map; const hasOdds = evRows.some((r) => r.sourceOdds !== null); if (hasOdds) { const oddsInput = evRows .filter((r) => r.sourceOdds !== null) .map((r) => ({ participantId: r.participantId, odds: r.sourceOdds! })); eloMap = convertFuturesToElo(oddsInput); } else { // Fall back: treat probFirst (as %) as championship win probability, // convert to a rough Elo by mapping [min, max] prob → [1250, 1750] const probs = evRows.map((r) => parseFloat(r.probFirst)); const minProb = Math.min(...probs); const maxProb = Math.max(...probs); const range = maxProb - minProb || 1; eloMap = new Map( evRows.map((r) => { const prob = parseFloat(r.probFirst); const normalised = (prob - minProb) / range; const elo = 1250 + normalised * 500; return [r.participantId, elo]; }) ); } const teamsForSimulation = evRows .filter((r) => eloMap.has(r.participantId)) .map((r) => ({ participantId: r.participantId, elo: eloMap.get(r.participantId)!, })); if (teamsForSimulation.length === 0) { throw new Error(`Could not build Elo ratings for sports season ${sportsSeasonId}.`); } const probMap = await simulateBracket(teamsForSimulation); return Array.from(probMap.entries()).map(([participantId, probs]) => ({ participantId, probabilities: { probFirst: probs[0], probSecond: probs[1], probThird: probs[2], probFourth: probs[3], probFifth: probs[4], probSixth: probs[5], probSeventh: probs[6], probEighth: probs[7], }, source: "bracket_monte_carlo", })); } }