- input-policy: revert the odds->Elo change that passed the policy's
eloMin/eloMax into convertFuturesToElo. That widened the default odds-derived
spread (1100/1900 vs the calibrated 1250/1750) — sharpening favorites for
default-policy odds sims (UCL, World Cup, NCAA-FB, MLB) — and clamped the
blend of a high directly-entered base Elo down to the ceiling. Odds now use
the calibrated band; the policy's eloMin/eloMax still clamp the result
(clampDerived), so a season's floor/ceiling narrows the spread without
widening it or capping high base Elos.
- admin simulator UI: restrict the structured Engine fields to a
HONORED_ENGINE_KNOBS allowlist of keys actually read from config (by a
simulator or the input-policy resolver). Bespoke per-sim constants present in
a profile but not read from config (homeFieldElo, eloDivisor, srsEloScale,
raceNoise, plBeta, fieldSize, bracketSize, ...) no longer render as editable
controls that silently do nothing; the raw-JSON escape hatch still edits them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAFMogMkFJf52YpHyCDvuf
Two problems addressed:
1. Favorites' P(1st) was too sharp (e.g. NHL top teams ~20% vs ~12% implied).
- The NHL simulator hardcoded its parity factor (1000) and ignored the
season config's parityFactor, so the knob meant to flatten the
distribution did nothing. It also re-blended raw futures odds into every
game on top of the odds->Elo conversion, double-counting the same signal.
- NHL now reads parityFactor/iterations/seasonGames/overtimeRate from config
and no longer re-blends odds per game (odds enter once, via the central
odds->Elo resolver). Honoring parity 2500 flattens a top team from ~29% to
~13% title odds.
2. "Season Config" and "Input Policy" were two forms over the same stored
object that didn't reflect each other, and the engine-knob half was inert
for many simulators.
- Every simulator now reads its engine knobs (iterations everywhere;
parityFactor for all Elo-based sims) from the merged config, passed in by
the runner via the Simulator interface. Defaults equal the former
hardcoded constants, so behavior is unchanged unless a season overrides.
- The admin simulator page is now a single "Simulator Configuration" card
with structured Engine and Input-derivation sections (profile-driven, so
each sport shows only the knobs it honors) plus an Advanced raw-JSON
escape hatch — all writing the same config.
Also: centralized the duplicated configNumber helpers into config-access.ts;
the central odds->Elo resolver now maps onto the configured Elo floor/ceiling
so those bounds set the odds-derived spread (a real flattening dial).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAFMogMkFJf52YpHyCDvuf
When an admin entered futures (preseason) odds for a season that already
had Elo ratings stored, the simulator kept using the old Elo and silently
ignored the new odds. This affected any Elo-based simulator (e.g. NHL).
Root cause: resolveSourceElos() ranks a direct sourceElo above the
sourceOdds -> convertFuturesToElo branch, but batchSaveFuturesOddsForSimulator()
only cleared the bracket-seeding `rating`/`ratingMethod` — never the stale
`sourceElo`/`sourceEloMethod`. A manually entered Elo (method "direct") is not
treated as generated, so it survived and short-circuited the resolver.
Fix:
- batchSaveFuturesOddsForSimulator now also nulls sourceElo and strips
sourceEloMethod (both the pre-update and upsert-conflict paths), so the
existing futures -> Elo conversion drives the run.
- resolveSourceElos' sourceOdds branch now guards for >= 2 participants
(mirroring resolveRatings), so a lone-odds season falls through to the
configured missing-Elo strategy instead of getting a flat ~1500.
- batchSaveSourceOdds clears the legacy EV sourceElo and marks source as
futures_odds so the elo-ratings page won't resurrect a stale rating.
Adds unit coverage for odds-derived Elo, the single-participant guard, the
post-clear regression, generated-vs-direct sourceElo suppression, and the
new clearing behavior in batchSaveFuturesOddsForSimulator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNfUEd9RzD3zm84oLHBHUH
- 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>
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>