Commit graph

17 commits

Author SHA1 Message Date
Chris Parsons
06339fd4b6
Fix EPL simulator parity config (#421) 2026-05-13 15:02:23 -07:00
Chris Parsons
af64a29cfa
Fix NCAAW futures odds simulation and admin import UX (#420)
- Revert ncaaw-simulator to Barthag win probability formula; set
  realistic rating bounds (ratingMin: 0.70, ratingMax: 0.97) so derived
  ratings stay in the range where the formula behaves well
- Add batchSaveFuturesOddsForSimulator which clears all ratings (manual
  and generated) before upserting sourceOdds, so futures odds always
  drive the simulation rather than being silently overridden by existing
  Barthag ratings from Simulator Setup
- Add clearSourceOddsForParticipants to zero out both tables for
  participants excluded from a bulk import
- Add "Clear existing odds" checkbox to the bulk import card; applies
  client-side on match and server-side on submit
- Fix missing sportsSeasonId filter in batchSaveFuturesOddsForSimulator
  pre-clear UPDATE (could have wiped ratings across other seasons)
- Fix race condition: run batchSaveSourceOdds then
  batchSaveFuturesOddsForSimulator sequentially so the simulator inputs
  table always ends in the correct cleared state
- Fix Math.round in convertFuturesToElo collapsing Barthag-scale ratings
  to 0 or 1; Elo callers already round after clamping
- Handle all-identical-odds edge case in convertFuturesToElo (assign
  midpoint instead of throwing)
- Add missingRatingStrategy: worstKnownMinus to ncaaw_bracket manifest
  so fallbackRatingDelta is live config, not dead
- Log warning in resolveRatings when only 1 participant has odds
- Reset clearExisting checkbox after applyMatches

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:06:16 -07:00
Chris Parsons
c88ae6b745
Upgrade NCAA preseason simulators (#419) 2026-05-12 22:26:25 -07:00
Chris Parsons
e5295812f6
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.

Key additions:
- manifest.ts: per-simulator display names, default configs, required/
  optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
  projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
  supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
  derived inputs, normalises result columns, zeroes omitted participants,
  snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
  summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
  falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
  only copied when explicitly requested

Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
  participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
  readiness failure, empty results, and error recovery with status reset

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
Chris Parsons
79092f0409
Fix college hockey bracket resolution (#396) 2026-05-08 12:29:39 -07:00
Chris Parsons
5b03b22267
Add Brackt as a league-private meta-sport (#200) (#377)
Commissioners can add Brackt to any league. Each team drafts one manager
from their own league; at season end, that manager's final overall fantasy
ranking earns placement points. Resolution fires automatically when all
other sports finalize.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:31:44 -07:00
Chris Parsons
2848231235
Canonical tournament layer: schema + backfill (1/2) (#365)
* refactor(schema): rename per-window tables to season_* prefix

Renames participants, participant_expected_values, participant_qualifying_totals,
participant_results, participant_surface_elos to season_* prefixed names.
Renames event_results.participant_id to season_participant_id.
Phase 1a of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: rename participant.ts model file to season-participant.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(models): update model layer to use renamed schema exports

Updated all model files to use the renamed schema exports from Task 1:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantQualifyingTotals → seasonParticipantQualifyingTotals
- participantResults → seasonParticipantResults
- participantSurfaceElos → seasonParticipantSurfaceElos
- eventResults.participantId → eventResults.seasonParticipantId
- db.query relation accessors updated
- Relation field .participant → .seasonParticipant where applicable
- Import paths updated: ./participant → ./season-participant

Files updated (14 model files + 3 test files):
- draft-pick.ts
- draft-utils.ts
- event-result.ts
- group-stage-match.ts
- participant-result.ts
- qualifying-points.ts
- scoring-calculator.ts
- scoring-event.ts
- sports-season.ts
- surface-elo.ts
- team-score-events.ts
- cs2-major-stage.ts
- golf-skills.ts
- participant-expected-value.ts
- __tests__/sports-season.clone.test.ts
- __tests__/auto-pick.test.ts
- __tests__/executeAutoPick.timer.test.ts

Typecheck errors decreased: 779 → 499 (280 fewer)
All model file errors related to renamed schemas resolved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(routes): update route layer to use renamed schema exports

- Update model import from ~/models/participant to ~/models/season-participant
- Rename schema.participants to schema.seasonParticipants
- Rename schema.participantResults to schema.seasonParticipantResults
- Rename db.query.participants to db.query.seasonParticipants
- Update 9 route files and 1 test file

Affected files:
- admin.sports-seasons.$id.events.$eventId.bracket.server.ts
- admin.sports-seasons.$id.participants.tsx
- api/draft.force-manual-pick.ts
- api/draft.make-pick.ts
- api/draft.replace-pick.ts
- api/seasons.$seasonId.draft.ts
- leagues/$leagueId.draft-board.$seasonId.tsx
- leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts
- admin/__tests__/sports-seasons-participants.test.ts

Error count reduced from 499 to 453 (46 errors fixed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(routes): update route files for schema rename

Update route imports from ~/models/participant to ~/models/season-participant
and fix references to .participant/.participantId on event results to use
.seasonParticipant/.seasonParticipantId after schema rename.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(services): update simulators and services for renamed schema

Update all simulators, services, and server files to use renamed schema tables:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantResults → seasonParticipantResults
- eventResults.participantId → eventResults.seasonParticipantId

Files updated:
- 20 sport simulators (NBA, NHL, NFL, MLB, etc.)
- probability-updater.ts
- standings-sync/index.ts
- sports-data-sync.server.ts
- server/socket.ts

Typecheck errors reduced from 365 to 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* migration: rename per-window tables to season_* prefix

* fix(tests): update mock query keys after participants table rename

Change mock db.query.participants to db.query.seasonParticipants in test
files to match the schema rename from commit 66145a9. This fixes
"Cannot read properties of undefined (reading 'findFirst'/'findMany')"
errors that occurred when production code queries db.query.seasonParticipants
but test mocks only defined the old participants key.

Files updated:
- app/services/simulations/__tests__/world-cup-simulator.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts
- app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts
- server/__tests__/timer-autodraft.test.ts
- app/models/__tests__/team-score-events.test.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): update remaining mock paths and keys after schema rename

* fix(tests): final two mock stragglers after schema rename

- draft-pick.test.ts: assertion on db.query.participantQualifyingTotals
- process-match-result.test.ts: mock key participants → seasonParticipants

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: add post-phase1a baseline capture (temp, for diff verification)

* chore: capture pre-migration baselines

* chore: remove post-phase1a capture helper after verification

* schema: add canonical tournament & participant tables

Adds tournaments, participants (canonical), tournament_results, and
participant_surface_elos (canonical). Adds nullable tournament_id to
scoring_events and nullable participant_id to season_participants.
Phase 1b of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(models): add canonical tournament, participant, result, surface-elo models

Adds CRUD modules for the canonical tables created in commit 775b905.
Each module mirrors existing app/models conventions (database() from
~/database/context, schema from ~/database/schema, mock-based tests).

Key implementation notes:
- participant.ts exports use "Canonical" prefix (CanonicalParticipant,
  createCanonicalParticipant, etc.) to avoid collision with existing
  season-participant.ts exports
- All four models include comprehensive unit tests following the
  audit-log.test.ts pattern
- Tests use mocked db responses (no real database access)
- Upsert functions use onConflictDoUpdate for appropriate unique constraints

Part of Phase 1b of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* migration: create canonical tables, add nullable FKs

* scripts: add extractTournamentIdentity helper for backfill

Pure function that derives canonical (name, year) identity from a
scoring_events row, stripping trailing 4-digit years from the name or
falling back to eventDate. Used by the Phase 2 backfill to group
per-window events into canonical tournaments.

* scripts: add backfill orchestrator for canonical layer

Populates canonical tournaments, participants, tournament_results, and
participant_surface_elos from per-window data for qualifying-points
sports. Skips already-linked rows, is idempotent, and supports dry-run
mode.

Critical invariants enforced by the implementation:
- qualifying_points_awarded is never copied to tournament_results
- season_participant_qualifying_totals is never touched
- conflicting surface-Elo values between windows raise a loud error
  (recorded in report.errors) rather than overwriting

* scripts: add backfill CLI with dry-run default

Wires backfill-canonical-layer.ts to a CLI entry point exposed as
`npm run backfill:canonical`. Defaults to --dry-run; requires --apply
to actually write. Supports --sport=<uuid> to limit to a single sport.
Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts).

* fix(backfill-cli): wrap runBackfill in DatabaseContext.run

The orchestrator uses database() from ~/database/context, which requires
AsyncLocalStorage to be populated. Wrap the CLI invocation with
DatabaseContext.run(db, ...) using server/db's cached connection pool.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(backfill-cli): exit 0 on success so pg pool doesn't block

The cached postgres connection pool keeps the Node event loop open after
main() returns. Explicit process.exit(0) on success mirrors the pattern
in scripts/capture-baseline.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Chris Parsons <chrisp@extrahop.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:13:18 -07:00
Chris Parsons
d5aa8a3de4
fix: address VORP code review issues (#283)
* fix: address VORP code review issues

- Switch admin EV form to batchUpsertParticipantEVs to avoid N×N
  concurrent UPDATE storm (was calling upsertParticipantEV per
  participant, each triggering a full syncVorpForSeason)
- deleteParticipantEV now resets vorpValue to "0" and calls
  syncVorpForSeason so remaining participants' ranks stay correct
- syncVorpForSeason issues a single bulk CASE UPDATE instead of
  N individual UPDATE statements
- Add doc warning on recalculateEV that callers must sync VORP manually
- Extract REPLACEMENT_LEVEL_START/END_IDX constants; clarify comment
  that 12-14 is a fixed product decision, not derived from league size
- Include vorpValue in draft room participant select projection
- Update drizzle-orm mock to support sql.join; update test assertions
  to reflect single bulk-update call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve lint errors (toSorted, unused var)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 23:20:02 -04:00
Chris Parsons
3c740c50bf feat: add syncVorpForSeason and wire into EV update flow
Adds syncVorpForSeason to recalculate and persist VORP for all participants
in a season after any EV change. Wires it into upsertParticipantEV,
batchUpsertParticipantEVs, and recalculateAllEVsForSeason so VORP stays
current whenever EVs are updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 02:05:36 +00:00
Chris Parsons
8c3663e01b
Add PDC World Darts Championship simulator with Elo-based bracket (#248)
Fixes #123

* Add PDC World Darts Championship simulator (128-player bracket)

- New DartsSimulator: 128-player single-elimination, 7 rounds with
  PDC-accurate best-of-sets formats (bo3/bo5/bo5/bo7/bo7/bo11/bo13).
  ELO_DIVISOR=500 via set-level Bernoulli model. Two simulation paths:
  Path A (bracket drawn) simulates from actual DB matches; Path B
  (pre-bracket) seeds top 32 by world ranking in fixed positions and
  randomly draws the remaining 96 per simulation run (50,000 iterations).
- darts_bracket added to simulatorTypeEnum and simulator registry.
- world_ranking nullable integer column added to participant_expected_values
  (migration 0067); batchSaveSourceElos now accepts and persists it.
- Admin route /admin/sports-seasons/:id/darts-elo: bulk import with format
  "Player Name, 2099, 1" (name, Elo, optional world ranking), fuzzy name
  matching, auto-runs simulation and updates EV/snapshots on save.
- DARTS_128 bracket template added (scoring starts at Quarterfinals).
- 30 unit tests: math helpers, bracket seeding structure, Path A/B integration.

https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz

* Review fixes: pre-compute hot-loop invariants, import normalizeName

- Pre-compute seededSlots and getSeededMatchOrder(32) before the 50k
  simulation loop — was being recomputed every iteration
- Pad unseeded pool to 96 once before the loop instead of inside it
- Inline bracket-building in the hot loop using pre-computed seededSlots;
  removes the per-iteration call to buildR1Bracket/getSeededMatchOrder
- Remove dead SEEDED_R1_PAIRS constant (was never referenced)
- Import normalizeName from ~/lib/fuzzy-match instead of redefining it locally

https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz

* Fix lint failures: unused vars, eqeqeq, toSorted

- Remove unused seededSet/unseededSet variables in test
- Replace != null with !== null && !== undefined (eqeqeq rule)
- Replace .sort() with .toSorted() in simulator and route (unicorn/no-array-sort)

https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz

* Fix TS2552: restore seededSet declaration removed during lint fix

https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-31 15:37:28 -07:00
Chris Parsons
1bdd5c3372 Add snooker bracket simulator and Elo ratings admin UI, fixes #119
- SnookerSimulator: Monte Carlo simulation of the 32-player World
  Championship bracket using per-frame Bernoulli win probabilities
  derived from Elo ratings (ELO_DIVISOR=700). Two paths: bracket-
  populated (respects completed matches) and pre-bracket (simulates
  qualifying, then seeds full draw).
- Admin route /sports-seasons/:id/elo-ratings: bulk-import and per-
  player Elo entry with fuzzy name matching; saves ratings and auto-
  runs the simulation in one action.
- schema: add snooker_bracket simulator type, source_elo column on
  participant_expected_values, unique index on (participant_id,
  sports_season_id).
- Fix batchSaveSourceElos to use INSERT ... ON CONFLICT DO UPDATE
  instead of N+1 SELECT+UPDATE loop.
- Fix existingElos returned as plain Record (Map doesn't survive JSON
  serialization through useLoaderData).
- Fix "How It Works" formula to show correct divisor (700, not 400).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:24:28 -07:00
Chris Parsons
c6ba59b0e6
User/chris/ev f1 framework (#93)
* feat: EV simulation framework with F1 Monte Carlo simulator

- Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons
- Add ev-snapshot model with upsert and history query functions
- Add simulator framework: types, bracket/F1/golf simulators, registry
- F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift
- Add admin simulate route and Run Simulation button on sports season page
- Rework futures-odds admin page to save odds then run simulation in one action
- Remove recalculate-probabilities route (superseded by simulate route)
- Remove EV trend chart panel and associated DB queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: map simulators to sports via simulatorType field

Adds a `simulator_type` enum column to the `sports` table so each sport
can be assigned a specific simulation algorithm rather than deriving it
from the sports season's scoring pattern.

- Add `simulatorTypeEnum` (f1_standings, indycar_standings,
  golf_qualifying_points, playoff_bracket) + `simulatorType` nullable
  column on `sports` table; migration 0037
- Rewrite simulator registry to key off `SimulatorType` instead of
  `ScoringPattern`; indycar_standings shares F1Simulator for now
- `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers
  have typed access to `sport.simulatorType`
- Simulate and futures-odds actions read `sport.simulatorType`; guard
  fires before setting `simulationStatus: running`
- Admin sport edit page gains a Simulator Type dropdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
Chris Parsons
9ae00a8385
Add Zod validation and expand export/import for EV and results data (#14)
* Include participant EV/futures data in data sync export/import

The export now includes the full participantExpectedValues records
(placement probabilities, source, sourceOdds) alongside participants.
On import, EV records are upserted in merge mode and created fresh in
replace mode (cascade handles cleanup). Backward-compatible with older
exports that lack the participantExpectedValues field.

https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B

* Address all data-sync code review findings

- Wrap importSportsDataFromJSON in a db.transaction() so any mid-import
  failure rolls back cleanly instead of leaving partial data
- Add zod validation of imported JSON before any DB writes, giving a
  clear error on malformed/incompatible files
- Fix N+1 queries: build participantIdMap during participant import so
  the EV and results sections resolve IDs from memory, not extra queries
- Fix merge mode silently skipping existing participants — now updates
  shortName, externalId, and expectedValue
- Add participantResults to export/import (was deleted in replace mode
  but never exported, so results were permanently lost)
- Restore calculatedAt timestamp on EV import instead of defaulting to
  now(); stored as ISO string in the export for portability
- Document that onDelete:cascade covers participantExpectedValues and
  participantResults when participants is deleted; remove the now-
  redundant explicit participantResults delete from replace mode
- Bump export version to 1.1; old v1.0 files still import cleanly since
  participantExpectedValues, participantResults, and calculatedAt are
  all optional in the zod schema
- Collapse all six DB fetches in exportSportsDataToJSON into one
  Promise.all for parallel execution
- Add countAllParticipants() and countAllParticipantEVs() model funcs
- Show Participants and EV Records counts on the dashboard stat cards
- Replace raw DOM form creation/submit in handleImport with useFetcher,
  giving proper loading state, no full-page reload, and type-safe data
- Remove dead export intent handler from the action function

https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 20:25:25 -08:00
Chris Parsons
2d6dd6ec9f
Refactor expected values form to batch save all participants (#4)
* fix: remove probability sum validation and add batch save with total EV

- Remove the requirement that manual probabilities sum to 1.0, allowing
  partial distributions (e.g. when a participant has <100% chance of top 8)
- Replace per-row save buttons with a single "Save All" button that submits
  all participants at once
- Add a Total EV row at the bottom of the table summing all participant EVs
- Update action to batch process all participants from a single form submission

https://claude.ai/code/session_01WHRynBCcugSK7HHEmN6Yuy

* fix: validate participantIds server-side instead of trusting form input

Fetch participants from the DB in the action using the season ID from
the URL params, eliminating the untrusted participantIds hidden field.
Remove the now-unused hidden input from the frontend form.

https://claude.ai/code/session_01WHRynBCcugSK7HHEmN6Yuy

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 15:57:05 -08:00
Chris Parsons
3210ab265f
Change participant expectedValue from integer to decimal (#1)
* feat: sync odds-based EV to participants table for draft room ranking

When admin enters futures odds or manual probabilities, the calculated EV
is now synced from participantExpectedValues to participants.expectedValue.
This closes the gap where EVs were calculated but never reflected in the
draft room ranking. Also changes expectedValue from integer to decimal(10,2)
for better precision, and displays "—" for participants with no odds data.

https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T

* fix: correct expectedValue type to string in participants route

Missed instance of `expectedValue: 0` (number) that failed typecheck
after schema change from integer to decimal.

https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 11:26:40 -08:00
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
Chris Parsons
79ec477a98 feat: implement Expected Value System with ICM probability calculator
Implements Phase 5.2 of the EV system with Harville-Malmuth Independent Chip Model
for calculating participant placement probabilities from futures odds.

## Key Features

### ICM Probability Calculator
- Implements Harville-Malmuth method for distributing probabilities
- Converts American odds to championship probabilities
- Generates P(1st) through P(8th) for all participants
- Column-normalized: each placement sums to 100% across all teams
- Works with any number of participants (not limited to 8)

### Admin UI - Futures Odds Entry
- Enter American odds (e.g., +550, -200) for championship futures
- Live preview of ICM-calculated probability distributions
- Displays all 8 placement probabilities
- Persists odds for editing on subsequent visits
- Automatic probability normalization (removes bookmaker vig)

### Database Schema Updates
- Renamed participant_expected_values.season_id → sports_season_id
- Updated foreign key to reference sports_seasons instead of seasons
- Added source_odds field to store original futures odds
- Migration 0025: Column rename and FK update
- Migration 0026: Add source_odds field

### Model Layer
- participant-expected-value: CRUD operations for probability distributions
- Supports multiple probability sources (manual, futures_odds, elo_simulation)
- Automatic EV calculation based on league scoring rules
- Probability validation and normalization

### Service Layer
- icm-calculator: Harville-Malmuth probability distribution
- probability-engine: Odds conversion and Elo utilities (for future use)
- bracket-simulator: Monte Carlo simulation (for future hybrid approach)
- ev-calculator: Expected value computation from probabilities

## Technical Details

- Uses exponential decay favoring top positions for strong teams
- Preserves championship probability ordering in final distributions
- Row sums vary (strong teams ~100%, weak teams lower)
- All probabilities between 0-1, mathematically valid
- Comprehensive test suite: 97 tests passing

## Future Enhancements

- Hybrid approach: ICM pre-playoffs, bracket simulation during playoffs
- Integration with league-specific scoring rules
- Historical probability tracking for accuracy analysis

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:19:46 -08:00