Session 61 (pre-merge): live-verification fixes from day-one rows
1. [settlement] armed boot line — settlement WAS scheduled (the tick runs settleAllOutcomes + settleAllLedgers FIRST at every snapshot slot, src/snapshotScheduler.js) but was invisible at boot. Now verifiable from logs forever. 2. Null locked_odds root cause (4 day-one rows, all betmgm SB unders): genuinely absent in the DB — the first-seen book row carried no juice for that side. indexOdds/indexProps now PREFER a book row with both sides priced (still a real row, never synthesized). Display was correct: absent beats wrong. 3. Verified retry-safe: 'Odds data temporarily unavailable' is the odds-api quota gate (oddsService:423) — thrown BEFORE any spend or cache write; runSnapshot errors write nothing; the next cron retries fresh with PropLine first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -100,13 +100,17 @@ function teamOpponentFor(g, prop) {
|
||||
return { team, opponent: null };
|
||||
}
|
||||
|
||||
/** Index odds props by nameKey|stat for lock/closing lookups. */
|
||||
/** Index odds props by nameKey|stat for lock/closing lookups.
|
||||
* Session 61 — prefer a book row with BOTH sides priced (same rule as
|
||||
* snapshotService.indexOdds): fewer genuinely-absent locked/closing odds
|
||||
* when another book carried the side. Real rows only, never synthesized. */
|
||||
function indexProps(props) {
|
||||
const map = {};
|
||||
const bothSides = (p) => p && p.over_odds != null && p.under_odds != null;
|
||||
for (const p of props || []) {
|
||||
if (!p || !p.player || !p.stat_type) continue;
|
||||
const k = `${nameKey(p.player)}|${String(p.stat_type).toLowerCase()}`;
|
||||
if (!map[k]) map[k] = p;
|
||||
if (!map[k] || (!bothSides(map[k]) && bothSides(p))) map[k] = p;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -52,12 +52,17 @@ async function mapLimit(items, concurrency, fn) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Index original odds props by propKey-ish (player|stat) for odds lookup. */
|
||||
/** Index original odds props by propKey-ish (player|stat) for odds lookup.
|
||||
* Session 61 — prefer a book row with BOTH sides priced: the first-seen row
|
||||
* sometimes carried only one side (e.g. betmgm SB unders with no juice),
|
||||
* which locked a NULL odds even though another book priced it. Still a
|
||||
* REAL book row, never synthesized. */
|
||||
function indexOdds(props) {
|
||||
const map = {};
|
||||
const bothSides = (p) => p && p.over_odds != null && p.under_odds != null;
|
||||
for (const p of props || []) {
|
||||
const k = `${norm(p.player)}|${String(p.stat_type || '').toLowerCase()}`;
|
||||
map[k] = p;
|
||||
if (!map[k] || (!bothSides(map[k]) && bothSides(p))) map[k] = p;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -147,6 +147,11 @@ function startSnapshotScheduler(opts = {}) {
|
||||
const interval = setInterval(() => { void tick(); void refreshTick(); }, 60_000);
|
||||
if (interval.unref) interval.unref();
|
||||
console.log(`[snapshotScheduler] armed — SNAPSHOT_CRON=${process.env.SNAPSHOT_CRON}, hours=${HOURS_UTC.join(',')} UTC, intraday=${process.env.INTRADAY_REFRESH === '0' ? 'off' : `${REFRESH_MINUTES}m (slate hours)`}`);
|
||||
// Session 61 — settlement is scheduled IN THIS tick (settleAllOutcomes +
|
||||
// settleAllLedgers run FIRST at every snapshot slot, before grading). It
|
||||
// was invisible at boot, which made "is settlement scheduled?" unanswerable
|
||||
// from logs. This line makes it verifiable forever.
|
||||
console.log(`[settlement] armed — outcomes + ledger settle pass runs FIRST at each snapshot slot (${HOURS_UTC.join(',')} UTC), idempotent re-runs`);
|
||||
return { interval, tick, refreshTick };
|
||||
}
|
||||
|
||||
|
||||
@@ -252,3 +252,22 @@ describe('getModelAggregate — per-tier calibration (n≥20 per tier)', () => {
|
||||
expect(agg.by_tier.B.hit_pct).toBeNull(); // under 20 → building
|
||||
});
|
||||
});
|
||||
|
||||
// Session 61 — the odds index prefers a fully-priced book row (the null
|
||||
// locked_odds rows from day one: first-seen betmgm SB unders had no juice
|
||||
// while another book priced both sides).
|
||||
describe('indexProps — prefers a book row with both sides priced', () => {
|
||||
const { indexProps } = ledger.__internals;
|
||||
test('a later both-sided row replaces a one-sided first row', () => {
|
||||
const oneSided = { player: 'A Guy', stat_type: 'stolen_bases', line: 0.5, book: 'betmgm', over_odds: 120, under_odds: null };
|
||||
const bothSided = { player: 'A Guy', stat_type: 'stolen_bases', line: 0.5, book: 'fanduel', over_odds: 130, under_odds: -170 };
|
||||
const idx = indexProps([oneSided, bothSided]);
|
||||
expect(idx['a guy|stolen_bases'].book).toBe('fanduel');
|
||||
});
|
||||
test('a both-sided first row is never displaced', () => {
|
||||
const bothSided = { player: 'A Guy', stat_type: 'hits', line: 1.5, book: 'fanduel', over_odds: -110, under_odds: -110 };
|
||||
const oneSided = { player: 'A Guy', stat_type: 'hits', line: 1.5, book: 'betmgm', over_odds: -115, under_odds: null };
|
||||
const idx = indexProps([bothSided, oneSided]);
|
||||
expect(idx['a guy|hits'].book).toBe('fanduel');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user