/** * 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 percentages (0-100) and sum to ~100 */ export interface ProbabilityDistribution { probFirst: number; // e.g., 15.5 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 (percentages 0-100) * @param scoringRules - League's point values for each placement * @returns Expected value (average projected points) * * @example * ```ts * const ev = calculateEV( * { probFirst: 20, probSecond: 15, probThird: 12, ..., probEighth: 5 }, * { pointsFor1st: 100, pointsFor2nd: 70, ..., pointsFor8th: 15 } * ); * // Returns: 45.5 (expected points) * ``` */ export function calculateEV( probabilities: ProbabilityDistribution, scoringRules: ScoringRules ): number { // Convert percentages to decimals (divide by 100) const ev = (probabilities.probFirst / 100) * scoringRules.pointsFor1st + (probabilities.probSecond / 100) * scoringRules.pointsFor2nd + (probabilities.probThird / 100) * scoringRules.pointsFor3rd + (probabilities.probFourth / 100) * scoringRules.pointsFor4th + (probabilities.probFifth / 100) * scoringRules.pointsFor5th + (probabilities.probSixth / 100) * scoringRules.pointsFor6th + (probabilities.probSeventh / 100) * scoringRules.pointsFor7th + (probabilities.probEighth / 100) * scoringRules.pointsFor8th; return Math.round(ev * 100) / 100; // Round to 2 decimal places } /** * Validate that probability distribution sums to approximately 100% * (allows small rounding errors) * * @param probabilities - Probability distribution to validate * @param tolerance - Acceptable deviation from 100% (default 0.1%) * @returns true if valid, false otherwise */ export function validateProbabilities( probabilities: ProbabilityDistribution, tolerance: number = 0.1 ): boolean { const sum = probabilities.probFirst + probabilities.probSecond + probabilities.probThird + probabilities.probFourth + probabilities.probFifth + probabilities.probSixth + probabilities.probSeventh + probabilities.probEighth; return Math.abs(sum - 100) <= tolerance; } /** * Normalize probabilities to sum to exactly 100% * Useful when importing odds or dealing with rounding errors * * @param probabilities - Probability distribution to normalize * @returns Normalized probabilities that sum to 100% */ 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 return { probFirst: 12.5, probSecond: 12.5, probThird: 12.5, probFourth: 12.5, probFifth: 12.5, probSixth: 12.5, probSeventh: 12.5, probEighth: 12.5, }; } const factor = 100 / sum; return { probFirst: Math.round(probabilities.probFirst * factor * 100) / 100, probSecond: Math.round(probabilities.probSecond * factor * 100) / 100, probThird: Math.round(probabilities.probThird * factor * 100) / 100, probFourth: Math.round(probabilities.probFourth * factor * 100) / 100, probFifth: Math.round(probabilities.probFifth * factor * 100) / 100, probSixth: Math.round(probabilities.probSixth * factor * 100) / 100, probSeventh: Math.round(probabilities.probSeventh * factor * 100) / 100, probEighth: Math.round(probabilities.probEighth * factor * 100) / 100, }; } /** * 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, }; }