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
|
|
|
|
import { convertFuturesToElo } from "~/services/probability-engine";
|
|
|
|
|
|
import { simulatorInputLabel, type SimulatorManifestProfile } from "./manifest";
|
2026-05-13 01:06:16 -07:00
|
|
|
|
import { logger } from "~/lib/logger";
|
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
|
|
|
|
|
|
|
|
|
|
export type MissingEloStrategy = "block" | "fallbackElo" | "averageKnown" | "worstKnownMinus";
|
2026-05-12 22:26:25 -07:00
|
|
|
|
export type MissingRatingStrategy = "block" | "fallbackRating" | "averageKnown" | "worstKnownMinus";
|
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
|
|
|
|
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Sources a participant's strength input can be derived from, in the order the
|
|
|
|
|
|
* resolver should try them. `"sourceElo"` represents the simulator's primary
|
|
|
|
|
|
* direct input — a manually entered Elo for Elo-based sims, or the direct
|
|
|
|
|
|
* `rating` for rating-based sims. Placing `"sourceOdds"` ahead of `"sourceElo"`
|
|
|
|
|
|
* is how an admin makes futures odds override stored Elo/ratings.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export type SourceEloKey = "sourceElo" | "projectedWins" | "projectedTablePoints" | "sourceOdds";
|
|
|
|
|
|
|
|
|
|
|
|
export const SOURCE_ELO_KEYS: readonly SourceEloKey[] = [
|
|
|
|
|
|
"sourceElo",
|
|
|
|
|
|
"projectedWins",
|
|
|
|
|
|
"projectedTablePoints",
|
|
|
|
|
|
"sourceOdds",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/** Default priority: stored Elo/projections win, futures odds are the fallback. */
|
|
|
|
|
|
export const ELO_FIRST_SOURCE_PRIORITY: SourceEloKey[] = [
|
|
|
|
|
|
"sourceElo",
|
|
|
|
|
|
"projectedWins",
|
|
|
|
|
|
"projectedTablePoints",
|
|
|
|
|
|
"sourceOdds",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/** Futures-override priority: freshly entered odds win over stored Elo/projections. */
|
|
|
|
|
|
export const FUTURES_FIRST_SOURCE_PRIORITY: SourceEloKey[] = [
|
|
|
|
|
|
"sourceOdds",
|
|
|
|
|
|
"sourceElo",
|
|
|
|
|
|
"projectedWins",
|
|
|
|
|
|
"projectedTablePoints",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/** Whether a priority list prefers futures odds over the direct Elo/rating input. */
|
|
|
|
|
|
export function prefersFuturesOdds(priority: SourceEloKey[]): boolean {
|
|
|
|
|
|
const oddsIndex = priority.indexOf("sourceOdds");
|
|
|
|
|
|
const eloIndex = priority.indexOf("sourceElo");
|
|
|
|
|
|
if (oddsIndex === -1) return false;
|
|
|
|
|
|
return eloIndex === -1 || oddsIndex < eloIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
export interface SimulatorInputPolicy {
|
|
|
|
|
|
missingEloStrategy: MissingEloStrategy;
|
2026-05-12 22:26:25 -07:00
|
|
|
|
missingRatingStrategy: MissingRatingStrategy;
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
/** Ordered preference for deriving a participant's strength input. */
|
|
|
|
|
|
sourceEloPriority: SourceEloKey[];
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Weight (0–1) given to the futures-odds signal in simulators that blend Elo
|
|
|
|
|
|
* and odds (UCL, World Cup, NCAA Football, MLB, college hockey). The Elo
|
|
|
|
|
|
* signal gets `1 - oddsWeight`. Set to 1.0 to make futures fully override Elo.
|
|
|
|
|
|
*/
|
|
|
|
|
|
oddsWeight: number;
|
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
|
|
|
|
fallbackElo: number;
|
|
|
|
|
|
fallbackEloDelta: number;
|
|
|
|
|
|
eloMin: number;
|
|
|
|
|
|
eloMax: number;
|
2026-05-12 22:26:25 -07:00
|
|
|
|
fallbackRating: number;
|
|
|
|
|
|
fallbackRatingDelta: number;
|
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
|
|
|
|
ratingMin: number;
|
|
|
|
|
|
ratingMax: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface ParticipantInputForPolicy {
|
|
|
|
|
|
participantId: string;
|
|
|
|
|
|
sourceOdds: number | null;
|
|
|
|
|
|
sourceElo: number | null;
|
|
|
|
|
|
rating: number | null;
|
|
|
|
|
|
projectedWins: number | null;
|
|
|
|
|
|
projectedTablePoints: number | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface ResolvedSourceElo {
|
|
|
|
|
|
participantId: string;
|
|
|
|
|
|
sourceElo: number;
|
|
|
|
|
|
method: "direct" | "projectedWins" | "projectedTablePoints" | "sourceOdds" | MissingEloStrategy;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface ResolvedRating {
|
|
|
|
|
|
participantId: string;
|
|
|
|
|
|
rating: number;
|
2026-05-12 22:26:25 -07:00
|
|
|
|
method: "direct" | "sourceOdds" | MissingRatingStrategy;
|
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
|
|
|
|
}
|
|
|
|
|
|
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
const DEFAULT_ODDS_WEIGHT = 0.3;
|
|
|
|
|
|
|
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
|
|
|
|
const DEFAULT_POLICY: SimulatorInputPolicy = {
|
|
|
|
|
|
missingEloStrategy: "block",
|
2026-05-12 22:26:25 -07:00
|
|
|
|
missingRatingStrategy: "block",
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
sourceEloPriority: ELO_FIRST_SOURCE_PRIORITY,
|
|
|
|
|
|
oddsWeight: DEFAULT_ODDS_WEIGHT,
|
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
|
|
|
|
fallbackElo: 1400,
|
|
|
|
|
|
fallbackEloDelta: 25,
|
|
|
|
|
|
eloMin: 1100,
|
|
|
|
|
|
eloMax: 1900,
|
2026-05-12 22:26:25 -07:00
|
|
|
|
fallbackRating: 0,
|
|
|
|
|
|
fallbackRatingDelta: 5,
|
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
|
|
|
|
ratingMin: -20,
|
|
|
|
|
|
ratingMax: 40,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function optionalNumber(value: unknown, fallback: number): number {
|
|
|
|
|
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clamp(value: number, min: number, max: number): number {
|
|
|
|
|
|
return Math.min(max, Math.max(min, value));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toWinRate(projection: number, maxProjection: number): number {
|
|
|
|
|
|
if (maxProjection <= 0) return 0.5;
|
|
|
|
|
|
return clamp(projection / maxProjection, 0.01, 0.99);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function projectionToElo(
|
|
|
|
|
|
projection: number,
|
|
|
|
|
|
maxProjection: number,
|
|
|
|
|
|
parityFactor: number,
|
|
|
|
|
|
policy: SimulatorInputPolicy
|
|
|
|
|
|
): number {
|
|
|
|
|
|
const rate = toWinRate(projection, maxProjection);
|
|
|
|
|
|
const elo = 1500 + parityFactor * Math.log10(rate / (1 - rate));
|
|
|
|
|
|
return clamp(Math.round(elo), policy.eloMin, policy.eloMax);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
function parseSourceEloPriority(value: unknown, fallback: SourceEloKey[]): SourceEloKey[] {
|
|
|
|
|
|
if (!Array.isArray(value)) return fallback;
|
|
|
|
|
|
const seen = new Set<SourceEloKey>();
|
|
|
|
|
|
const parsed: SourceEloKey[] = [];
|
|
|
|
|
|
for (const entry of value) {
|
|
|
|
|
|
if (typeof entry === "string" && (SOURCE_ELO_KEYS as readonly string[]).includes(entry) && !seen.has(entry as SourceEloKey)) {
|
|
|
|
|
|
seen.add(entry as SourceEloKey);
|
|
|
|
|
|
parsed.push(entry as SourceEloKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return parsed.length > 0 ? parsed : fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
export function getSimulatorInputPolicy(config: Record<string, unknown>): SimulatorInputPolicy {
|
|
|
|
|
|
const rawPolicy = isRecord(config.inputPolicy) ? config.inputPolicy : {};
|
2026-05-12 22:26:25 -07:00
|
|
|
|
const eloStrategy = rawPolicy.missingEloStrategy;
|
|
|
|
|
|
const ratingStrategy = rawPolicy.missingRatingStrategy;
|
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
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
missingEloStrategy:
|
2026-05-12 22:26:25 -07:00
|
|
|
|
eloStrategy === "fallbackElo" || eloStrategy === "averageKnown" || eloStrategy === "worstKnownMinus"
|
|
|
|
|
|
? eloStrategy
|
|
|
|
|
|
: "block",
|
|
|
|
|
|
missingRatingStrategy:
|
|
|
|
|
|
ratingStrategy === "fallbackRating" || ratingStrategy === "averageKnown" || ratingStrategy === "worstKnownMinus"
|
|
|
|
|
|
? ratingStrategy
|
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
|
|
|
|
: "block",
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
sourceEloPriority: parseSourceEloPriority(rawPolicy.sourceEloPriority, DEFAULT_POLICY.sourceEloPriority),
|
|
|
|
|
|
// Allow oddsWeight to live either in inputPolicy (the unified knob) or at the
|
|
|
|
|
|
// top level of config (legacy per-profile defaultConfig), preferring the policy.
|
|
|
|
|
|
oddsWeight: clamp(
|
|
|
|
|
|
optionalNumber(rawPolicy.oddsWeight, optionalNumber(config.oddsWeight, DEFAULT_POLICY.oddsWeight)),
|
|
|
|
|
|
0,
|
|
|
|
|
|
1
|
|
|
|
|
|
),
|
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
|
|
|
|
fallbackElo: optionalNumber(rawPolicy.fallbackElo, DEFAULT_POLICY.fallbackElo),
|
|
|
|
|
|
fallbackEloDelta: optionalNumber(rawPolicy.fallbackEloDelta, DEFAULT_POLICY.fallbackEloDelta),
|
|
|
|
|
|
eloMin: optionalNumber(rawPolicy.eloMin, DEFAULT_POLICY.eloMin),
|
|
|
|
|
|
eloMax: optionalNumber(rawPolicy.eloMax, DEFAULT_POLICY.eloMax),
|
2026-05-12 22:26:25 -07:00
|
|
|
|
fallbackRating: optionalNumber(rawPolicy.fallbackRating, DEFAULT_POLICY.fallbackRating),
|
|
|
|
|
|
fallbackRatingDelta: optionalNumber(rawPolicy.fallbackRatingDelta, DEFAULT_POLICY.fallbackRatingDelta),
|
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
|
|
|
|
ratingMin: optionalNumber(rawPolicy.ratingMin, DEFAULT_POLICY.ratingMin),
|
|
|
|
|
|
ratingMax: optionalNumber(rawPolicy.ratingMax, DEFAULT_POLICY.ratingMax),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function sourceEloRequirementLabel(profile: SimulatorManifestProfile): string {
|
|
|
|
|
|
const alternatives = profile.derivableInputs?.sourceElo ?? [];
|
|
|
|
|
|
// Omit "configured fallback" from the label: if a participant appears in
|
|
|
|
|
|
// missingInputs it means no fallback resolved it, so mentioning fallback
|
|
|
|
|
|
// as an option would be misleading.
|
|
|
|
|
|
return ["Elo rating", ...alternatives.map(simulatorInputLabel)].join(" / ");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function ratingRequirementLabel(profile: SimulatorManifestProfile): string {
|
|
|
|
|
|
const alternatives = profile.derivableInputs?.rating ?? [];
|
|
|
|
|
|
return ["rating", ...alternatives.map(simulatorInputLabel)].join(" / ");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveSourceElos(
|
|
|
|
|
|
inputs: ParticipantInputForPolicy[],
|
|
|
|
|
|
profile: Pick<SimulatorManifestProfile, "derivableInputs">,
|
|
|
|
|
|
config: Record<string, unknown>
|
|
|
|
|
|
): Map<string, ResolvedSourceElo> {
|
|
|
|
|
|
const policy = getSimulatorInputPolicy(config);
|
|
|
|
|
|
const resolved = new Map<string, ResolvedSourceElo>();
|
|
|
|
|
|
const alternatives = new Set(profile.derivableInputs?.sourceElo ?? []);
|
|
|
|
|
|
const parityFactor = optionalNumber(config.parityFactor, 400);
|
|
|
|
|
|
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
// Resolve each participant from the first source in the configured priority
|
|
|
|
|
|
// that (a) is permitted for this simulator and (b) has a value. The direct
|
|
|
|
|
|
// `sourceElo` input is always permitted; the rest must be declared in the
|
|
|
|
|
|
// profile's `derivableInputs.sourceElo`. Reordering the priority so that
|
|
|
|
|
|
// `sourceOdds` precedes `sourceElo` makes freshly entered futures odds
|
|
|
|
|
|
// override a stale Elo (and projections) without destroying the stored value.
|
|
|
|
|
|
for (const source of policy.sourceEloPriority) {
|
|
|
|
|
|
if (source !== "sourceElo" && !alternatives.has(source)) continue;
|
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
|
|
|
|
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
if (source === "sourceElo") {
|
|
|
|
|
|
for (const input of inputs) {
|
|
|
|
|
|
if (resolved.has(input.participantId)) continue;
|
|
|
|
|
|
if (input.sourceElo !== null && input.sourceElo !== undefined) {
|
|
|
|
|
|
resolved.set(input.participantId, {
|
|
|
|
|
|
participantId: input.participantId,
|
|
|
|
|
|
sourceElo: input.sourceElo,
|
|
|
|
|
|
method: "direct",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (source === "projectedWins") {
|
|
|
|
|
|
const seasonGames = optionalNumber(config.seasonGames, Math.max(...inputs.map((input) => input.projectedWins ?? 0), 1));
|
|
|
|
|
|
for (const input of inputs) {
|
|
|
|
|
|
if (resolved.has(input.participantId) || input.projectedWins === null || input.projectedWins === undefined) continue;
|
|
|
|
|
|
resolved.set(input.participantId, {
|
|
|
|
|
|
participantId: input.participantId,
|
|
|
|
|
|
sourceElo: projectionToElo(input.projectedWins, seasonGames, parityFactor, policy),
|
|
|
|
|
|
method: "projectedWins",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (source === "projectedTablePoints") {
|
|
|
|
|
|
const seasonGames = optionalNumber(config.seasonGames, 38);
|
|
|
|
|
|
const maxPoints = optionalNumber(config.maxTablePoints, seasonGames * 3);
|
|
|
|
|
|
for (const input of inputs) {
|
|
|
|
|
|
if (resolved.has(input.participantId) || input.projectedTablePoints === null || input.projectedTablePoints === undefined) continue;
|
|
|
|
|
|
resolved.set(input.participantId, {
|
|
|
|
|
|
participantId: input.participantId,
|
|
|
|
|
|
sourceElo: projectionToElo(input.projectedTablePoints, maxPoints, parityFactor, policy),
|
|
|
|
|
|
method: "projectedTablePoints",
|
2026-06-25 17:43:31 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
} else if (source === "sourceOdds") {
|
|
|
|
|
|
// Odds-derived Elo maps onto eloMin/eloMax via convertFuturesToElo; when
|
|
|
|
|
|
// mixed with direct Elo for other participants the two scales are not
|
|
|
|
|
|
// jointly calibrated. That mixed case is intentionally left as-is.
|
|
|
|
|
|
const oddsInputs = inputs
|
|
|
|
|
|
.filter((input) => !resolved.has(input.participantId) && input.sourceOdds !== null && input.sourceOdds !== undefined)
|
|
|
|
|
|
.map((input) => ({ participantId: input.participantId, odds: input.sourceOdds as number }));
|
|
|
|
|
|
if (oddsInputs.length === 1) {
|
|
|
|
|
|
logger.warn(
|
|
|
|
|
|
`resolveSourceElos: only 1 participant has sourceOdds (${oddsInputs[0].participantId}). ` +
|
|
|
|
|
|
`convertFuturesToElo requires at least 2 to derive a meaningful Elo spread — falling through to the next source.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (oddsInputs.length >= 2) {
|
|
|
|
|
|
const converted = convertFuturesToElo(oddsInputs);
|
|
|
|
|
|
for (const [participantId, elo] of converted) {
|
|
|
|
|
|
resolved.set(participantId, {
|
|
|
|
|
|
participantId,
|
|
|
|
|
|
sourceElo: clamp(Math.round(elo), policy.eloMin, policy.eloMax),
|
|
|
|
|
|
method: "sourceOdds",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const knownElos = [...resolved.values()].map((value) => value.sourceElo);
|
|
|
|
|
|
const averageKnown = knownElos.length > 0
|
|
|
|
|
|
? knownElos.reduce((sum, elo) => sum + elo, 0) / knownElos.length
|
|
|
|
|
|
: policy.fallbackElo;
|
|
|
|
|
|
const worstKnown = knownElos.length > 0 ? Math.min(...knownElos) : policy.fallbackElo;
|
|
|
|
|
|
|
|
|
|
|
|
for (const input of inputs) {
|
|
|
|
|
|
if (resolved.has(input.participantId) || policy.missingEloStrategy === "block") continue;
|
|
|
|
|
|
|
|
|
|
|
|
let sourceElo: number;
|
|
|
|
|
|
if (policy.missingEloStrategy === "averageKnown") {
|
|
|
|
|
|
sourceElo = averageKnown;
|
|
|
|
|
|
} else if (policy.missingEloStrategy === "worstKnownMinus") {
|
|
|
|
|
|
sourceElo = worstKnown - policy.fallbackEloDelta;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sourceElo = policy.fallbackElo;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resolved.set(input.participantId, {
|
|
|
|
|
|
participantId: input.participantId,
|
|
|
|
|
|
sourceElo: clamp(Math.round(sourceElo), policy.eloMin, policy.eloMax),
|
|
|
|
|
|
method: policy.missingEloStrategy,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resolved;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRatings(
|
|
|
|
|
|
inputs: ParticipantInputForPolicy[],
|
|
|
|
|
|
profile: Pick<SimulatorManifestProfile, "derivableInputs">,
|
|
|
|
|
|
config: Record<string, unknown>
|
|
|
|
|
|
): Map<string, ResolvedRating> {
|
|
|
|
|
|
const policy = getSimulatorInputPolicy(config);
|
|
|
|
|
|
const resolved = new Map<string, ResolvedRating>();
|
|
|
|
|
|
const alternatives = new Set(profile.derivableInputs?.rating ?? []);
|
|
|
|
|
|
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
// Ratings share the configured priority list: the `"sourceElo"` slot stands
|
|
|
|
|
|
// for this simulator's direct strength input (here, `rating`), so ordering
|
|
|
|
|
|
// `"sourceOdds"` ahead of `"sourceElo"` makes futures override the rating too.
|
|
|
|
|
|
// Projection sources do not apply to rating-based simulators and are skipped.
|
|
|
|
|
|
for (const source of policy.sourceEloPriority) {
|
|
|
|
|
|
if (source === "sourceElo") {
|
|
|
|
|
|
for (const input of inputs) {
|
|
|
|
|
|
if (resolved.has(input.participantId)) continue;
|
|
|
|
|
|
if (input.rating !== null && input.rating !== undefined) {
|
|
|
|
|
|
resolved.set(input.participantId, {
|
|
|
|
|
|
participantId: input.participantId,
|
|
|
|
|
|
rating: input.rating,
|
|
|
|
|
|
method: "direct",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (source === "sourceOdds" && alternatives.has("sourceOdds")) {
|
|
|
|
|
|
const oddsInputs = inputs
|
|
|
|
|
|
.filter((input) => !resolved.has(input.participantId) && input.sourceOdds !== null && input.sourceOdds !== undefined)
|
|
|
|
|
|
.map((input) => ({ participantId: input.participantId, odds: input.sourceOdds as number }));
|
|
|
|
|
|
if (oddsInputs.length === 1) {
|
|
|
|
|
|
logger.warn(
|
|
|
|
|
|
`resolveRatings: only 1 participant has sourceOdds (${oddsInputs[0].participantId}). ` +
|
|
|
|
|
|
`convertFuturesToElo requires at least 2 to derive a meaningful rating — falling through to the next source.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (oddsInputs.length >= 2) {
|
|
|
|
|
|
const converted = convertFuturesToElo(oddsInputs, "american", {
|
|
|
|
|
|
exponent: 0.33,
|
|
|
|
|
|
eloMin: policy.ratingMin,
|
|
|
|
|
|
eloMax: policy.ratingMax,
|
2026-05-12 22:26:25 -07:00
|
|
|
|
});
|
Unify futures odds with the simulation system
Make the futures-vs-Elo relationship an explicit, configurable rule and fix
futures odds failing to override stored Elo.
Root causes addressed:
- resolveSourceElos/resolveRatings used a hardcoded precedence (direct Elo ->
projectedWins -> projectedTablePoints -> odds). The prior fix nulled sourceElo
on futures entry but not projections, which still outranked odds.
- The override relied on a destructive null-on-save hack that also wiped
manually entered Elo.
- Blended simulators buried their Elo/odds weight in module constants.
- Futures odds were not surfaced on the /admin/simulators inventory.
Changes:
- Add a configurable source policy: sourceEloPriority (ordered) and oddsWeight,
parsed/clamped in getSimulatorInputPolicy. resolveSourceElos/resolveRatings now
resolve each participant by the configured priority instead of a fixed order.
Futures-centric simulators (ncaam, ncaaw, world_cup, ncaa_football,
college_hockey) default to a futures-override priority.
- Drop the destructive nulling in batchSaveFuturesOddsForSimulator and
batchSaveSourceOdds; override is now governed by policy, preserving stored Elo.
- Thread an optional SimulationContext (oddsWeight) through the Simulator
interface so blended sims (UCL, World Cup, NCAA FB, MLB) read the blend weight
from the season policy; defaults preserve prior calibration when no context is
passed.
- Add a "Futures vs. Elo" strategy control and Odds Blend Weight input to the
Simulator Setup input-policy card, persisted via save-input-policy.
- Surface futures on /admin/simulators: a source badge and a Futures Odds quick
link; extend listSportsSeasonSimulatorSummaries with odds source info.
- Tests: configurable priority override (Elo/projections/rating), oddsWeight
parsing/clamping, prefersFuturesOdds, manifest defaults, an NCAA Football
context-blend behavioral test, and an updated non-destructive save test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhNeB7gN6VKene7sdbBVQT
2026-06-25 22:11:32 +00:00
|
|
|
|
for (const [participantId, rating] of converted) {
|
|
|
|
|
|
resolved.set(participantId, {
|
|
|
|
|
|
participantId,
|
|
|
|
|
|
rating: clamp(rating, policy.ratingMin, policy.ratingMax),
|
|
|
|
|
|
method: "sourceOdds",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-12 22:26:25 -07:00
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 22:26:25 -07:00
|
|
|
|
const knownRatings = [...resolved.values()].map((value) => value.rating);
|
|
|
|
|
|
const averageKnown = knownRatings.length > 0
|
|
|
|
|
|
? knownRatings.reduce((sum, rating) => sum + rating, 0) / knownRatings.length
|
|
|
|
|
|
: policy.fallbackRating;
|
|
|
|
|
|
const worstKnown = knownRatings.length > 0 ? Math.min(...knownRatings) : policy.fallbackRating;
|
|
|
|
|
|
|
|
|
|
|
|
for (const input of inputs) {
|
|
|
|
|
|
if (resolved.has(input.participantId) || policy.missingRatingStrategy === "block") continue;
|
|
|
|
|
|
|
|
|
|
|
|
let rating: number | null;
|
|
|
|
|
|
if (policy.missingRatingStrategy === "averageKnown") {
|
|
|
|
|
|
rating = averageKnown;
|
|
|
|
|
|
} else if (policy.missingRatingStrategy === "worstKnownMinus") {
|
|
|
|
|
|
rating = knownRatings.length > 0 ? worstKnown - policy.fallbackRatingDelta : null;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
rating = policy.fallbackRating;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rating === null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
resolved.set(input.participantId, {
|
|
|
|
|
|
participantId: input.participantId,
|
|
|
|
|
|
rating: clamp(rating, policy.ratingMin, policy.ratingMax),
|
|
|
|
|
|
method: policy.missingRatingStrategy,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
return resolved;
|
|
|
|
|
|
}
|