/** * Expected Value (EV) Calculator * * Calculates fantasy points expected value based on probability distributions * and league-specific scoring rules. */ /** * Scoring rules for a fantasy league season */ export interface ScoringRules { pointsFor1st: number; pointsFor2nd: number; pointsFor3rd: number; pointsFor4th: number; pointsFor5th: number; pointsFor6th: number; pointsFor7th: number; pointsFor8th: number; } /** * Probability distribution for a participant's placements * All values should be decimals (0-1) and sum to ~1.0 */ export interface ProbabilityDistribution { probFirst: number; // e.g., 0.155 means 15.5% probSecond: number; probThird: number; probFourth: number; probFifth: number; probSixth: number; probSeventh: number; probEighth: number; } /** * Calculate Expected Value for a participant * * EV = Σ (probability × points) for each placement * * @param probabilities - Probability distribution (decimals 0-1) * @param scoringRules - League's point values for each placement * @returns Expected value (average projected points) * * @example * ```ts * const ev = calculateEV( * { probFirst: 0.20, probSecond: 0.15, probThird: 0.12, ..., probEighth: 0.05 }, * { pointsFor1st: 100, pointsFor2nd: 70, ..., pointsFor8th: 15 } * ); * // Returns: 45.5 (expected points) * ``` */ export function calculateEV( probabilities: ProbabilityDistribution, scoringRules: ScoringRules ): number { const ev = probabilities.probFirst * scoringRules.pointsFor1st + probabilities.probSecond * scoringRules.pointsFor2nd + probabilities.probThird * scoringRules.pointsFor3rd + probabilities.probFourth * scoringRules.pointsFor4th + probabilities.probFifth * scoringRules.pointsFor5th + probabilities.probSixth * scoringRules.pointsFor6th + probabilities.probSeventh * scoringRules.pointsFor7th + probabilities.probEighth * scoringRules.pointsFor8th; return ev; } /** * Validate that probability distribution sums to approximately 1.0 * (allows small rounding errors) * * @param probabilities - Probability distribution to validate * @param tolerance - Acceptable deviation from 1.0 (default 0.01) * @returns true if valid, false otherwise */ export function validateProbabilities( probabilities: ProbabilityDistribution, tolerance: number = 0.01 ): boolean { const sum = probabilities.probFirst + probabilities.probSecond + probabilities.probThird + probabilities.probFourth + probabilities.probFifth + probabilities.probSixth + probabilities.probSeventh + probabilities.probEighth; return Math.abs(sum - 1.0) <= tolerance; } /** * Normalize probabilities to sum to exactly 1.0 * Useful when importing odds or dealing with rounding errors * * @param probabilities - Probability distribution to normalize * @returns Normalized probabilities that sum to 1.0 */ export function normalizeProbabilities( probabilities: ProbabilityDistribution ): ProbabilityDistribution { const sum = probabilities.probFirst + probabilities.probSecond + probabilities.probThird + probabilities.probFourth + probabilities.probFifth + probabilities.probSixth + probabilities.probSeventh + probabilities.probEighth; if (sum === 0) { // Avoid division by zero - return equal probabilities (1/8 = 0.125) return { probFirst: 0.125, probSecond: 0.125, probThird: 0.125, probFourth: 0.125, probFifth: 0.125, probSixth: 0.125, probSeventh: 0.125, probEighth: 0.125, }; } const factor = 1.0 / sum; return { probFirst: Math.round(probabilities.probFirst * factor * 10000) / 10000, probSecond: Math.round(probabilities.probSecond * factor * 10000) / 10000, probThird: Math.round(probabilities.probThird * factor * 10000) / 10000, probFourth: Math.round(probabilities.probFourth * factor * 10000) / 10000, probFifth: Math.round(probabilities.probFifth * factor * 10000) / 10000, probSixth: Math.round(probabilities.probSixth * factor * 10000) / 10000, probSeventh: Math.round(probabilities.probSeventh * factor * 10000) / 10000, probEighth: Math.round(probabilities.probEighth * factor * 10000) / 10000, }; } /** * Calculate projected total points for a team * * @param actualPoints - Points from finished participants * @param remainingParticipantEVs - Array of EV values for unfinished participants * @returns Object with actual, projected, and remaining count */ export function calculateProjectedTotal( actualPoints: number, remainingParticipantEVs: number[] ): { actualPoints: number; projectedPoints: number; participantsFinished: number; participantsRemaining: number; } { const evSum = remainingParticipantEVs.reduce((sum, ev) => sum + ev, 0); const projectedPoints = actualPoints + evSum; return { actualPoints: Math.round(actualPoints * 100) / 100, projectedPoints: Math.round(projectedPoints * 100) / 100, participantsFinished: 0, // Caller should provide this participantsRemaining: remainingParticipantEVs.length, }; }