/** * 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 }, };