import { logger } from "~/lib/logger"; /** * ICM (Independent Chip Model) Calculator - Monte Carlo Simulation * * Calculates probability distributions for tournament placements based on * championship odds using Monte Carlo simulation (100,000+ iterations). * * Algorithm: * 1. Pick 1st place: weighted random selection based on championship probabilities * 2. Remove winner, pick 2nd place: weighted random from remaining * 3. Continue for all 8 positions * 4. Repeat 100,000 times * 5. Calculate probabilities from simulation results * * This approach is much faster and more memory-efficient than exact recursive * calculation for large fields (30+ participants). Results converge to true ICM * probabilities with sufficient iterations. */ /** * Participant with championship probability (chip stack) */ export interface ParticipantChips { participantId: string; championshipProbability: number; // 0-1 (e.g., 0.154 = 15.4%) } /** * ICM result for a single participant * Probabilities for finishing in each placement */ export interface ICMResult { participantId: string; probabilities: { first: number; second: number; third: number; fourth: number; fifth: number; sixth: number; seventh: number; eighth: number; }; } /** * Simulate one tournament using weighted random selection * * @param participants Participants with championship probabilities (weights) * @param scoringPlaces Number of positions to simulate * @returns Array of participant IDs in finish order [1st, 2nd, ..., 8th] */ function simulateTournament( participants: ParticipantChips[], scoringPlaces: number ): string[] { const placements: string[] = []; let remaining = [...participants]; for (let pos = 0; pos < scoringPlaces && remaining.length > 0; pos++) { // Calculate total weight of remaining participants const totalWeight = remaining.reduce((sum, p) => sum + p.championshipProbability, 0); // Weighted random selection const rand = Math.random() * totalWeight; let cumulative = 0; let selected = remaining[0]; for (const p of remaining) { cumulative += p.championshipProbability; if (rand <= cumulative) { selected = p; break; } } // Record placement and remove from remaining placements.push(selected.participantId); remaining = remaining.filter(p => p.participantId !== selected.participantId); } return placements; } /** * Calculate ICM probability distribution for all participants using Monte Carlo simulation * * Simulates 100,000 tournaments using weighted random selection based on championship * probabilities. Much faster and more memory-efficient than exact recursive calculation * for large fields (30+ participants). * * @param participants Array of participants with championship probabilities * @param scoringPlaces Number of places that score points (default 8) * @param iterations Number of simulations to run (default 100,000) * @returns Map of participantId to probability distribution * * @example * const participants = [ * { participantId: 'OKC', championshipProbability: 0.364 }, // 36.4% (+175 odds) * { participantId: 'BOS', championshipProbability: 0.300 }, * // ... 28 more NBA teams * ]; * const results = calculateICM(participants); * // Results for all 30 teams, each with P(1st) through P(8th) from simulation */ export function calculateICM( participants: ParticipantChips[], scoringPlaces = 8, iterations = 100000 ): Map { if (participants.length === 0) { return new Map(); } // Normalize championship probabilities to sum to 1.0 (remove bookmaker vig) const totalProb = participants.reduce((sum, p) => sum + p.championshipProbability, 0); const normalized = participants.map(p => ({ ...p, championshipProbability: totalProb > 0 ? p.championshipProbability / totalProb : 1 / participants.length, })); logger.log(`[ICM Simulation] Starting ${iterations.toLocaleString()} simulations for ${normalized.length} participants`); const startTime = Date.now(); // Initialize counters for each participant const counts = new Map(); for (const p of normalized) { counts.set(p.participantId, Array(scoringPlaces).fill(0)); } // Run simulations for (let iter = 0; iter < iterations; iter++) { const placements = simulateTournament(normalized, scoringPlaces); // Record results for (let pos = 0; pos < placements.length; pos++) { const participantCounts = counts.get(placements[pos]); if (participantCounts) participantCounts[pos]++; } // Progress logging every 10,000 iterations if ((iter + 1) % 10000 === 0) { const progress = ((iter + 1) / iterations * 100).toFixed(0); const elapsed = Date.now() - startTime; const rate = (iter + 1) / (elapsed / 1000); logger.log(`[ICM Simulation] ${progress}% complete (${(iter + 1).toLocaleString()}/${iterations.toLocaleString()}) - ${rate.toFixed(0)} sims/sec`); } } // Convert counts to probabilities const results = new Map(); for (const p of normalized) { const participantCounts = counts.get(p.participantId) ?? Array(scoringPlaces).fill(0); const probabilities = participantCounts.map(count => count / iterations); results.set(p.participantId, { participantId: p.participantId, probabilities: { first: probabilities[0] || 0, second: probabilities[1] || 0, third: probabilities[2] || 0, fourth: probabilities[3] || 0, fifth: probabilities[4] || 0, sixth: probabilities[5] || 0, seventh: probabilities[6] || 0, eighth: probabilities[7] || 0, }, }); } const elapsed = Date.now() - startTime; logger.log(`[ICM Simulation] Completed in ${(elapsed / 1000).toFixed(1)}s (${(iterations / (elapsed / 1000)).toFixed(0)} sims/sec)`); return results; } /** * Convert ICM result to array format for database storage * * @param icmResult ICM result object * @returns Array of 8 probabilities [P(1st), P(2nd), ..., P(8th)] */ export function icmResultToArray(icmResult: ICMResult): number[] { return [ icmResult.probabilities.first, icmResult.probabilities.second, icmResult.probabilities.third, icmResult.probabilities.fourth, icmResult.probabilities.fifth, icmResult.probabilities.sixth, icmResult.probabilities.seventh, icmResult.probabilities.eighth, ]; } /** * Convert American odds to implied probability * * @param odds American odds (e.g., +175, -200) * @returns Implied probability (0-1) */ function convertAmericanOddsToProbability(odds: number): number { if (odds > 0) { // Positive odds (underdog): probability = 100 / (odds + 100) return 100 / (odds + 100); } else { // Negative odds (favorite): probability = -odds / (-odds + 100) return -odds / (-odds + 100); } } /** * Convert futures odds to championship probabilities and calculate ICM * * Complete pipeline from odds to probability distributions. * * @param futuresOdds Array of {participantId, odds} with American odds * @param scoringPlaces Number of places that score points (default 8) * @returns Map of participantId to ICM result * * @example * const odds = [ * { participantId: 'OKC', odds: 175 }, // +175 (36.4% implied) * { participantId: 'BOS', odds: -150 }, // -150 (60% implied) * // ... more teams * ]; * const results = calculateICMFromOdds(odds); */ export function calculateICMFromOdds( futuresOdds: Array<{ participantId: string; odds: number }>, scoringPlaces = 8 ): Map { // Convert odds to championship probabilities const participants: ParticipantChips[] = futuresOdds.map(({ participantId, odds }) => ({ participantId, championshipProbability: convertAmericanOddsToProbability(odds), })); return calculateICM(participants, scoringPlaces); }