- Added comprehensive implementation plan for Phase 5, including executive summary, clarifications, recommended architecture, task breakdown, and performance considerations. - Defined database schema changes for participant probabilities and expected values. - Outlined service layer architecture for probability generation, EV calculation, and probability updates. - Included detailed task breakdown for each sub-phase, focusing on manual entry, futures odds integration, real results integration, projected totals display, draft integration, and daily update job. - Incorporated validation data and calibration goals for NHL futures odds. - Updated planning questions and answers based on previous discussions to refine the approach for EV calculations and autodraft integration.
30 KiB
Phase 5: Expected Value System - Detailed Implementation Plan
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
- Create
participant_probabilitiestable - Add
actualPoints,projectedPointstoteam_standings_snapshots - Create migration
- Add Drizzle schema definitions
- Create
-
5.1.2 Core calculation functions
app/services/ev-calculator.tscalculateEV(probabilities, scoringRules)- pure functioncalculateAllEVsForLeague(seasonId, sportsSeasonId)- batch calculationcalculateProjectedTotal(teamId, seasonId)- team projection
- Unit tests (15+ tests)
- Test EV calculation with different scoring rules
- Test with finished participants (EV = actual points)
- Test projected totals (actual + EVs)
-
5.1.3 Probability storage model
app/models/participant-probability.tscreateProbability()- insert new probabilitiesupdateProbability()- update existinggetProbabilitiesForSportsSeason()- get all for a sportgetProbabilityForParticipant()- get one participant- Validation: probabilities sum to 100% (±0.1% tolerance)
- Unit tests (10+ tests)
-
5.1.4 Admin UI - Manual probability entry
- Route:
/admin/sports-seasons/:id/probabilities - List all participants in sports season
- For each participant: 8 input fields (P(1st) through P(8th))
- Live validation: sum must equal 100%
- Auto-distribute remaining probability
- Save button →
createProbability()orupdateProbability() - "Recalculate EVs" button (manual trigger)
- Route:
-
5.1.5 Admin UI - Bulk import
- CSV upload format:
Participant Name, P(1st), P(2nd), ..., P(8th) Chiefs, 20.5, 15.3, ..., 2.1 - Validate CSV rows
- Preview before import
- Bulk insert probabilities
- CSV upload format:
Phase 5.2: Futures Odds Integration
Goal: Convert Vegas futures to probabilities automatically
-
5.2.1 Odds conversion logic
app/services/probability-engine.tsconvertAmericanOddsToProbability(odds)- +500 → 16.7%convertDecimalOddsToProbability(odds)- 6.00 → 16.7%normalizeProbabilities(probs)- ensure sum to 100%distributeProbabilitiesToPlacements(winProbs)- championship win % → P(1st-8th)
- Unit tests (20+ tests)
- Test odds conversions (American, Decimal)
- Test normalization (handle bookmaker vig)
- Test distribution algorithms
-
5.2.2 Distribution algorithms
For playoffs (single elimination):
- Championship odds → P(1st)
- Simulate bracket → P(2nd), P(3rd/4th), P(5th-8th)
For season standings (F1):
- Championship odds → P(1st)
- Apply statistical distribution for P(2nd-8th) based on strength
For qualifying points (Golf):
- Major win odds for each of 4 majors
- Simulate QP accumulation
- Convert QP standings → P(1st-8th)
-
5.2.3 Admin UI - Futures odds entry
- Route:
/admin/sports-seasons/:id/futures-odds - For each participant: enter futures odds
- Dropdown: American odds / Decimal odds
- "Generate Probabilities" button
- Runs conversion + distribution algorithm
- Preview P(1st-8th) for each participant
- Confirm → save to
participant_probabilities
- Route:
-
5.2.4 Sports betting API research & integration (optional)
- 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
- Research API options:
Phase 5.3: Real Results Integration
Goal: Update probabilities when results come in
-
5.3.1 Probability update service
app/services/probability-updater.tsupdateProbabilitiesAfterResult(sportsSeasonId)- For finished participants: set to 100% at their placement, 0% elsewhere
- For unfinished participants: re-run simulation/calculation
-
5.3.2 Partial results handling
Qualifying Points (Golf/Tennis):
- Get actual QP from completed majors
- Simulate remaining majors
- Combine actual + simulated QP → final standings probabilities
Playoffs:
- Eliminated teams → 0% for better placements
- Remaining teams → re-simulate from current bracket state
Season Standings (F1):
- Get actual championship points so far
- Simulate remaining races
- Combine to get final standings probabilities
-
5.3.3 Admin trigger
- "Recalculate Probabilities" button on event completion page
- Runs
updateProbabilitiesAfterResult() - Shows before/after probabilities
- Updates all affected participant probabilities
-
5.3.4 Automatic triggers (optional)
- When event marked complete → auto-recalculate
- When participant result entered → auto-recalculate
- Background job (if expensive, queue it)
Phase 5.4: Projected Totals Display
Goal: Show "Projected Points" to league members
-
5.4.1 Update standings snapshot calculation
- Modify
recalculateStandings()inapp/models/scoring-calculator.ts - For each team:
- Calculate
actualPoints(finished participants only) - Calculate EVs for unfinished participants
projectedPoints = actualPoints + sum(EVs)
- Calculate
- Store in
team_standings_snapshots
- Modify
-
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"
- Route:
-
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
- Route:
-
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
- Route:
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.