Session 15: Intelligence hardening — park factors, weather, Tank01 prefetch, pace factors, signal audit, founder pricing fix (1405 tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-11 16:21:18 -04:00
parent f5d79cf70d
commit 167996d99a
20 changed files with 1550 additions and 28 deletions
+66
View File
@@ -0,0 +1,66 @@
/**
* NBA team pace factors (Session 15).
*
* Source: NBA.com/stats team pace data 2024-25 season (possessions
* per 48 minutes, league average ~98). Indexed to 100 = league
* average so values compose multiplicatively with the player's
* baseline stats — a 105-pace team adds ~5% to counting stats
* (points, rebounds, assists, etc.); a 94-pace team subtracts ~6%.
*
* Update annually before the playoffs (the regular-season composite
* is stable by All-Star break). For mid-season grades, consider
* blending this static table with the live `team_pace` value the
* orchestrator already computes from game logs.
*/
const PACE_FACTORS = Object.freeze({
// East
ATL: 103, BOS: 99, BKN: 100, CHA: 102, CHI: 98,
CLE: 96, DET: 102, IND: 105, MIA: 98, MIL: 99,
NYK: 95, ORL: 94, PHI: 99, TOR: 100, WAS: 102,
// West
DAL: 99, DEN: 99, GSW: 102, HOU: 102, LAC: 99,
LAL: 100, MEM: 102, MIN: 99, NOP: 100, OKC: 100,
PHX: 99, POR: 102, SAC: 104, SAS: 100, UTA: 102,
});
const LEAGUE_AVERAGE = 100;
function normalizeTeamCode(team) {
if (!team) return null;
return String(team).trim().toUpperCase();
}
/**
* getPaceFactor — returns null for unknown teams so the feature
* extractor drops the signal entirely rather than reporting "league-
* average pace" on a typo.
*/
function getPaceFactor(team) {
const code = normalizeTeamCode(team);
if (!code) return null;
// The grading orchestrator uses some legacy abbreviations:
// NJN → BKN (Nets relocated 2012)
// NOH/NOK → NOP (Pelicans rebrand)
// SEA → OKC (Sonics relocated)
// CHO → CHA (Hornets rebrand)
// Folded here so old game-log teams still resolve to current pace.
const alias = ALIASES[code];
return PACE_FACTORS[alias || code] || null;
}
const ALIASES = Object.freeze({
NJN: 'BKN',
NOH: 'NOP',
NOK: 'NOP',
SEA: 'OKC',
CHO: 'CHA',
});
module.exports = {
PACE_FACTORS,
LEAGUE_AVERAGE,
ALIASES,
getPaceFactor,
__internals: { normalizeTeamCode },
};
+107
View File
@@ -0,0 +1,107 @@
/**
* MLB park factors (Session 15).
*
* Source: FanGraphs 2024-25 park factor data (three-year weighted
* average — FanGraphs methodology). Numbers indexed to 100 = league
* average. Pulled from https://www.fangraphs.com/guts.aspx (Park
* Factors tab) 2025-04. Update annually; the rolling-three-year
* window means values move slowly.
*
* Fields per park:
* hr — home run factor (Coors ~128 means HRs are 28% more likely;
* Oracle/SF ~85 means 15% less likely)
* h — overall hit factor
* r — runs factor
*
* The feature extractor reads this via `getParkFactor(homeTeam)`
* during the MLB branch and merges hr/h/r as `park_hr`, `park_h`,
* `park_r` so the grading engine + reasoning builder can read them
* without re-doing the lookup.
*
* Indexing convention: home-team abbreviation. The road team plays
* at the home team's park, so a Yankees-at-Angels game uses LAA's
* park factors regardless of whose batter you're grading.
*/
const PARK_FACTORS = Object.freeze({
// AL East
BAL: { hr: 109, h: 100, r: 102 }, // Camden Yards — left field wall changes leveled off post-2022
BOS: { hr: 100, h: 108, r: 106 }, // Fenway — Monster boosts singles/doubles, suppresses HR
NYY: { hr: 114, h: 101, r: 104 }, // Yankee Stadium — short porch in right
TB: { hr: 93, h: 96, r: 94 }, // Tropicana — neutral-to-pitcher dome
TOR: { hr: 106, h: 101, r: 102 }, // Rogers Centre — slight HR boost
// AL Central
CHW: { hr: 109, h: 102, r: 105 }, // Guaranteed Rate Field
CLE: { hr: 96, h: 99, r: 98 }, // Progressive Field — slight pitcher park
DET: { hr: 90, h: 100, r: 97 }, // Comerica — deep alleys, HR-suppressed
KC: { hr: 93, h: 103, r: 101 }, // Kauffman — XBH-friendly, HR-neutral
MIN: { hr: 105, h: 100, r: 101 }, // Target Field
// AL West
HOU: { hr: 100, h: 101, r: 101 }, // Minute Maid — Crawford boxes ~neutral, dome
LAA: { hr: 97, h: 98, r: 98 }, // Angel Stadium
OAK: { hr: 91, h: 95, r: 92 }, // Coliseum — extreme pitcher park
SEA: { hr: 96, h: 98, r: 96 }, // T-Mobile Park — marine air
TEX: { hr: 104, h: 102, r: 103 }, // Globe Life — climate-controlled since 2020
// NL East
ATL: { hr: 103, h: 100, r: 101 }, // Truist Park
MIA: { hr: 89, h: 97, r: 94 }, // loanDepot park — deep alleys
NYM: { hr: 95, h: 99, r: 97 }, // Citi Field — slight pitcher
PHI: { hr: 109, h: 100, r: 102 }, // Citizens Bank Park — bandbox right field
WSH: { hr: 100, h: 100, r: 100 }, // Nationals Park — true neutral
// NL Central
CHC: { hr: 106, h: 100, r: 102 }, // Wrigley — wind-driven swings; multi-year average
CIN: { hr: 113, h: 102, r: 105 }, // Great American — top-3 HR park most years
MIL: { hr: 104, h: 100, r: 101 }, // American Family Field
PIT: { hr: 96, h: 101, r: 99 }, // PNC Park — XBH-favorable, HR-suppressed
STL: { hr: 93, h: 98, r: 96 }, // Busch — neutral-to-pitcher
// NL West
ARI: { hr: 100, h: 98, r: 98 }, // Chase Field — humidor since 2018
COL: { hr: 128, h: 112, r: 120 }, // Coors Field — altitude. Single biggest park effect.
LAD: { hr: 102, h: 99, r: 99 }, // Dodger Stadium
SD: { hr: 95, h: 97, r: 95 }, // Petco — marine air pitcher park
SF: { hr: 85, h: 96, r: 92 }, // Oracle Park — Bay winds suppress HRs heavily
});
const NEUTRAL = Object.freeze({ hr: 100, h: 100, r: 100 });
function normalizeTeamCode(team) {
if (!team) return null;
return String(team).trim().toUpperCase();
}
/**
* getParkFactor — lookup by home-team code. Returns null for unknown
* teams so the feature extractor can drop the signal entirely rather
* than falsely report "neutral park" on a misspelled abbreviation.
*
* @param {string} homeTeam — three-letter team code (BAL, NYY, COL, ...)
* @returns {{hr:number, h:number, r:number}|null}
*/
function getParkFactor(homeTeam) {
const code = normalizeTeamCode(homeTeam);
if (!code) return null;
return PARK_FACTORS[code] || null;
}
/**
* Convenience — when you want a guaranteed object (e.g. arithmetic
* downstream that can't handle null). Falls back to league-neutral.
* Prefer getParkFactor + explicit-null branching in code; this is
* for tests and downstream services that prefer 100s over null.
*/
function getParkFactorOrNeutral(homeTeam) {
return getParkFactor(homeTeam) || NEUTRAL;
}
module.exports = {
PARK_FACTORS,
NEUTRAL,
getParkFactor,
getParkFactorOrNeutral,
__internals: { normalizeTeamCode },
};
+103
View File
@@ -0,0 +1,103 @@
/**
* Venue coordinates + dome flags (Session 15).
*
* Sources:
* - MLB stadium coordinates: Wikipedia infoboxes, cross-checked against
* each team's "stadium" entry on baseball-reference.com (2025).
* - World Cup 2026 venues: official FIFA host-city announcements + the
* static venue list already in `src/data/worldcup2026.js`.
* - Dome flag = roof either fully closed or retractable. Retractable
* stadiums count as dome for weather purposes (operators close the
* roof when conditions warrant — weather doesn't drive grade).
*
* Lat/lon precision: 4 decimal places (~10m). Sufficient for an
* Open-Meteo grid-cell lookup. Higher precision would be theater.
*/
// ---- MLB ----
const MLB_VENUES = Object.freeze({
// AL East
BAL: { lat: 39.2839, lon: -76.6217, dome: false, name: 'Camden Yards' },
BOS: { lat: 42.3467, lon: -71.0972, dome: false, name: 'Fenway Park' },
NYY: { lat: 40.8296, lon: -73.9262, dome: false, name: 'Yankee Stadium' },
TB: { lat: 27.7682, lon: -82.6534, dome: true, name: 'Tropicana Field' },
TOR: { lat: 43.6414, lon: -79.3894, dome: true, name: 'Rogers Centre' },
// AL Central
CHW: { lat: 41.8300, lon: -87.6338, dome: false, name: 'Guaranteed Rate Field' },
CLE: { lat: 41.4962, lon: -81.6852, dome: false, name: 'Progressive Field' },
DET: { lat: 42.3390, lon: -83.0485, dome: false, name: 'Comerica Park' },
KC: { lat: 39.0517, lon: -94.4803, dome: false, name: 'Kauffman Stadium' },
MIN: { lat: 44.9817, lon: -93.2778, dome: false, name: 'Target Field' },
// AL West
HOU: { lat: 29.7572, lon: -95.3553, dome: true, name: 'Minute Maid Park' },
LAA: { lat: 33.8003, lon: -117.8827, dome: false, name: 'Angel Stadium' },
OAK: { lat: 37.7516, lon: -122.2005, dome: false, name: 'Oakland Coliseum' },
SEA: { lat: 47.5914, lon: -122.3324, dome: true, name: 'T-Mobile Park' }, // retractable
TEX: { lat: 32.7475, lon: -97.0822, dome: true, name: 'Globe Life Field' }, // retractable
// NL East
ATL: { lat: 33.8908, lon: -84.4678, dome: false, name: 'Truist Park' },
MIA: { lat: 25.7781, lon: -80.2197, dome: true, name: 'loanDepot park' }, // retractable
NYM: { lat: 40.7571, lon: -73.8458, dome: false, name: 'Citi Field' },
PHI: { lat: 39.9061, lon: -75.1665, dome: false, name: 'Citizens Bank Park' },
WSH: { lat: 38.8730, lon: -77.0074, dome: false, name: 'Nationals Park' },
// NL Central
CHC: { lat: 41.9484, lon: -87.6553, dome: false, name: 'Wrigley Field' },
CIN: { lat: 39.0975, lon: -84.5067, dome: false, name: 'Great American Ball Park' },
MIL: { lat: 43.0280, lon: -87.9712, dome: true, name: 'American Family Field' }, // retractable
PIT: { lat: 40.4469, lon: -80.0058, dome: false, name: 'PNC Park' },
STL: { lat: 38.6226, lon: -90.1928, dome: false, name: 'Busch Stadium' },
// NL West
ARI: { lat: 33.4453, lon: -112.0667, dome: true, name: 'Chase Field' }, // retractable
COL: { lat: 39.7559, lon: -104.9942, dome: false, name: 'Coors Field' },
LAD: { lat: 34.0739, lon: -118.2400, dome: false, name: 'Dodger Stadium' },
SD: { lat: 32.7073, lon: -117.1566, dome: false, name: 'Petco Park' },
SF: { lat: 37.7786, lon: -122.3893, dome: false, name: 'Oracle Park' },
});
function normalizeCode(code) {
if (!code) return null;
return String(code).trim().toUpperCase();
}
function getMlbVenue(teamCode) {
const code = normalizeCode(teamCode);
if (!code) return null;
return MLB_VENUES[code] || null;
}
// ---- World Cup 2026 ----
// Sourced from src/data/worldcup2026.js (the canonical list of host
// venues + altitudes). Added lat/lon + dome flag here to keep the
// weather-fetch path independent of the i18n/altitude consumer.
const WC_VENUES = Object.freeze({
'MetLife Stadium': { lat: 40.8135, lon: -74.0746, dome: false },
'AT&T Stadium': { lat: 32.7473, lon: -97.0945, dome: true }, // retractable
'SoFi Stadium': { lat: 33.9535, lon: -118.3392, dome: true }, // partial roof
'Hard Rock Stadium': { lat: 25.9580, lon: -80.2389, dome: false },
'Lincoln Financial Field': { lat: 39.9008, lon: -75.1675, dome: false },
'Lumen Field': { lat: 47.5952, lon: -122.3316, dome: false },
'Gillette Stadium': { lat: 42.0909, lon: -71.2643, dome: false },
'Mercedes-Benz Stadium': { lat: 33.7553, lon: -84.4006, dome: true }, // retractable
'NRG Stadium': { lat: 29.6847, lon: -95.4107, dome: true }, // retractable
'Arrowhead Stadium': { lat: 39.0489, lon: -94.4839, dome: false },
'BC Place': { lat: 49.2768, lon: -123.1116, dome: true }, // retractable
'BMO Field': { lat: 43.6332, lon: -79.4185, dome: false },
'Estadio Azteca': { lat: 19.3030, lon: -99.1503, dome: false },
'Estadio BBVA': { lat: 25.6692, lon: -100.2444, dome: false },
'Estadio Akron': { lat: 20.6817, lon: -103.4625, dome: false },
"Levi's Stadium": { lat: 37.4032, lon: -121.9698, dome: false },
});
function getWcVenueCoords(name) {
if (!name) return null;
return WC_VENUES[name] || null;
}
module.exports = {
MLB_VENUES,
WC_VENUES,
getMlbVenue,
getWcVenueCoords,
__internals: { normalizeCode },
};