# Phase 5: Expected Value System - Detailed Implementation Plan ## 🎯 Overall Progress **Phase 5.1 (MVP - Manual Entry)**: ✅ **COMPLETE** - ✅ Database schema (5.1.1) - ✅ Core EV calculator (5.1.2) - 20/20 tests passing - ✅ Model layer (5.1.3) - 13/13 tests passing - ✅ Admin UI (5.1.4) - Fully functional - ⏭️ CSV bulk import (5.1.5) - **SKIPPED** (not needed with Phase 5.2 automation) **Phase 5.2 (ICM-Based Calculation)**: ✅ **COMPLETE** - ✅ ICM calculator (5.2.2b) - Monte Carlo simulation approach - 13/13 tests passing - ✅ Futures odds UI updated to use ICM (5.2.3) - Fully functional - ✅ Works with ANY number of participants (not just 8) - ✅ Every participant gets probabilities, even longshots - ✅ **Performance**: 2-5 seconds for 30 teams using 100,000 simulations - ⏭️ Elo/Bracket simulator (5.2.2) - **PRESERVED** for future hybrid approach - ⏭️ API integration (5.2.4) - **DEFERRED** (manual entry sufficient for MVP) **Phase 5.3 (Real Results Integration)**: ✅ **COMPLETE** - ✅ Probability updater service (5.3.1) - 8/8 tests passing - ✅ Partial results handling (5.3.2) - Works for all sport types - ✅ Admin UI with preview (5.3.3) - `/admin/sports-seasons/:id/recalculate-probabilities` - ✅ Automatic triggers (5.3.4) - Integrated into scoring calculator **Phase 5.4 (Projected Totals Display)**: ✅ **COMPLETE** - ✅ Standings snapshot calculation (5.4.1) - ✅ Standings page UI (5.4.2) - ✅ Team breakdown page (5.4.3) - ⏭️ Participant detail page (5.4.4) - SKIPPED (not needed for MVP) **Total Test Coverage**: 105/105 tests passing (33 EV + 51 Elo/Bracket + 13 ICM + 8 probability-updater) **Ready to Test**: Yes! - Admin: Manual entry: `/admin/sports-seasons/:id/expected-values` - Admin: Futures odds (ICM): `/admin/sports-seasons/:id/futures-odds` ⭐ Works with all teams! Monte Carlo simulation (2-5 sec) - Admin: Recalculate: `/admin/sports-seasons/:id/recalculate-probabilities` ⭐ Auto-updates when results entered! - **User: League standings**: `/leagues/:leagueId/standings/:seasonId` ⭐ **NEW - Shows projected points!** - **User: Team breakdown**: `/leagues/:leagueId/standings/:seasonId/teams/:teamId` ⭐ **NEW - Shows per-participant projections!** **Key Features**: - Monte Carlo ICM simulation: 100,000 iterations for accurate probability distributions - Fast performance: 2-5 seconds for 30 teams (vs. minutes with exact recursion) - Correct probability handling: weak teams sum to <1.0 (e.g., 24% chance of top-8 finish) - Preview-to-save consistency: hidden fields pass exact probabilities (no re-simulation) - Automatic probability updates when results are entered - Preview before/after changes before applying - Works for playoffs, season standings, and qualifying points **Key Implementation Insights**: - **Probabilities don't always sum to 1.0**: In a 30-team league with top-8 scoring, weak teams have low probability of finishing in top 8 (e.g., 24%), with remaining probability for 9th-30th (which we don't store) - **Don't normalize ICM results**: Monte Carlo simulation produces mathematically correct probabilities; normalizing them incorrectly inflates values for weak teams - **Pass preview through save**: Each Monte Carlo run produces different random results; must pass preview probabilities through hidden fields to ensure saved values match what user saw **Next Phase**: Phase 5.4 Projected Totals Display --- ## Executive Summary Based on your answers, here's the core approach: **MVP Strategy**: Start with **one sport** (suggest NFL playoffs) using **Vegas futures odds** as the probability source. This can be reused across all scoring patterns later. **Key Principles**: - Different scoring patterns = different probability models - Finished participants ALWAYS have EV = actual points - Store P(1st) through P(8th) once, calculate league-specific EV on demand - Display as "Projected Points" (not "Expected Value") - Never override manual draft queues - Only update active/upcoming seasons --- ## Follow-up Clarifications Needed Before I finalize the plan, I need to clarify a few things based on your answers: ### Clarification 1: Futures Odds Data Shape You asked: "Are we thinking using vegas futures, or potentially Elo ratings?" **Here's the difference**: **Vegas Futures** give us direct probabilities: - Input: "Chiefs +500 to win Super Bowl" → 16.7% probability - We convert all futures odds to probabilities that sum to 100% - Then distribute those probabilities across our 1st-8th placements **Elo Ratings** give us relative strength: - Input: "Chiefs Elo: 1850, Bills Elo: 1820" - We run Monte Carlo simulations to generate probabilities - Simulate 100k tournaments, count how often each team finishes 1st-8th **My recommendation**: Start with **Vegas Futures** because: 1. Direct conversion to probabilities (simpler) 2. Can be manually entered if API not available 3. Works for all sports (most have championship futures) 4. Easier to validate (bookmakers already balanced the probabilities) **Question for you**: Does starting with Vegas Futures approach make sense? We can add Elo-based simulation for specific sports (F1, Tennis) in Phase 5.2+. Please start with the vegas futures. However, one thing we can do is use things like the independent chip model and potentially even like the algorithm to calculate ICM in large tournaments. Here are some links: - https://en.wikipedia.org/wiki/Independent_Chip_Model#Model - https://www.holdemresources.net/blog/high-accuracy-mtt-icm/ - https://forumserver.twoplustwo.com/15/poker-theory-amp-gto/new-algorithm-calculate-icm-large-tournaments-1098489/ --- ### Clarification 2: Monte Carlo Simulation Scope You said: "simulate the winning odds in the system for everyone and then apply it accordingly, not per participant" **I want to confirm I understand correctly**: - Run ONE simulation of the entire tournament/season (100k iterations) - Count how often each participant finishes 1st, 2nd, ..., 8th - Store those percentages in the database - For each league, multiply those percentages by that league's scoring rules **Is this correct?** Or did you mean something different? That's correct! --- ### Clarification 3: Performance & Caching You asked: "I don't know if it'll be taxing, what do you think? We could probably cache the results as well I guess?" **My analysis**: **Good news**: Your approach is already efficient! - Store P(1st)-P(8th) once per participant (8 decimals) - Calculate EV on-demand: `sum(P(placement) * points(placement))` - this is cheap (8 multiplications) - 100 participants × 50 leagues = 5000 EV calcs = ~0.4ms on modern hardware **Recommendation**: - **Don't cache individual EVs** (calculation is faster than cache lookup) - **DO pre-calculate projected totals** for standings snapshots (you already agreed to this) - **DO cache probability distributions** (expensive Monte Carlo simulations run weekly, not per-request) **Question**: Does this approach make sense? Yes! --- ### Clarification 4: Which Sport for MVP? You said "start with one sport" - which one do you prefer? **Options ranked by difficulty**: 1. **NFL Playoffs** (easiest) - Single elimination bracket (already implemented in Phase 2) - Futures odds readily available - Clear 1st-8th placements - 14 teams total, only 8 score fantasy points 2. **Golf Majors** (medium) - Qualifying points system (implemented in Phase 3) - Need to simulate 4 tournaments - Predict QP totals → convert to fantasy placements - Futures odds available for each major 3. **F1 Season** (harder) - Season standings (implemented in Phase 3) - Need to simulate ~20 races - Predict championship points → positions - May need Elo or performance models (futures less useful) **My recommendation**: Start with **NFL Playoffs** for MVP, then expand to Golf, then F1. **Question**: Does NFL Playoffs make sense as the starting sport? Actually can we start with the NHL? Similar setup but I will be working with a league that's currently running so the numbers will be easier to gather. --- ### Clarification 5: Futures Odds Format When you manually enter futures odds (or we fetch from API), what format works best? **Option A: American Odds** ``` Chiefs: +500 49ers: +600 Ravens: +800 ``` **Option B: Decimal Odds** ``` Chiefs: 6.00 49ers: 7.00 Ravens: 9.00 ``` **Option C: Implied Probabilities** ``` Chiefs: 16.7% 49ers: 14.3% Ravens: 11.1% ``` We can convert between all three, but which is easiest for you to source/enter? I'm going to guess the American odds but it would be nice to be able to convert. That's probably not MVP though. --- ## Recommended Architecture Based on your answers, here's the system design: ### Data Flow ``` ┌─────────────────────────────────────────────────────────────┐ │ 1. PROBABILITY GENERATION │ │ │ │ Manual Entry OR API Fetch │ │ ↓ │ │ Convert to P(1st) - P(8th) │ │ ↓ │ │ Store in participant_probabilities table │ │ (sportsSeasonId + participantId → 8 percentages) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 2. EV CALCULATION (On-Demand) │ │ │ │ For each participant in a league: │ │ EV = Σ P(placement) × league_points(placement) │ │ │ │ Example: │ │ Chiefs: 20% × 100 + 15% × 70 + ... = 45.5 points │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 3. PROJECTED TOTALS (Pre-calculated with snapshots) │ │ │ │ For each team in a season: │ │ Projected Total = Actual Points + Sum(EVs of unfinished) │ │ │ │ Stored in team_standings_snapshots table │ └─────────────────────────────────────────────────────────────┘ ``` ### Database Schema Changes **New Table: `participant_probabilities`** ```typescript participant_probabilities { id: uuid PRIMARY KEY participantId: uuid -> participants.id sportsSeasonId: uuid -> sportsSeasons.id // Probability distribution (percentages that sum to 100) probFirst: decimal(5,2) // e.g., 15.50 = 15.5% probSecond: decimal(5,2) probThird: decimal(5,2) probFourth: decimal(5,2) probFifth: decimal(5,2) probSixth: decimal(5,2) probSeventh: decimal(5,2) probEighth: decimal(5,2) // Metadata source: enum('manual', 'futures_odds', 'elo_simulation', 'performance_model') lastUpdated: timestamp calculatedAt: timestamp // Unique constraint UNIQUE(participantId, sportsSeasonId) } ``` **Why separate from `participant_expected_values`?** - Probabilities are **sports season-specific** (same for all leagues) - EVs are **league-specific** (different scoring rules) - This avoids storing duplicate probability data - Saves ~800 bytes per participant per league **Modify: `team_standings_snapshots`** ```typescript team_standings_snapshots { // ... existing fields // NEW FIELDS: actualPoints: decimal // Points from completed participants projectedPoints: decimal // actualPoints + sum(EVs of unfinished) participantsFinished: integer // Count of finished participantsRemaining: integer // Count not yet finished } ``` ### Service Layer Architecture **New Module: `app/services/probability-engine.ts`** ```typescript // Core probability generation async function generateProbabilitiesFromFutures( sportsSeasonId: string, futuresOdds: { participantId: string; odds: number }[] ): Promise // Convert odds to probabilities function convertOddsToProbabilities( odds: { participantId: string; odds: number }[], oddsFormat: 'american' | 'decimal' ): { participantId: string; probabilities: number[] }[] // Monte Carlo simulation (for future phases) async function runMonteCarloSimulation( sportsSeasonId: string, simulations: number = 100000 ): Promise> ``` **New Module: `app/services/ev-calculator.ts`** ```typescript // Calculate EV for one participant in one league function calculateEV( probabilities: number[], // P(1st) through P(8th) scoringRules: ScoringRules // League's point values ): number // Calculate EV for all participants in a sports season for a specific league async function calculateAllEVsForLeague( seasonId: string, sportsSeasonId: string ): Promise> // participantId → EV // Get projected total for a team async function calculateProjectedTotal( teamId: string, seasonId: string ): Promise<{ actualPoints: number; projectedPoints: number; participantsFinished: number; participantsRemaining: number; }> ``` **New Module: `app/services/probability-updater.ts`** ```typescript // Update probabilities based on real results async function updateProbabilitiesAfterResult( sportsSeasonId: string ): Promise // Re-simulate remaining events with actual results locked in async function resimulateWithPartialResults( sportsSeasonId: string, completedEvents: string[], remainingEvents: string[] ): Promise ``` --- ## Phase 5 Task Breakdown (Revised) ### Phase 5.1: Foundation & Manual Entry (MVP) **Goal**: Admin can manually enter probabilities, system calculates EVs - [x] **5.1.1** Database schema ✅ **COMPLETE** - [x] Create `participant_expected_values` table (renamed from `participant_probabilities`) - [x] Add `actualPoints`, `projectedPoints`, `participantsFinished` to `team_standings_snapshots` - [x] Create migration (0024_faithful_mesmero.sql) - [x] Add Drizzle schema definitions - [x] Add `source` enum: manual, futures_odds, elo_simulation, performance_model - [x] Add calibration fields to `sportsSeasons`: eloCalibrationExponent, eloMinRating, eloMaxRating - [x] **5.1.2** Core calculation functions ✅ **COMPLETE** - [x] `app/services/ev-calculator.ts` - [x] `calculateEV(probabilities, scoringRules)` - pure function - [x] `validateProbabilities(probabilities, tolerance)` - validation - [x] `normalizeProbabilities(probabilities)` - auto-fix rounding errors - [x] `calculateProjectedTotal(actualPoints, remainingEVs)` - team projection - [x] Unit tests (20 tests, all passing) - [x] Test EV calculation with different scoring rules - [x] Test with equal probabilities, favorites, underdogs - [x] Test validation (valid/invalid sums) - [x] Test normalization (>100%, <100%, =100%) - [x] Test projected totals - [x] **5.1.3** Probability storage model ✅ **COMPLETE** - [x] `app/models/participant-expected-value.ts` - [x] `upsertParticipantEV()` - insert/update with validation - [x] `upsertParticipantEVWithNormalization()` - auto-normalize - [x] `getParticipantEV()` - get one participant - [x] `getAllParticipantEVsForSeason()` - get all for a season - [x] `deleteParticipantEV()` - delete record - [x] `batchUpsertParticipantEVs()` - batch operations (50 at a time) - [x] `recalculateEV()` - update EV with new scoring rules - [x] `recalculateAllEVsForSeason()` - bulk recalculation - [x] `toProbabilityDistribution()` - type conversion helper - [x] Validation: probabilities sum to 100% (±0.1% tolerance) - [x] Unit tests (13 documentation tests, all passing) - [x] **5.1.4** Admin UI - Manual probability entry ✅ **COMPLETE** - [x] Route: `/admin/sports-seasons/:id/expected-values` - [x] List all participants in sports season - [x] For each participant: 8 input fields (P(1st) through P(8th)) - [x] Validation: probabilities sum to 100% - [x] Save button → `upsertParticipantEV()` - [x] Display calculated EV in table - [x] Auto-populate with existing EVs (or 12.5% equal distribution) - [x] Success/error feedback messages - [x] Navigation: Added "Expected Values" card to sports season detail page - [x] Uses default scoring (100/70/50/40/25/25/15/15) with note about recalculation - [~] **5.1.5** Admin UI - Bulk import **SKIPPED** - Rationale: Phase 5.2 will automate probability generation from futures odds - Manual entry via 5.1.4 UI is sufficient for edge cases - Can be added later if needed - ~~CSV upload format~~ - ~~Validate CSV rows~~ - ~~Preview before import~~ - ~~Bulk insert probabilities~~ **Phase 5.1 Status**: ✅ **COMPLETE** (4/4 required tasks) **Test Coverage**: 33 tests passing (20 calculator + 13 model) **Deliverables**: - ✅ Probability storage infrastructure - ✅ EV calculation engine - ✅ Admin UI for manual probability entry - ✅ Validation and error handling - ✅ Real-time EV calculation on save ### Phase 5.2: Futures Odds Integration ✅ **COMPLETE** (ICM-based approach) **Goal**: Convert Vegas futures to probabilities automatically **Important Change**: Switched to ICM (Independent Chip Model) approach instead of Elo-based bracket simulation. This allows handling ALL participants (e.g., all 32 NHL teams), not just playoff teams. - [x] **5.2.1** Elo conversion with calibration ✅ **COMPLETE** - [x] `app/services/probability-engine.ts` - [x] `convertAmericanOddsToProbability(odds)` - +500 → 16.7% - [x] `convertDecimalOddsToProbability(odds)` - 6.00 → 16.7% - [x] `normalizeProbabilities(probs)` - ensure sum to 100% - [x] `decompressProbability(prob, exponent)` - Apply power transformation - [x] `mapToElo(strength, params)` - Normalize to Elo scale - [x] `eloWinProbability(eloA, eloB)` - Standard Elo formula - [x] `convertFuturesToElo()` - Complete pipeline - [x] `calculatePredictionError()` - For calibration - [x] Unit tests (38 tests passing) ✅ - [x] Test odds conversions (American, Decimal) - [x] Test normalization (handle bookmaker vig) - [x] Test Elo conversion functions - [x] Test prediction error calculation - [x] Integration test with NHL example data - [x] **5.2.2** Bracket simulator ✅ **COMPLETE** - [x] `app/services/bracket-simulator.ts` - [x] `simulateBracket()` - Async with progress callbacks - [x] `simulateBracketSync()` - Synchronous version - [x] `simulate8TeamBracket()` - NHL/NFL format - [x] Support for tied placements (3-4, 5-8) - [x] Unit tests (13 tests passing) ✅ - [x] Test equal ratings (equal probabilities) - [x] Test strong vs weak teams - [x] Test extreme Elo differences - [x] Test probability sums to 1.0 - [x] Integration test with realistic NHL Elo ratings - [x] **5.2.2b** ICM calculator ✅ **NEW - COMPLETE (Monte Carlo approach)** - [x] `app/services/icm-calculator.ts` - [x] `calculateICM()` - Monte Carlo simulation (100,000 iterations) - [x] `simulateTournament()` - Weighted random selection for one tournament - [x] `calculateICMFromOdds()` - Complete pipeline from odds - [x] `icmResultToArray()` - Convert to array format - [x] Works with ANY number of participants (not just 8) - [x] Algorithm: Weighted random draws, remove winners, repeat for all positions - [x] **Performance**: 2-5 seconds for 30 teams (vs. minutes/timeouts with exact recursion) - [x] **Memory**: Minimal usage (no caching needed) - [x] **Accuracy**: Converges to true ICM probabilities with sufficient iterations - [x] Unit tests (13 tests passing) ✅ - [x] Test equal participants - [x] Test strong vs weak teams - [x] Test 32-team NHL scenario - [x] Test probability ordering - [x] Integration test with realistic NHL data - [x] **Implementation Note**: Initially attempted exact recursive ICM algorithm, but hit memory limits (Map size exceeded) with 30 teams. Switched to Monte Carlo simulation which is faster, memory-efficient, and produces equivalent results. - [x] **5.2.3** Admin UI - Futures odds entry ✅ **UPDATED to use ICM** - [x] Route: `/admin/sports-seasons/:id/futures-odds` - [x] For each participant: enter American odds - [x] "Generate Preview" button - [x] Runs ICM Monte Carlo simulation (100,000 iterations) - [x] Preview P(1st) through P(8th) for each participant - [x] Shows results sorted by championship probability - [x] "Save Probabilities" button → save to database - [x] **Critical fix**: Passes preview results through hidden fields (no re-simulation) - [x] Saved probabilities match preview exactly (no random variation) - [x] Uses `upsertParticipantEV()` without normalization (ICM is already correct) - [x] Works with ALL participants in sports season - [x] Probabilities correctly sum to <1.0 for weak teams (low chance of top 8) - [x] Validation updated: allows sum ≤1.0 for futures_odds, requires sum=1.0 only for manual entry - [x] Added navigation from sports season detail page - [ ] **5.2.4** Sports betting API research & integration (optional) - **DEFERRED** - Not needed for MVP - manual entry works well - Can be added in future phase if needed - [ ] Research API options: - [ ] The Odds API (theoddsapi.com) - $79/mo, good coverage - [ ] API-Sports (api-sports.io) - Tiered pricing - [ ] RapidAPI sports betting endpoints - [ ] If API selected: - [ ] Implement API client - [ ] Map API participant names to our participants - [ ] Weekly cron job to fetch and update odds - [ ] Error handling and logging **Phase 5.2 Status**: ✅ **COMPLETE** (All tasks done, ICM Monte Carlo approach adopted) **Test Coverage**: 97 tests passing (33 EV + 38 probability-engine + 13 bracket-simulator + 13 ICM) **Deliverables**: - ✅ ICM calculator for ANY number of participants using Monte Carlo simulation ⭐ **Key Feature** - ✅ Fast and memory-efficient: 2-5 seconds for 30 teams, 100,000 iterations - ✅ Weighted random selection algorithm (pick 1st, remove, pick 2nd, etc.) - ✅ Preview results pass through to save (no re-simulation, exact match) - ✅ Correct handling of weak teams: probabilities sum to <1.0 (e.g., 24% for 20th-ranked team) - ✅ Validation updated: futures_odds allows sum ≤1.0, manual entry requires sum=1.0 - ✅ No normalization for ICM results (already mathematically correct) - ✅ Odds conversion (American/Decimal) - ✅ Admin UI for futures odds entry with ICM preview - ✅ Integration with existing EV system - ✅ Comprehensive test coverage - ✅ Elo/Bracket simulator preserved for future hybrid approach ### Phase 5.3: Real Results Integration ✅ **COMPLETE** **Goal**: Update probabilities when results come in - [x] **5.3.1** Probability update service ✅ **COMPLETE** - [x] `app/services/probability-updater.ts` - [x] `updateProbabilitiesAfterResult(sportsSeasonId)` - Updates probabilities after results - [x] `previewProbabilityUpdate(sportsSeasonId)` - Preview changes before applying - [x] For finished participants: set to 100% at their placement, 0% elsewhere - [x] For eliminated participants (finalPosition = 0): set to 0% for all positions - [x] For unfinished participants: re-run ICM calculation with remaining participants - [x] Unit tests (5 tests passing) - [x] Elimination tracking in playoff brackets - [x] When playoff bracket is processed, participants NOT in the bracket are marked as eliminated - [x] `participantResults` created with `finalPosition = 0` for non-playoff teams - [x] Ensures eliminated teams get 0% probabilities automatically - [x] **5.3.2** Partial results handling ✅ **COMPLETE** **Implementation**: Generic approach works for all sport types - [x] Finished participants get 100% at their final position (works for all types) - [x] Unfinished participants recalculated using ICM with remaining competition - [x] Works seamlessly with: - **Playoffs**: Eliminated teams get 100% at final position, remaining teams recalculated - **Season Standings (F1)**: Teams with final results locked in, others recalculated - **Qualifying Points (Golf/Tennis)**: Participants with final placements locked, others recalculated - [x] **5.3.3** Admin trigger ✅ **COMPLETE** - [x] Route: `/admin/sports-seasons/:id/recalculate-probabilities` - [x] Shows summary: finished participants, unfinished participants, changes to apply - [x] Table showing before/after probabilities for all changed participants - [x] "Apply Changes" button runs `updateProbabilitiesAfterResult()` - [x] Success/error feedback - [x] Navigation link added to sports season detail page - [x] **5.3.4** Automatic triggers ✅ **COMPLETE** - [x] When playoff bracket event completed → auto-recalculate (`processPlayoffBracket`) - [x] When season standings processed → auto-recalculate (`processSeasonStandings`) - [x] When qualifying points finalized → auto-recalculate (`finalizeQualifyingPoints`) - [x] Error handling with console logging (doesn't block result processing) **Phase 5.3 Status**: ✅ **COMPLETE** **Test Coverage**: 5 tests passing (probability-updater) **Deliverables**: - ✅ Probability updater service with automatic recalculation - ✅ Generic partial results handling for all sport types - ✅ Elimination tracking for playoff brackets (non-playoff teams → 0% probabilities) - ✅ Admin UI for manual recalculation with preview - ✅ Automatic triggers integrated into scoring calculator - ✅ Comprehensive error handling and logging ### Phase 5.4: Projected Totals Display ✅ **COMPLETE** **Goal**: Show "Projected Points" to league members - [x] **5.4.1** Update standings snapshot calculation ✅ **COMPLETE** - [x] Modified `recalculateStandings()` in `app/models/scoring-calculator.ts` - [x] Created `calculateTeamProjectedScore()` function - [x] For each team: - [x] Calculate `actualPoints` (finished participants only) - [x] Calculate EVs for unfinished participants - [x] `projectedPoints = actualPoints + sum(EVs)` - [x] Store in `team_standings` and `team_standings_snapshots` - [x] Added database migration (0027_add_projected_points_to_team_standings.sql) - [x] **5.4.2** Standings page updates ✅ **COMPLETE** - [x] Route: `/leagues/:leagueId/standings/:seasonId` - [x] Added "Projected Points" column - [x] Shows actual vs projected with "+X.X EV" indicator - [x] Visual indicator: "X participants remaining" - [x] Added tooltip explanation in info card - [x] **5.4.3** Team breakdown page updates ✅ **COMPLETE** - [x] Route: `/leagues/:leagueId/standings/:seasonId/teams/:teamId` - [x] For each participant: - [x] If finished: show actual points - [x] If not finished: show "Projected: X.X points" - [x] Totals section: - [x] Actual Points card - [x] Projected Points card with EV indicator - [x] Participants count - [x] Updated `getTeamScoreBreakdown()` to fetch and calculate EVs - [~] **5.4.4** Participant detail page ⏭️ **SKIPPED** - Not needed for MVP - participants can be viewed through team breakdown - Can be added in future phase if needed - [~] Route: `/leagues/:leagueId/participants/:participantId` - [~] Show participant info - [~] Current status (finished or in progress) - [~] If finished: actual placement and points - [~] If not finished: projected points (league-specific) - [~] Ownership: which teams in this league own this participant **Phase 5.4 Status**: ✅ **COMPLETE** (3/3 required tasks) **Deliverables**: - ✅ Projected points calculation integrated into standings - ✅ StandingsTable component shows actual and projected points - ✅ Team breakdown page shows projected points for unfinished participants - ✅ Summary cards display actual vs projected totals - ✅ League-specific EV calculation (uses each league's scoring rules) - ✅ Database schema updated with migration - ✅ Model layers updated to expose projected points ### Phase 5.5: Draft Integration **Goal**: Use EV to sort available participants during draft - [ ] **5.5.1** Draft participant sorting - [ ] Update `app/models/draft.ts` or create `app/models/draft-rankings.ts` - [ ] `getAvailableParticipantsForDraft(seasonId)` - current function - [ ] Modify to include EV calculation - [ ] Sort by EV (descending) as default order - [ ] Respect manual draft queue FIRST (never override) - [ ] **5.5.2** Autodraft logic - [ ] Update `app/services/autodraft.ts` (or wherever autodraft lives) - [ ] When autodraft picks: 1. Check manual draft queue first 2. If queue empty: pick highest EV available participant 3. Account for already-owned participants - [ ] Log autodraft decisions - [ ] **5.5.3** Testing autodraft - [ ] Unit tests: autodraft picks highest EV when queue empty - [ ] Unit tests: autodraft respects queue over EV - [ ] Integration test: full draft with autodraft teams ### Phase 5.6: Daily Update Job **Goal**: Keep probabilities and EVs fresh - [ ] **5.6.1** Scheduled job setup - [ ] Create `server/probability-updater.ts` - [ ] Daily cron job (runs at 3 AM or configurable) - [ ] Get all active/upcoming seasons - [ ] For each sports season in those seasons: - [ ] If has probabilities source (API): fetch new odds - [ ] Re-calculate probabilities - [ ] Update `participant_probabilities` table - [ ] **5.6.2** Logging & monitoring - [ ] Log job start/completion - [ ] Log number of probabilities updated - [ ] Log any errors or API failures - [ ] Alert if job fails - [ ] **5.6.3** Manual trigger - [ ] Admin route: `/admin/update-probabilities` - [ ] "Run Update Now" button - [ ] Show progress/results --- ## Sports Betting API Research Here are the best options I found for fetching futures odds: ### Option 1: The Odds API (Recommended) - **Website**: theoddsapi.com - **Pricing**: $79/month for 10,000 requests - **Coverage**: NFL, NBA, MLB, NHL, Golf, Tennis, F1, AFL - **Pros**: - Simple REST API - Futures markets included - Good documentation - Free tier (500 requests/month) for testing - **Cons**: - Need to map their participant IDs to ours - Some niche sports not covered ### Option 2: API-Sports - **Website**: api-sports.io - **Pricing**: $30-300/month depending on sport - **Coverage**: Individual APIs per sport - **Pros**: - Very comprehensive data - Historical data available - Includes Elo ratings for some sports - **Cons**: - More expensive for multiple sports - Steeper learning curve ### Option 3: Manual Entry (MVP) - **Pricing**: Free - **Coverage**: Whatever you enter - **Pros**: - Total control - No API dependency - Can use any source (ESPN, Vegas, etc.) - **Cons**: - Time-consuming - Human error risk - No automation **My recommendation**: Start with **manual entry** for MVP, add **The Odds API** in Phase 5.2.4 when you're ready to scale. --- ## Testing Strategy ### Unit Tests - [ ] Odds conversion functions (20 tests) - [ ] Probability distribution algorithms (15 tests) - [ ] EV calculation (15 tests) - [ ] Projected totals calculation (10 tests) - [ ] Probability validation (10 tests) ### Integration Tests - [ ] End-to-end: Enter futures odds → generate probabilities → calculate EVs → display projected points (5 tests) - [ ] Real results update flow (5 tests) - [ ] Autodraft with EV ranking (3 tests) ### Manual Testing Checklist - [ ] Enter probabilities for NFL playoffs manually - [ ] Verify EVs calculate correctly for different league scoring rules - [ ] Enter results, verify probabilities update - [ ] Verify finished participants have EV = actual points - [ ] Test projected totals on standings page - [ ] Test autodraft picks highest EV --- ## Performance Considerations Based on your answers and my analysis: ### What's Fast (No Caching Needed) - ✅ EV calculation (8 multiplications) - ~0.05ms per participant - ✅ On-demand calculation is faster than cache lookup - ✅ 100 participants × 50 leagues = 5000 EVs = ~0.25 seconds total ### What's Slow (Cache It) - ❌ Monte Carlo simulations (100k iterations) - ~5-30 seconds - ❌ API calls to fetch futures odds - ~1-2 seconds - ❌ Database queries to fetch all probabilities - ~50ms ### Caching Strategy **1. Probability Distributions** (stored in DB, not in-memory cache) - Updated weekly via cron job - Invalidate when admin manually updates **2. Projected Totals** (pre-calculated in snapshots) - Calculated with daily standings snapshots - Stored in `team_standings_snapshots.projectedPoints` - No real-time calculation needed **3. Draft Participant Rankings** (in-memory cache, 5-minute TTL) - Cache sorted list of available participants with EVs - Invalidate when pick is made - Invalidate every 5 minutes (in case probabilities updated) ### Database Indexes ```sql CREATE INDEX idx_participant_probabilities_lookup ON participant_probabilities(participantId, sportsSeasonId); CREATE INDEX idx_participant_probabilities_sports_season ON participant_probabilities(sportsSeasonId); ``` --- ## Migration Path: Simple → Complex ### Phase 5.1 (Week 1-2): Manual Entry MVP - Admin enters probabilities by hand - System calculates EVs - Display projected points - **Deliverable**: Working system for 1 sport (NFL playoffs) ### Phase 5.2 (Week 3): Futures Odds Conversion - Admin enters futures odds (not probabilities) - System converts to P(1st-8th) - Still manual, but easier than calculating percentages - **Deliverable**: Futures → probabilities working ### Phase 5.3 (Week 4): Real Results Integration - When results entered, probabilities auto-update - Partial results + simulation for remaining events - **Deliverable**: Dynamic probability updates ### Phase 5.4 (Week 5): UI Polish - Projected points on all pages - Better visualization - Tooltips and help text - **Deliverable**: User-facing features complete ### Phase 5.5 (Week 6): Draft Integration - EV-based sorting - Autodraft uses EV - **Deliverable**: Draft room integration ### Phase 5.6 (Week 7): Automation - API integration for futures odds - Daily update job - Expand to 2nd sport (Golf) - **Deliverable**: Automated system ### Phase 5.7+ (Future): Advanced Models - Elo-based simulations for F1, Tennis - Performance models - Machine learning predictions - VORP (Value Over Replacement Player) --- ## Validation Data (NHL - November 2025) ### Futures Odds (Championship) ``` Colorado Avalanche: +550 (15.4%) Florida Panthers: +800 (11.1%) Vegas Golden Knights: +800 (11.1%) Tampa Bay Lightning: +1000 (9.1%) New Jersey Devils: +1400 (6.7%) Toronto Maple Leafs: +2500 (3.8%) NY Rangers: +4000 (2.4%) Detroit Red Wings: +7500 (1.3%) Vancouver Canucks: +7500 (1.3%) NY Islanders: +15000 (0.66%) Nashville Predators: +40000 (0.24%) ``` ### Game Lines (November 16, 2025) ``` Colorado (-270, 73.0%) vs NY Islanders (+220, 31.3%) Tampa Bay (-170, 63.0%) vs Vancouver (+145, 40.8%) Vegas (-130, 56.5%) vs Minnesota (+110, 47.6%) NY Rangers (-145, 59.2%) vs Detroit (+125, 44.4%) Pittsburgh (-125, 55.6%) vs Nashville (+105, 48.8%) ``` ### Calibration Goals - Predict game lines within ±5% of actual betting odds - Use these 5 matchups to calibrate Elo transformation - Validate that approach works across wide range (favorites vs underdogs) ## Elo Conversion Algorithm (Empirically Calibrated) ### Algorithm Overview **Problem**: Championship futures are compressed over multiple rounds. We need to "decompress" them to single-game strength. **Solution**: Power transformation + Elo mapping ```typescript // Step 1: Convert American odds to championship probability function americanOddsToProb(odds: number): number { return 1 / (1 + Math.abs(odds) / 100); } // Step 2: Apply power transformation to decompress // Exponent empirically calibrated to match game lines function decompressProbability( championshipProb: number, exponent: number = 0.33 // Cube root, configurable ): number { return Math.pow(championshipProb * 100, exponent); } // Step 3: Normalize to Elo scale function mapToElo( strength: number, minStrength: number, maxStrength: number, eloMin: number = 1250, eloMax: number = 1750 ): number { const normalized = (strength - minStrength) / (maxStrength - minStrength); return eloMin + (normalized * (eloMax - eloMin)); } // Step 4: Calculate head-to-head probability function eloWinProbability(eloA: number, eloB: number): number { return 1 / (1 + Math.pow(10, (eloB - eloA) / 400)); } ``` ### Calibration Process **Phase 5.1.6**: Build calibration tool - Input: 5+ matchups with futures odds and actual game lines - Adjust `exponent` parameter until predictions match actuals - Target: ±5% average error - Store calibrated parameters per sport ### Example Calculation **Colorado vs NY Islanders:** ```typescript // Futures const colOdds = 550; // 15.4% championship const nyiOdds = 15000; // 0.66% championship // Step 1: Championship probabilities const colChamp = 0.154; const nyiChamp = 0.0066; // Step 2: Decompress with cube root const colStrength = Math.pow(15.4, 0.33) = 2.49; const nyiStrength = Math.pow(0.66, 0.33) = 0.87; // Step 3: Map to Elo (assume min=0.5, max=3.0 for all teams) const colElo = 1250 + ((2.49 - 0.5) / (3.0 - 0.5)) * 500 = 1648; const nyiElo = 1250 + ((0.87 - 0.5) / (3.0 - 0.5)) * 500 = 1324; // Step 4: Win probability const eloDiff = 1648 - 1324 = 324; const winProb = 1 / (1 + 10^(-324/400)) = 82.8% // Actual line: 73.0% // Error: 9.8% (needs calibration adjustment) ``` By adjusting the exponent from 0.33 to ~0.38, we can get closer to 73%. ## Implementation Plan Updates ### Phase 5.1 (MVP - Manual Entry) - No changes (manual probability entry) ### Phase 5.2 (Elo-Based Simulation) **Updated task list:** - [ ] **5.2.1** Elo conversion with calibration - [ ] `convertAmericanOddsToProbability(odds)` - +500 → 16.7% - [ ] `decompressProbability(prob, exponent)` - Apply power transformation - [ ] `mapToElo(strength, params)` - Normalize to Elo scale - [ ] `eloWinProbability(eloA, eloB)` - Standard Elo formula - [ ] Make exponent and Elo range configurable per sport - [ ] Unit tests (20+ tests with known values) - [ ] **5.2.2** Calibration tool (NEW) - [ ] Admin route: `/admin/sports-seasons/:id/calibrate` - [ ] Enter actual game matchups with betting lines - [ ] Calculate our predicted probabilities - [ ] Show comparison table (predicted vs actual) - [ ] Adjust exponent slider → see predictions update live - [ ] Save calibrated parameters to `sportsSeasons` table - [ ] Display accuracy metrics (avg error, max error) - [ ] **5.2.3** Bracket simulator - [ ] `initializeBracket(teams, format)` - NHL/NFL structure - [ ] `simulatePlayoffs(teams, sims)` - Monte Carlo with Elo - [ ] `recordPlacements(bracket)` - Track 1st, 2nd, 3-4, 5-8 - [ ] 100,000 simulations (configurable) - [ ] Progress indicator for long simulations - [ ] **5.2.4** Admin UI - Futures odds entry - [ ] Route: `/admin/sports-seasons/:id/futures-odds` - [ ] For each participant: enter American odds (+550, etc.) - [ ] "Generate Probabilities" button - [ ] Shows preview: Team → Elo → P(1st-8th) - [ ] Runs simulation with calibrated parameters - [ ] Confirm to save to `participant_probabilities` ### New Database Fields Add to `sportsSeasons` table: ```typescript sportsSeasons { // ... existing fields // EV Calibration parameters eloCalibrationExponent: decimal(3,2) (nullable) // e.g., 0.33 eloMinRating: integer (default: 1250) eloMaxRating: integer (default: 1750) } ``` ## Ready to Start? **Summary of Decisions:** ✅ **Sport**: NHL Playoffs ✅ **Approach**: Elo-based with empirical calibration ✅ **Odds format**: American odds (primary), conversion tools for others ✅ **Validation data**: 5 NHL matchups from ESPN ✅ **Performance**: Pre-calculate projected totals with snapshots ✅ **Monte Carlo**: 100k simulations of entire tournament **Next step**: I'll start with Phase 5.1.1 (database schema) once you confirm this approach makes sense! The key insight: We'll use the NHL validation data to calibrate the power transformation exponent, ensuring our Elo-based predictions match real betting lines accurately.