claude/clever-archimedes-dvye42 #110
4 changed files with 45 additions and 19 deletions
|
|
@ -394,10 +394,10 @@ export async function batchSaveSourceOdds(
|
|||
await tx
|
||||
.update(seasonParticipantExpectedValues)
|
||||
// Persist the odds and label the source as odds-driven for display.
|
||||
// We no longer null a stored Elo here: whether these odds override it
|
||||
// is decided at run time by the configurable source priority
|
||||
// (SimulatorInputPolicy.sourceEloPriority), and prepareSimulatorInputsForRun
|
||||
// overwrites the derived Elo before the simulator reads it.
|
||||
// We no longer null a stored Elo here: how much these odds move it is
|
||||
// decided at run time by the season's blend weight
|
||||
// (SimulatorInputPolicy.oddsWeight), and prepareSimulatorInputsForRun
|
||||
// overwrites the resolved Elo before the simulator reads it.
|
||||
.set({ sourceOdds, source: "futures_odds", updatedAt: now })
|
||||
.where(eq(seasonParticipantExpectedValues.id, existing[0].id));
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -367,10 +367,10 @@ export async function batchSaveFuturesOddsForSimulator(
|
|||
const db = database();
|
||||
const now = new Date();
|
||||
|
||||
// Persist the odds non-destructively. Whether these odds override a stored
|
||||
// Elo/rating is now governed by the season's configurable source priority
|
||||
// (see resolveSourceElos / SimulatorInputPolicy.sourceEloPriority), so we no
|
||||
// longer null out manually entered Elo here — that policy decides at run time.
|
||||
// Persist the odds non-destructively. How much these odds move a stored
|
||||
// Elo/rating is now governed by the season's blend weight (see resolveSourceElos
|
||||
// / SimulatorInputPolicy.oddsWeight), so we no longer null out manually entered
|
||||
// Elo here — that policy decides at run time.
|
||||
await db
|
||||
.insert(schema.seasonParticipantSimulatorInputs)
|
||||
.values(
|
||||
|
|
|
|||
|
|
@ -188,6 +188,23 @@ describe("simulator input policy", () => {
|
|||
expect(resolved.get("missing-tail")).toMatchObject({ rating: -10, method: "worstKnownMinus" });
|
||||
});
|
||||
|
||||
it("does not clamp a directly entered Elo above the policy ceiling", () => {
|
||||
// Snooker/darts enter manual Elos well above the default 1900 ceiling and
|
||||
// have no odds source — the direct value must pass through unclamped.
|
||||
const eloOnlyProfile = {} as Pick<SimulatorManifestProfile, "derivableInputs">;
|
||||
const resolved = resolveSourceElos(
|
||||
[
|
||||
{ participantId: "ronnie", sourceElo: 2450, rating: null, sourceOdds: null, projectedWins: null, projectedTablePoints: null },
|
||||
{ participantId: "judd", sourceElo: 2300, rating: null, sourceOdds: null, projectedWins: null, projectedTablePoints: null },
|
||||
],
|
||||
eloOnlyProfile,
|
||||
{}
|
||||
);
|
||||
|
||||
expect(resolved.get("ronnie")).toMatchObject({ sourceElo: 2450, method: "direct" });
|
||||
expect(resolved.get("judd")).toMatchObject({ sourceElo: 2300, method: "direct" });
|
||||
});
|
||||
|
||||
it("blends base Elo and futures odds into a single Elo by oddsWeight", () => {
|
||||
const inputs = [
|
||||
{ participantId: "favorite", sourceElo: 1800, rating: null, sourceOdds: 300, projectedWins: null, projectedTablePoints: null },
|
||||
|
|
|
|||
|
|
@ -240,35 +240,41 @@ export function resolveSourceElos(
|
|||
}
|
||||
|
||||
// 3. Blend base + odds into the single Elo that feeds the simulator.
|
||||
// Only odds-derived / blended / fallback values are clamped+rounded onto the
|
||||
// [eloMin, eloMax] band (they are mapped onto it by design). A base Elo is
|
||||
// passed through untouched: a directly entered Elo is trusted as-is (some
|
||||
// sports use ratings above the default ceiling, e.g. snooker ~2450) and a
|
||||
// projection-derived Elo was already clamped by projectionToElo.
|
||||
const w = policy.oddsWeight;
|
||||
const clampDerived = (elo: number): number => clamp(Math.round(elo), policy.eloMin, policy.eloMax);
|
||||
for (const input of inputs) {
|
||||
const base = baseElo.get(input.participantId);
|
||||
const odds = oddsElo.get(input.participantId);
|
||||
let elo: number;
|
||||
let sourceElo: number;
|
||||
let method: ResolvedSourceElo["method"];
|
||||
if (base !== undefined && odds !== undefined) {
|
||||
if (w <= 0) {
|
||||
elo = base.elo;
|
||||
sourceElo = base.elo;
|
||||
method = base.method;
|
||||
} else if (w >= 1) {
|
||||
elo = odds;
|
||||
sourceElo = clampDerived(odds);
|
||||
method = "sourceOdds";
|
||||
} else {
|
||||
elo = (1 - w) * base.elo + w * odds;
|
||||
sourceElo = clampDerived((1 - w) * base.elo + w * odds);
|
||||
method = "blend";
|
||||
}
|
||||
} else if (base !== undefined) {
|
||||
elo = base.elo;
|
||||
sourceElo = base.elo;
|
||||
method = base.method;
|
||||
} else if (odds !== undefined) {
|
||||
elo = odds;
|
||||
sourceElo = clampDerived(odds);
|
||||
method = "sourceOdds";
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
resolved.set(input.participantId, {
|
||||
participantId: input.participantId,
|
||||
sourceElo: clamp(Math.round(elo), policy.eloMin, policy.eloMax),
|
||||
sourceElo,
|
||||
method,
|
||||
});
|
||||
}
|
||||
|
|
@ -343,7 +349,10 @@ export function resolveRatings(
|
|||
}
|
||||
}
|
||||
|
||||
// As with Elo: only odds-derived / blended values are clamped onto the rating
|
||||
// band; a directly entered rating is trusted as-is (matching prior behavior).
|
||||
const w = policy.oddsWeight;
|
||||
const clampDerived = (rating: number): number => clamp(rating, policy.ratingMin, policy.ratingMax);
|
||||
for (const input of inputs) {
|
||||
const base = baseRating.get(input.participantId);
|
||||
const odds = oddsRating.get(input.participantId);
|
||||
|
|
@ -354,24 +363,24 @@ export function resolveRatings(
|
|||
rating = base;
|
||||
method = "direct";
|
||||
} else if (w >= 1) {
|
||||
rating = odds;
|
||||
rating = clampDerived(odds);
|
||||
method = "sourceOdds";
|
||||
} else {
|
||||
rating = (1 - w) * base + w * odds;
|
||||
rating = clampDerived((1 - w) * base + w * odds);
|
||||
method = "blend";
|
||||
}
|
||||
} else if (base !== undefined) {
|
||||
rating = base;
|
||||
method = "direct";
|
||||
} else if (odds !== undefined) {
|
||||
rating = odds;
|
||||
rating = clampDerived(odds);
|
||||
method = "sourceOdds";
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
resolved.set(input.participantId, {
|
||||
participantId: input.participantId,
|
||||
rating: clamp(rating, policy.ratingMin, policy.ratingMax),
|
||||
rating,
|
||||
method,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue