* Update claude.md to push more tests. * Archiving old plans. * fix: run DB migrations programmatically on server startup Replace unreliable drizzle-kit CLI migration with drizzle-orm's built-in migrator running in the server process before accepting connections. This ensures migrations are always applied on deploy and fails fast if they error. - Add runMigrations() to server.ts using drizzle-orm/postgres-js/migrator - Skip migrations in development (handled manually via db:migrate) - Add DATABASE_URL guard with a clear error message - Remove start:production script (now identical to start) - Update Dockerfile CMD to use npm run start Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
41 KiB
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:
- Direct conversion to probabilities (simpler)
- Can be manually entered if API not available
- Works for all sports (most have championship futures)
- 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:
-
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
-
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
-
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
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_valuestable (renamed fromparticipant_probabilities) - Add
actualPoints,projectedPoints,participantsFinishedtoteam_standings_snapshots - Create migration (0024_faithful_mesmero.sql)
- Add Drizzle schema definitions
- Add
sourceenum: manual, futures_odds, elo_simulation, performance_model - Add calibration fields to
sportsSeasons: eloCalibrationExponent, eloMinRating, eloMaxRating
- Create
-
5.1.2 Core calculation functions ✅ COMPLETE
app/services/ev-calculator.tscalculateEV(probabilities, scoringRules)- pure functionvalidateProbabilities(probabilities, tolerance)- validationnormalizeProbabilities(probabilities)- auto-fix rounding errorscalculateProjectedTotal(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.tsupsertParticipantEV()- insert/update with validationupsertParticipantEVWithNormalization()- auto-normalizegetParticipantEV()- get one participantgetAllParticipantEVsForSeason()- get all for a seasondeleteParticipantEV()- delete recordbatchUpsertParticipantEVs()- batch operations (50 at a time)recalculateEV()- update EV with new scoring rulesrecalculateAllEVsForSeason()- bulk recalculationtoProbabilityDistribution()- 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
- Route:
-
[~] 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 formatValidate CSV rowsPreview before importBulk 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.tsconvertAmericanOddsToProbability(odds)- +500 → 16.7%convertDecimalOddsToProbability(odds)- 6.00 → 16.7%normalizeProbabilities(probs)- ensure sum to 100%decompressProbability(prob, exponent)- Apply power transformationmapToElo(strength, params)- Normalize to Elo scaleeloWinProbability(eloA, eloB)- Standard Elo formulaconvertFuturesToElo()- Complete pipelinecalculatePredictionError()- 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.tssimulateBracket()- Async with progress callbackssimulateBracketSync()- Synchronous versionsimulate8TeamBracket()- 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.tscalculateICM()- Monte Carlo simulation (100,000 iterations)simulateTournament()- Weighted random selection for one tournamentcalculateICMFromOdds()- Complete pipeline from oddsicmResultToArray()- 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
- Route:
-
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.tsupdateProbabilitiesAfterResult(sportsSeasonId)- Updates probabilities after resultspreviewProbabilityUpdate(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
participantResultscreated withfinalPosition = 0for 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
- Route:
-
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)
- When playoff bracket event completed → auto-recalculate (
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
-
5.4.1 Update standings snapshot calculation ✅ COMPLETE
- Modified
recalculateStandings()inapp/models/scoring-calculator.ts - Created
calculateTeamProjectedScore()function - For each team:
- Calculate
actualPoints(finished participants only) - Calculate EVs for unfinished participants
projectedPoints = actualPoints + sum(EVs)
- Calculate
- Store in
team_standingsandteam_standings_snapshots - Added database migration (0027_add_projected_points_to_team_standings.sql)
- Modified
-
5.4.2 Standings page updates ✅ COMPLETE
- Route:
/leagues/:leagueId/standings/:seasonId - Added "Projected Points" column
- Shows actual vs projected with "+X.X EV" indicator
- Visual indicator: "X participants remaining"
- Added tooltip explanation in info card
- Route:
-
5.4.3 Team breakdown page updates ✅ COMPLETE
- 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 card
- Projected Points card with EV indicator
- Participants count
- Updated
getTeamScoreBreakdown()to fetch and calculate EVs
- Route:
-
[~] 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.tsor createapp/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)
- Update
-
5.5.2 Autodraft logic
- Update
app/services/autodraft.ts(or wherever autodraft lives) - When autodraft picks:
- Check manual draft queue first
- If queue empty: pick highest EV available participant
- Account for already-owned participants
- Log autodraft decisions
- Update
-
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_probabilitiestable
- Create
-
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
- Admin route:
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
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
exponentparameter 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 transformationmapToElo(strength, params)- Normalize to Elo scaleeloWinProbability(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
sportsSeasonstable - Display accuracy metrics (avg error, max error)
- Admin route:
-
5.2.3 Bracket simulator
initializeBracket(teams, format)- NHL/NFL structuresimulatePlayoffs(teams, sims)- Monte Carlo with ElorecordPlacements(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
- Route:
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.