167996d99a
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.2 KiB
JavaScript
95 lines
3.2 KiB
JavaScript
/**
|
|
* MLB matchup context helpers (Session 15).
|
|
*
|
|
* Two pure functions, no I/O, no state:
|
|
* - platoonAdvantage(pitcherHand, batterHand)
|
|
* - projectedPA(lineupPosition)
|
|
*
|
|
* Sources:
|
|
* - Platoon splits: the standard MLB convention — opposite-handed
|
|
* matchups favor the batter (LHP vs RHB, RHP vs LHB). Switch
|
|
* hitters get the advantage in either matchup because they bat
|
|
* opposite-handed by definition.
|
|
* - Lineup → projected plate appearances: derived from MLB-average
|
|
* team PA distributions per lineup slot (2024 league composite).
|
|
* A leadoff hitter sees ~4.7 PA in a 9-inning game; the 9-hole
|
|
* sees ~3.5. Numbers from baseball-reference team batting tables
|
|
* averaged across all 30 teams.
|
|
*
|
|
* Callers (computeFeatures MLB branch) attach the outputs to the
|
|
* feature vector as `platoon_advantage` (bool|null) and
|
|
* `projected_pa` (number|null). When inputs are absent or invalid
|
|
* the helpers return null — engine1 treats null as a neutral signal
|
|
* and reasoning omits the line gracefully.
|
|
*/
|
|
|
|
const HAND_VALUES = new Set(['L', 'R', 'S']);
|
|
|
|
function normalizeHand(hand) {
|
|
if (!hand) return null;
|
|
const h = String(hand).trim().toUpperCase().charAt(0);
|
|
return HAND_VALUES.has(h) ? h : null;
|
|
}
|
|
|
|
/**
|
|
* platoonAdvantage — true when the batter has the platoon edge.
|
|
*
|
|
* LHP vs RHB → true (right-handed batter sees pitches better)
|
|
* LHP vs LHB → false
|
|
* RHP vs RHB → false
|
|
* RHP vs LHB → true
|
|
* any vs Switch hitter → true (switch hits opposite of pitcher)
|
|
*
|
|
* Returns null when either hand is unknown. The grading engine reads
|
|
* `null` as "no signal" — same treatment as `false` for confidence
|
|
* arithmetic, but reasoning will skip the line.
|
|
*/
|
|
function platoonAdvantage(pitcherHand, batterHand) {
|
|
const p = normalizeHand(pitcherHand);
|
|
const b = normalizeHand(batterHand);
|
|
if (!p || !b) return null;
|
|
if (b === 'S') return true; // switch hitter — always has edge
|
|
if (p === b) return false; // same-handed → pitcher edge
|
|
return true; // opposite-handed → batter edge
|
|
}
|
|
|
|
// League-average plate appearances per lineup slot in 9-inning games.
|
|
// Source: 2024 MLB composite (baseball-reference team batting tables).
|
|
// Slot 1 (leadoff) leads the team; later slots see fewer PAs because
|
|
// they may not bat in the bottom of innings already wrapped up.
|
|
const PA_BY_SLOT = Object.freeze({
|
|
1: 4.70,
|
|
2: 4.55,
|
|
3: 4.43,
|
|
4: 4.31,
|
|
5: 4.19,
|
|
6: 4.07,
|
|
7: 3.95,
|
|
8: 3.83,
|
|
9: 3.71,
|
|
});
|
|
|
|
/**
|
|
* projectedPA — expected plate appearances by lineup position.
|
|
*
|
|
* @param {number|string} position 1..9
|
|
* @returns {number|null}
|
|
*
|
|
* Out-of-range or invalid → null. This keeps reasoning honest when
|
|
* the odds payload doesn't carry batting order yet (the more common
|
|
* case today — odds-api doesn't expose lineup position in the prop
|
|
* envelope).
|
|
*/
|
|
function projectedPA(position) {
|
|
if (position == null) return null;
|
|
const p = Number(position);
|
|
if (!Number.isInteger(p) || p < 1 || p > 9) return null;
|
|
return PA_BY_SLOT[p];
|
|
}
|
|
|
|
module.exports = {
|
|
platoonAdvantage,
|
|
projectedPA,
|
|
__internals: { normalizeHand, HAND_VALUES, PA_BY_SLOT },
|
|
};
|