Job 1: per-sport snapshot cadence (config, not baseball's rhythm for all)
Every ACTIVE sport was graded at all five MLB slots (14/19/22/1/3 UTC). Sports post lines on different clocks, so that inheritance was wasteful both ways: WNBA props aren't posted at 14:00 UTC (10am ET) → that slot always graded 0 (the audit's "wnba:0"); soccer odds come from the 500/MONTH odds-api key, so five slots/day is a third of the budget for 1-2 matches. New src/config/sportCadence.js is the single source of truth (config-over- constants). Mapped from reality + quota headroom (PropLine 9k/day abundant, odds-api 500/mo scarce): mlb 14/19/22/1/3 intraday (full grid — games+props all day) nba 14/19/22/1/3 intraday (in-season fits; off-season self-skips empty) wnba 19/22/1 intraday (afternoon→evening ET; drops the 14/3 waste) soccer 14/19 NO intraday (WC live; 2 lean odds-api reads, key-protected) The scheduler still fires at HOURS_UTC and the missed-cron watchdog still references MLB (which runs every grid hour) — each slot now grades only sportsForHour(h), and only intradaySports() get the 20-min refresh. Every sport's hours are kept a subset of the firing grid (a boot-time guard + a test warn if that's ever violated). Retune a sport by editing one table row. Adaptive, not constant: near-zero when a sport is quiet, protecting the scarce odds-api quota from being drained by noon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,13 @@ const HOURS_UTC = (process.env.SNAPSHOT_HOURS_UTC || '14,19,22,1,3')
|
||||
.map((n) => parseInt(n, 10))
|
||||
.filter((n) => Number.isInteger(n) && n >= 0 && n <= 23);
|
||||
|
||||
// Job 1 — per-sport cadence. The scheduler still FIRES at HOURS_UTC (and the
|
||||
// missed-cron watchdog still references MLB, which runs at every grid hour), but
|
||||
// each slot grades only the sports whose lines actually post/move then, and only
|
||||
// PropLine-backed sports get the 20-min intraday refresh (soccer is odds-api
|
||||
// quota-gated). All of that lives in the config table, not here.
|
||||
const cadence = require('./config/sportCadence');
|
||||
|
||||
// Session 56 — missed-cron detection (pure, testable).
|
||||
// The latest scheduled hour:00 (UTC) at or before `date`. null if none in 48h.
|
||||
function mostRecentExpectedSlot(date, hours = HOURS_UTC) {
|
||||
@@ -204,9 +211,12 @@ function startSnapshotScheduler(opts = {}) {
|
||||
}
|
||||
} catch { /* alarm evaluation must never break the tick */ }
|
||||
try {
|
||||
const results = await runAll();
|
||||
// Job 1 — only the sports whose cadence includes this hour. MLB runs every
|
||||
// grid hour; WNBA skips 14/3 UTC (props not posted); soccer runs 14/19 only.
|
||||
const scheduled = cadence.sportsForHour(h);
|
||||
const results = await runAll({ sports: scheduled });
|
||||
const ok = results.filter((r) => r.status === 'ok');
|
||||
console.log(`[snapshot] cron fired ${h}:00 UTC — ${ok.length}/${results.length} sports graded`);
|
||||
console.log(`[snapshot] cron fired ${h}:00 UTC — ${ok.length}/${results.length} sports graded (scheduled: ${scheduled.join(',') || 'none'})`);
|
||||
// Session 63 (A1-S4) — after the day's FIRST slot, tell Kev the media
|
||||
// pack is ready. One ping, only when something actually graded.
|
||||
if (h === HOURS_UTC[0] && ok.length > 0) {
|
||||
@@ -251,7 +261,9 @@ function startSnapshotScheduler(opts = {}) {
|
||||
if (slot === lastRefreshSlot) return;
|
||||
lastRefreshSlot = slot;
|
||||
try {
|
||||
const results = await refreshAll();
|
||||
// Job 1 — only PropLine-backed sports refresh intraday (9k/day headroom).
|
||||
// Soccer is excluded: a 20-min odds-api refresh would drain the 500/mo key.
|
||||
const results = await refreshAll({ sports: cadence.intradaySports() });
|
||||
const touched = results.filter((r) => r.status === 'ok');
|
||||
const revised = results.reduce((n, r) => n + (r.revised || 0), 0);
|
||||
if (touched.length > 0) console.log(`[intraday] refresh — ${touched.length} sports, ${revised} public revisions`);
|
||||
@@ -263,6 +275,15 @@ 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)`}`);
|
||||
// Job 1 — state each sport's cadence at boot so "when does WNBA grade?" is
|
||||
// answerable from logs, and warn if a sport is scheduled at an hour the
|
||||
// scheduler never fires (it would silently never run).
|
||||
const cadenceSummary = cadence.ALL_SPORTS.map((s) => `${s}=${cadence.hoursFor(s).join('/') || '—'}${cadence.SPORT_CADENCE[s].intraday ? '' : ' (no intraday)'}`).join(' ');
|
||||
console.log(`[snapshotScheduler] per-sport cadence — ${cadenceSummary}`);
|
||||
const orphanHours = cadence.ALL_HOURS.filter((h) => !HOURS_UTC.includes(h));
|
||||
if (orphanHours.length > 0) {
|
||||
console.warn(`[snapshotScheduler] WARNING — sports scheduled at ${orphanHours.join(',')} UTC but the scheduler never fires then (not in hours=${HOURS_UTC.join(',')}); those slots will silently never run. Add them to SNAPSHOT_HOURS_UTC.`);
|
||||
}
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user