172 lines
5.1 KiB
TypeScript
172 lines
5.1 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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,
|
|||
|
|
};
|
|||
|
|
}
|