claude/clever-archimedes-dvye42 #110

Merged
chrisp merged 4 commits from claude/clever-archimedes-dvye42 into main 2026-06-26 05:16:54 +00:00
4 changed files with 45 additions and 19 deletions
Showing only changes of commit 8fff8d2715 - Show all commits

View file

@ -394,10 +394,10 @@ export async function batchSaveSourceOdds(
await tx await tx
.update(seasonParticipantExpectedValues) .update(seasonParticipantExpectedValues)
// Persist the odds and label the source as odds-driven for display. // 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 // We no longer null a stored Elo here: how much these odds move it is
// is decided at run time by the configurable source priority // decided at run time by the season's blend weight
// (SimulatorInputPolicy.sourceEloPriority), and prepareSimulatorInputsForRun // (SimulatorInputPolicy.oddsWeight), and prepareSimulatorInputsForRun
// overwrites the derived Elo before the simulator reads it. // overwrites the resolved Elo before the simulator reads it.
.set({ sourceOdds, source: "futures_odds", updatedAt: now }) .set({ sourceOdds, source: "futures_odds", updatedAt: now })
.where(eq(seasonParticipantExpectedValues.id, existing[0].id)); .where(eq(seasonParticipantExpectedValues.id, existing[0].id));
} else { } else {

View file

@ -367,10 +367,10 @@ export async function batchSaveFuturesOddsForSimulator(
const db = database(); const db = database();
const now = new Date(); const now = new Date();
// Persist the odds non-destructively. Whether these odds override a stored // Persist the odds non-destructively. How much these odds move a stored
// Elo/rating is now governed by the season's configurable source priority // Elo/rating is now governed by the season's blend weight (see resolveSourceElos
// (see resolveSourceElos / SimulatorInputPolicy.sourceEloPriority), so we no // / SimulatorInputPolicy.oddsWeight), so we no longer null out manually entered
// longer null out manually entered Elo here — that policy decides at run time. // Elo here — that policy decides at run time.
await db await db
.insert(schema.seasonParticipantSimulatorInputs) .insert(schema.seasonParticipantSimulatorInputs)
.values( .values(

View file

@ -188,6 +188,23 @@ describe("simulator input policy", () => {
expect(resolved.get("missing-tail")).toMatchObject({ rating: -10, method: "worstKnownMinus" }); 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", () => { it("blends base Elo and futures odds into a single Elo by oddsWeight", () => {
const inputs = [ const inputs = [
{ participantId: "favorite", sourceElo: 1800, rating: null, sourceOdds: 300, projectedWins: null, projectedTablePoints: null }, { participantId: "favorite", sourceElo: 1800, rating: null, sourceOdds: 300, projectedWins: null, projectedTablePoints: null },

View file

@ -240,35 +240,41 @@ export function resolveSourceElos(
} }
// 3. Blend base + odds into the single Elo that feeds the simulator. // 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 w = policy.oddsWeight;
const clampDerived = (elo: number): number => clamp(Math.round(elo), policy.eloMin, policy.eloMax);
for (const input of inputs) { for (const input of inputs) {
const base = baseElo.get(input.participantId); const base = baseElo.get(input.participantId);
const odds = oddsElo.get(input.participantId); const odds = oddsElo.get(input.participantId);
let elo: number; let sourceElo: number;
let method: ResolvedSourceElo["method"]; let method: ResolvedSourceElo["method"];
if (base !== undefined && odds !== undefined) { if (base !== undefined && odds !== undefined) {
if (w <= 0) { if (w <= 0) {
elo = base.elo; sourceElo = base.elo;
method = base.method; method = base.method;
} else if (w >= 1) { } else if (w >= 1) {
elo = odds; sourceElo = clampDerived(odds);
method = "sourceOdds"; method = "sourceOdds";
} else { } else {
elo = (1 - w) * base.elo + w * odds; sourceElo = clampDerived((1 - w) * base.elo + w * odds);
method = "blend"; method = "blend";
} }
} else if (base !== undefined) { } else if (base !== undefined) {
elo = base.elo; sourceElo = base.elo;
method = base.method; method = base.method;
} else if (odds !== undefined) { } else if (odds !== undefined) {
elo = odds; sourceElo = clampDerived(odds);
method = "sourceOdds"; method = "sourceOdds";
} else { } else {
continue; continue;
} }
resolved.set(input.participantId, { resolved.set(input.participantId, {
participantId: input.participantId, participantId: input.participantId,
sourceElo: clamp(Math.round(elo), policy.eloMin, policy.eloMax), sourceElo,
method, 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 w = policy.oddsWeight;
const clampDerived = (rating: number): number => clamp(rating, policy.ratingMin, policy.ratingMax);
for (const input of inputs) { for (const input of inputs) {
const base = baseRating.get(input.participantId); const base = baseRating.get(input.participantId);
const odds = oddsRating.get(input.participantId); const odds = oddsRating.get(input.participantId);
@ -354,24 +363,24 @@ export function resolveRatings(
rating = base; rating = base;
method = "direct"; method = "direct";
} else if (w >= 1) { } else if (w >= 1) {
rating = odds; rating = clampDerived(odds);
method = "sourceOdds"; method = "sourceOdds";
} else { } else {
rating = (1 - w) * base + w * odds; rating = clampDerived((1 - w) * base + w * odds);
method = "blend"; method = "blend";
} }
} else if (base !== undefined) { } else if (base !== undefined) {
rating = base; rating = base;
method = "direct"; method = "direct";
} else if (odds !== undefined) { } else if (odds !== undefined) {
rating = odds; rating = clampDerived(odds);
method = "sourceOdds"; method = "sourceOdds";
} else { } else {
continue; continue;
} }
resolved.set(input.participantId, { resolved.set(input.participantId, {
participantId: input.participantId, participantId: input.participantId,
rating: clamp(rating, policy.ratingMin, policy.ratingMax), rating,
method, method,
}); });
} }