brackt/plans/phase-5-detailed-implementation-plan.md
Chris Parsons 840212c4f8 feat: implement sports season expected value calculations and probability updates
- Added loader and action functions for managing expected values in sports seasons.
- Implemented UI for recalculating probabilities based on participant results.
- Created a service to update probabilities after results are finalized, including handling finished and unfinished participants.
- Developed tests for the probability updater service to ensure correct functionality.
- Introduced a preview feature to show potential changes before applying updates.
2025-11-21 22:05:50 -08:00

39 KiB
Raw Blame History

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

Total Test Coverage: 105/105 tests passing (33 EV + 51 Elo/Bracket + 13 ICM + 8 probability-updater)

Ready to Test: Yes!

  • Manual entry: /admin/sports-seasons/:id/expected-values
  • Futures odds (ICM): /admin/sports-seasons/:id/futures-odds Works with all teams! Monte Carlo simulation (2-5 sec)
  • Recalculate: /admin/sports-seasons/:id/recalculate-probabilities NEW - Auto-updates when results entered!

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:


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.


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

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

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

// Core probability generation
async function generateProbabilitiesFromFutures(
  sportsSeasonId: string,
  futuresOdds: { participantId: string; odds: number }[]
): Promise<void>

// 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<Map<string, number[]>>

New Module: app/services/ev-calculator.ts

// 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<Map<string, number>>  // 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

// Update probabilities based on real results
async function updateProbabilitiesAfterResult(
  sportsSeasonId: string
): Promise<void>

// Re-simulate remaining events with actual results locked in
async function resimulateWithPartialResults(
  sportsSeasonId: string,
  completedEvents: string[],
  remainingEvents: string[]
): Promise<void>

Phase 5 Task Breakdown (Revised)

Phase 5.1: Foundation & Manual Entry (MVP)

