Files
vyndr/src/config/sportCadence.js
T
builtbykev 4cd933d83e 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>
2026-07-15 16:17:55 -04:00

100 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 12 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,
};