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
.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 {

View file

@ -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(

View file

@ -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 },

View file

@ -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,
});
}