Goal: Admin can manually enter probabilities, system calculates EVs

  • 5.1.1 Database schema COMPLETE

    • Create participant_expected_values table (renamed from participant_probabilities)
    • Add actualPoints, projectedPoints, participantsFinished to team_standings_snapshots
    • Create migration (0024_faithful_mesmero.sql)
    • Add Drizzle schema definitions
    • Add source enum: manual, futures_odds, elo_simulation, performance_model
    • Add calibration fields to sportsSeasons: eloCalibrationExponent, eloMinRating, eloMaxRating
  • 5.1.2 Core calculation functions COMPLETE

    • app/services/ev-calculator.ts
      • calculateEV(probabilities, scoringRules) - pure function
      • validateProbabilities(probabilities, tolerance) - validation
      • normalizeProbabilities(probabilities) - auto-fix rounding errors
      • calculateProjectedTotal(actualPoints, remainingEVs) - team projection
    • Unit tests (20 tests, all passing)
      • Test EV calculation with different scoring rules
      • Test with equal probabilities, favorites, underdogs
      • Test validation (valid/invalid sums)
      • Test normalization (>100%, <100%, =100%)
      • Test projected totals
  • 5.1.3 Probability storage model COMPLETE

    • app/models/participant-expected-value.ts
      • upsertParticipantEV() - insert/update with validation
      • upsertParticipantEVWithNormalization() - auto-normalize
      • getParticipantEV() - get one participant
      • getAllParticipantEVsForSeason() - get all for a season
      • deleteParticipantEV() - delete record
      • batchUpsertParticipantEVs() - batch operations (50 at a time)
      • recalculateEV() - update EV with new scoring rules
      • recalculateAllEVsForSeason() - bulk recalculation
      • toProbabilityDistribution() - type conversion helper
      • Validation: probabilities sum to 100% (±0.1% tolerance)
    • Unit tests (13 documentation tests, all passing)
  • 5.1.4 Admin UI - Manual probability entry COMPLETE

    • Route: /admin/sports-seasons/:id/expected-values
    • List all participants in sports season
    • For each participant: 8 input fields (P(1st) through P(8th))
    • Validation: probabilities sum to 100%
    • Save button → upsertParticipantEV()
    • Display calculated EV in table
    • Auto-populate with existing EVs (or 12.5% equal distribution)
    • Success/error feedback messages
    • Navigation: Added "Expected Values" card to sports season detail page
    • 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.

  • 5.2.1 Elo conversion with calibration COMPLETE

    • app/services/probability-engine.ts
      • convertAmericanOddsToProbability(odds) - +500 → 16.7%
      • convertDecimalOddsToProbability(odds) - 6.00 → 16.7%
      • normalizeProbabilities(probs) - ensure sum to 100%
      • decompressProbability(prob, exponent) - Apply power transformation
      • mapToElo(strength, params) - Normalize to Elo scale
      • eloWinProbability(eloA, eloB) - Standard Elo formula
      • convertFuturesToElo() - Complete pipeline
      • calculatePredictionError() - For calibration
    • Unit tests (38 tests passing)
      • Test odds conversions (American, Decimal)
      • Test normalization (handle bookmaker vig)
      • Test Elo conversion functions
      • Test prediction error calculation
      • Integration test with NHL example data
  • 5.2.2 Bracket simulator COMPLETE

    • app/services/bracket-simulator.ts
      • simulateBracket() - Async with progress callbacks
      • simulateBracketSync() - Synchronous version
      • simulate8TeamBracket() - NHL/NFL format
      • Support for tied placements (3-4, 5-8)
    • Unit tests (13 tests passing)
      • Test equal ratings (equal probabilities)
      • Test strong vs weak teams
      • Test extreme Elo differences
      • Test probability sums to 1.0
      • Integration test with realistic NHL Elo ratings
  • 5.2.2b ICM calculator NEW - COMPLETE (Monte Carlo approach)

    • app/services/icm-calculator.ts
      • calculateICM() - Monte Carlo simulation (100,000 iterations)
      • simulateTournament() - Weighted random selection for one tournament
      • calculateICMFromOdds() - Complete pipeline from odds
      • icmResultToArray() - Convert to array format
      • Works with ANY number of participants (not just 8)
      • Algorithm: Weighted random draws, remove winners, repeat for all positions
      • Performance: 2-5 seconds for 30 teams (vs. minutes/timeouts with exact recursion)
      • Memory: Minimal usage (no caching needed)
      • Accuracy: Converges to true ICM probabilities with sufficient iterations
    • Unit tests (13 tests passing)
      • Test equal participants
      • Test strong vs weak teams
      • Test 32-team NHL scenario
      • Test probability ordering
      • Integration test with realistic NHL data
    • 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.
  • 5.2.3 Admin UI - Futures odds entry UPDATED to use ICM

    • Route: /admin/sports-seasons/:id/futures-odds
    • For each participant: enter American odds
    • "Generate Preview" button
      • Runs ICM Monte Carlo simulation (100,000 iterations)
      • Preview P(1st) through P(8th) for each participant
      • Shows results sorted by championship probability
    • "Save Probabilities" button → save to database
      • Critical fix: Passes preview results through hidden fields (no re-simulation)
      • Saved probabilities match preview exactly (no random variation)
      • Uses upsertParticipantEV() without normalization (ICM is already correct)
    • Works with ALL participants in sports season
    • Probabilities correctly sum to <1.0 for weak teams (low chance of top 8)
    • Validation updated: allows sum ≤1.0 for futures_odds, requires sum=1.0 only for manual entry
    • 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

  • 5.3.1 Probability update service COMPLETE

    • app/services/probability-updater.ts
      • updateProbabilitiesAfterResult(sportsSeasonId) - Updates probabilities after results
      • previewProbabilityUpdate(sportsSeasonId) - Preview changes before applying
      • For finished participants: set to 100% at their placement, 0% elsewhere
      • For eliminated participants (finalPosition = 0): set to 0% for all positions
      • For unfinished participants: re-run ICM calculation with remaining participants
    • Unit tests (5 tests passing)
    • Elimination tracking in playoff brackets
      • When playoff bracket is processed, participants NOT in the bracket are marked as eliminated
      • participantResults created with finalPosition = 0 for non-playoff teams
      • Ensures eliminated teams get 0% probabilities automatically
  • 5.3.2 Partial results handling COMPLETE

    Implementation: Generic approach works for all sport types

    • Finished participants get 100% at their final position (works for all types)
    • Unfinished participants recalculated using ICM with remaining competition
    • 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
  • 5.3.3 Admin trigger COMPLETE

    • Route: /admin/sports-seasons/:id/recalculate-probabilities
    • Shows summary: finished participants, unfinished participants, changes to apply
    • Table showing before/after probabilities for all changed participants
    • "Apply Changes" button runs updateProbabilitiesAfterResult()
    • Success/error feedback
    • Navigation link added to sports season detail page
  • 5.3.4 Automatic triggers COMPLETE

    • When playoff bracket event completed → auto-recalculate (processPlayoffBracket)
    • When season standings processed → auto-recalculate (processSeasonStandings)
    • When qualifying points finalized → auto-recalculate (finalizeQualifyingPoints)
    • 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

Goal: Show "Projected Points" to league members

  • 5.4.1 Update standings snapshot calculation

    • Modify recalculateStandings() in app/models/scoring-calculator.ts
    • For each team:
      • Calculate actualPoints (finished participants only)
      • Calculate EVs for unfinished participants
      • projectedPoints = actualPoints + sum(EVs)
    • Store in team_standings_snapshots
  • 5.4.2 Standings page updates

    • Route: /leagues/:leagueId/standings/:seasonId
    • Add "Projected Points" column
    • Show actual vs projected
    • Visual indicator: "X participants remaining"
    • Tooltip: "Projected points = actual points + expected value of remaining participants"
  • 5.4.3 Team breakdown page updates

    • Route: /leagues/:leagueId/standings/:seasonId/teams/:teamId
    • For each participant:
      • If finished: show actual points
      • If not finished: show "Projected: X.X points"
    • Totals section:
      • Actual Points: XXX
      • Projected Points: XXX
      • Participants Remaining: X
  • 5.4.4 Participant detail page (new)

    • 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.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:

  • 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

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

// 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:

// 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:

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.