4cd933d83e
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>
100 lines
4.6 KiB
JavaScript
100 lines
4.6 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Per-sport snapshot cadence (Job 1) — the config that replaces baseball's
|
||
* one-size-fits-all rhythm.
|
||
*
|
||
* WHY THIS EXISTS
|
||
* The snapshot cron fired every ACTIVE sport at all five MLB slots
|
||
* (14/19/22/1/3 UTC). But sports post their lines on entirely different
|
||
* clocks, so that inheritance was wasteful in two directions:
|
||
* - WNBA props aren't posted at 14:00 UTC (10am ET) → that slot always
|
||
* graded 0 for WNBA (the "wnba:0" the audit flagged). Wasted work.
|
||
* - Soccer odds come from the odds-api key (500/MONTH, scarce). Running it
|
||
* at five slots/day is a third of the monthly budget for 1–2 matches.
|
||
*
|
||
* THE RULE: high frequency only when a sport's lines actually post/move,
|
||
* near-zero otherwise. This is a SCHEDULING fix, not a volume one — "never a
|
||
* gap" must never become "pull constantly", because burning the odds-api
|
||
* quota by noon means nothing all afternoon (strictly worse than a gap).
|
||
*
|
||
* QUOTA HEADROOM (mapped from reality, 2026-07):
|
||
* - PropLine (mlb/nba/wnba props): 3 keys × 3,000/day = 9,000/day. Abundant.
|
||
* Five slots × a few sports × intraday is a rounding error against that.
|
||
* - The Odds API (soccer props + futures): 500/MONTH. Every soccer slot is
|
||
* ~1 credit. So soccer gets the fewest, best-timed slots and NO intraday
|
||
* refresh (a 20-min soccer refresh would burn ~36 credits/day = the whole
|
||
* month in ~2 weeks).
|
||
*
|
||
* DESIGN CONSTRAINT (low blast radius): every sport's hours are a SUBSET of the
|
||
* scheduler's firing grid (snapshotScheduler.HOURS_UTC, default 14/19/22/1/3).
|
||
* The scheduler still fires at those hours; this config just decides WHICH
|
||
* sports run at each one. Keeping the union inside the grid means the
|
||
* missed-cron watchdog (which references MLB) needs no change. If a sport ever
|
||
* genuinely needs an hour outside the grid, add it to HOURS_UTC too AND scope
|
||
* the overdue watchdog to MLB's hours (see snapshotScheduler).
|
||
*
|
||
* POSTING RHYTHMS (why each sport gets the hours it does):
|
||
* - mlb — games + props all day; the full grid is correct. Keep 14/19/22/1/3.
|
||
* - nba — off-season now (self-skips empty on PropLine, ~0 cost); in-season
|
||
* posts morning→afternoon ET. The full grid fits both. Keep.
|
||
* - wnba — games tip evening ET; props post ~afternoon ET. 19 UTC (3pm ET)
|
||
* catches early games, 22 UTC (6pm ET) the bulk, 1 UTC (9pm ET) the
|
||
* west-coast lock. 14 UTC (10am ET) and 3 UTC (11pm ET, games done)
|
||
* are dropped — that's the wasted "wnba:0" slot removed.
|
||
* - soccer— World Cup live now (final Jul 19), club leagues resume Aug. Lines
|
||
* post well ahead and barely move; two well-placed odds-api reads
|
||
* (14 UTC morning research, 19 UTC pre-afternoon-match lock) cover a
|
||
* match day for ~2 credits. NO intraday (protects the 500/mo key).
|
||
*
|
||
* config-over-constants: to retune a sport, edit ONLY this table.
|
||
*/
|
||
|
||
const SPORT_CADENCE = Object.freeze({
|
||
mlb: { hours: [14, 19, 22, 1, 3], intraday: true, source: 'propline' },
|
||
nba: { hours: [14, 19, 22, 1, 3], intraday: true, source: 'propline' },
|
||
wnba: { hours: [19, 22, 1], intraday: true, source: 'propline' },
|
||
soccer: { hours: [14, 19], intraday: false, source: 'odds-api' },
|
||
});
|
||
|
||
const ALL_SPORTS = Object.freeze(Object.keys(SPORT_CADENCE));
|
||
|
||
// The union of every sport's snapshot hours — the set the scheduler must fire
|
||
// at so no sport's slot is missed. Kept for the cross-check that this union is
|
||
// a subset of the scheduler grid.
|
||
const ALL_HOURS = Object.freeze(
|
||
[...new Set(ALL_SPORTS.flatMap((s) => SPORT_CADENCE[s].hours))].sort((a, b) => a - b),
|
||
);
|
||
|
||
/**
|
||
* sportsForHour — which sports are scheduled to snapshot at this UTC hour.
|
||
* @param {number} hour UTC hour 0..23
|
||
* @param {string[]} [sports] restrict to this candidate set (defaults to all)
|
||
* @returns {string[]}
|
||
*/
|
||
function sportsForHour(hour, sports = ALL_SPORTS) {
|
||
const h = Number(hour);
|
||
return sports.filter((s) => SPORT_CADENCE[s] && SPORT_CADENCE[s].hours.includes(h));
|
||
}
|
||
|
||
/** intradaySports — sports that opt into the 20-min intraday line refresh.
|
||
* Soccer is excluded on purpose (odds-api quota). */
|
||
function intradaySports(sports = ALL_SPORTS) {
|
||
return sports.filter((s) => SPORT_CADENCE[s] && SPORT_CADENCE[s].intraday);
|
||
}
|
||
|
||
/** hoursFor — a single sport's snapshot hours (empty array if unknown). */
|
||
function hoursFor(sport) {
|
||
const c = SPORT_CADENCE[String(sport || '').toLowerCase()];
|
||
return c ? c.hours.slice() : [];
|
||
}
|
||
|
||
module.exports = {
|
||
SPORT_CADENCE,
|
||
ALL_SPORTS,
|
||
ALL_HOURS,
|
||
sportsForHour,
|
||
intradaySports,
|
||
hoursFor,
|
||
};
|