Spec: metrics-engine north star + sourcing scope report (no code)
NORTH STAR (design philosophy, not built): VYNDR measures players by MODERN FUNCTION, not legacy label — the principle already under the archetype system, from Rashad Phillips' Basketball Position Metric. The rule: every proprietary metric is baselined against the player's functional ARCHETYPE's CURRENT-SEASON behavior, never the position's inherited standard. The edge is that the market often prices today's players against yesterday's baselines, so archetype-vs-position baseline disagreement is a repeatable mispricing. Generalizes across sports. Moat = proprietary metrics x current-game calibration x our private outcome data. Metrics ship as VALIDATED FAMILIES: hypothesis, flagged build, backtest, ship-or-delete with the negative result written down. Nothing is real until the harness proves it predicts better. SOURCING SCOPE (report, no code): MLB opponent strength IS derivable from statsapi, verified live — one free call returns all 30 teams' pitching splits (era/whip/avg/slg/ops/homeRuns/strikeOuts/HR9), which beats the ESPN field we were reaching for because it is STAT-SPECIFIC, exactly what opp_rank_stat wants. NBA/WNBA cannot use ESPN (its team endpoint carries only a team's own stats, no defensive rating or pace); options are stats.nba.com dashboards, deriving allowed-points from scoreboard finals we already fetch, or API-Sports. API-Sports is a fallback tier at best — 100/day will not survive per-team-per-day. ESPN stays last, always behind an adapter. Proposed the SOURCE-ADAPTER pattern: one interface per feed, config-driven primary+fallback per (sport x capability), normalized output so vendor quirks stay in adapters, fallback announced rather than silent, sources with zero callers deleted rather than left as corpses, and a health check that PAGES when a source returns empty or broken — where EMPTY IS A FAILURE. Tonight's crash (captured 0 / errored 15) and the months-null opp_rank_stat are both exactly what that check exists to catch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
This commit is contained in:
@@ -153,6 +153,132 @@ timestamp) is unchanged. `toHero` now also passes through
|
||||
|
||||
---
|
||||
|
||||
## 2C. METRICS-ENGINE NORTH STAR (design philosophy — NOT yet built)
|
||||
|
||||
**VYNDR classifies and measures players by MODERN FUNCTION, not legacy label.**
|
||||
This is the principle already underneath the archetype system, drawn from Rashad
|
||||
Phillips' *Basketball Position Metric* — the work VYNDR's classification started
|
||||
from. Writing it down so every future metric inherits it.
|
||||
|
||||
### The principle
|
||||
A "center" who shoots 7 threes a game is not doing a center's job. A contact
|
||||
hitter and a launch-angle hitter share the label "outfielder" and share almost
|
||||
nothing else. Positions are inherited labels; **function is what a player
|
||||
actually does this season.** VYNDR models function.
|
||||
|
||||
### The rule every metric must follow
|
||||
> **Every proprietary metric is baselined against the player's functional
|
||||
> ARCHETYPE's CURRENT-SEASON behavior — never against the position's inherited
|
||||
> or historical standard.**
|
||||
|
||||
A STRETCH BIG's rebounding is judged against how stretch bigs rebound *this
|
||||
season*, not against what centers rebounded in 2015.
|
||||
|
||||
### Why it is an edge, not just a nicety
|
||||
**The market frequently prices today's players against yesterday's baselines.**
|
||||
Books and public models lean on positional priors that lag the way the game is
|
||||
actually played. Where the archetype baseline and the positional baseline
|
||||
disagree, that gap is a real, repeatable mispricing — and it is ours to measure
|
||||
because we already classify by function.
|
||||
|
||||
### It generalizes across sports
|
||||
Launch-angle vs contact hitters · mobile vs pocket QBs · position-less wings ·
|
||||
bullpen-game openers vs traditional starters. The archetype registry is already
|
||||
cross-sport; the baseline rule is the same everywhere.
|
||||
|
||||
### The moat
|
||||
Proprietary metrics, calibrated to the CURRENT game, validated on **our private
|
||||
outcome data** (the ledger). None of those three is individually rare. Together,
|
||||
and compounding as the ledger grows, they are hard to copy.
|
||||
|
||||
### How metrics get built — VALIDATED FAMILIES, never a big-bang dump
|
||||
1. Propose ONE metric family with an explicit hypothesis about what it predicts.
|
||||
2. Implement behind a flag, computed but not surfaced.
|
||||
3. **Run the backtest harness: does it predict better WITH the metric than
|
||||
without?** Hit rate, ROI, Brier, CLV where capture exists.
|
||||
4. Ships only if it earns its place. Fails → deleted, and the negative result is
|
||||
written down so it is not re-proposed.
|
||||
5. Then, and only then, it becomes a surface.
|
||||
|
||||
**Nothing in this vision is real until the harness can prove a metric predicts
|
||||
better than without it.** The harness (Phase 2) is the gate for all of it.
|
||||
|
||||
## 2D. SOURCING SCOPE — REPORT (Session 64, no code written)
|
||||
|
||||
Triggered by tonight's failure: we wired `refreshTeamStats` into production and
|
||||
it crashed on every team for a shape change nobody noticed, because **it had no
|
||||
caller, no health check, and no alarm.** The feed was dead and invisible.
|
||||
|
||||
### Q: can opponent strength be DERIVED from data we already ingest?
|
||||
|
||||
**MLB — YES, and better than what we were reaching for. ✅ VERIFIED LIVE.**
|
||||
`GET https://statsapi.mlb.com/api/v1/teams/stats?season=YYYY&group=pitching&stats=season&sportIds=1`
|
||||
returns **all 30 teams in ONE call**, free, official, no quota, no auth. Verified
|
||||
keys include `era`, `whip`, `avg` (opponent batting average against), `obp`,
|
||||
`slg`, `ops`, `homeRuns`, `strikeOuts`, `runsScoredPer9`, `homeRunsPer9`.
|
||||
|
||||
That is strictly better than the ESPN field we tried to fetch, because it is
|
||||
**stat-specific** — which is what `opp_rank_stat` actually wants:
|
||||
|
||||
| Prop stat | Opponent-strength input |
|
||||
|---|---|
|
||||
| hits | opponent pitching `avg` (BAA) |
|
||||
| total_bases / home_runs | `slg`, `homeRunsPer9` |
|
||||
| strikeouts (batter) | team `strikeOuts` per BF |
|
||||
| earned_runs / pitcher props | opposing lineup's `ops` (group=hitting) |
|
||||
|
||||
Ranking is a normalize-across-30-teams pass we already have in `teamStatsCache`.
|
||||
|
||||
**NBA/WNBA — ESPN cannot do it.** Verified: `/teams/{id}/statistics` carries only
|
||||
a team's OWN stats (rebounds, FG%, blocks) — no defensive rating, no pace, no
|
||||
opponent-allowed anything. Options, in order: (a) `stats.nba.com` team
|
||||
dashboards, which DO carry real `DEF_RATING`/`PACE` (needs careful headers, is
|
||||
rate-limited, and is the source the offline Python service used); (b) derive
|
||||
allowed-points ourselves from scoreboard finals we already fetch — free, slower
|
||||
to build, fully under our control; (c) API-Sports basketball. **WNBA is the
|
||||
in-season priority.**
|
||||
|
||||
**API-Sports family** — we hold `API_FOOTBALL_KEY` (validated, free tier 100/day,
|
||||
active to 2027, currently dormant). Same vendor covers basketball/baseball on
|
||||
separate keys. Worth evaluating as the *fallback* tier, not primary: the free
|
||||
quota is thin (100/day) and would not survive a per-team-per-day pattern.
|
||||
|
||||
**ESPN stays LAST and always behind an adapter.** It is free and broad, but it is
|
||||
an undocumented site API that changes shape without notice — which is precisely
|
||||
what bit us tonight.
|
||||
|
||||
### PROPOSAL: the SOURCE-ADAPTER pattern (design only — not built)
|
||||
|
||||
Every feed behind one interface, so a source can be swapped without touching
|
||||
feature code, and **so a dead source is loud instead of invisible.**
|
||||
|
||||
```
|
||||
interface StatSource {
|
||||
id: 'mlb-statsapi' | 'espn' | 'nba-stats' | 'api-sports'
|
||||
supports(sport, capability): boolean // 'team_defense' | 'game_logs' | ...
|
||||
fetch(sport, capability, params): Promise<Normalized|null>
|
||||
health(): Promise<{ ok, checked_at, sample_nonempty, note }>
|
||||
}
|
||||
```
|
||||
|
||||
- **Registry, config-driven:** per (sport × capability), an ordered
|
||||
`[primary, ...fallbacks]`. MLB team_defense → `[mlb-statsapi, espn]`.
|
||||
WNBA team_defense → `[nba-stats, derived-from-scoreboard, espn]`.
|
||||
- **Normalized output only.** Adapters own every vendor quirk; nothing upstream
|
||||
learns a vendor's shape. (Tonight's `results.stats` object-vs-array would have
|
||||
been one adapter's problem, not a pipeline crash.)
|
||||
- **Health check that PAGES.** Each adapter self-tests on a known entity and
|
||||
asserts a NON-EMPTY, plausible result. Run on a schedule + before the snapshot.
|
||||
**Empty is a FAILURE, not a pass** — `captured: 0, errored: 15` must page, and
|
||||
so must `captured: 30` where every value is null.
|
||||
- **Fallback is announced, never silent.** Dropping to a fallback logs and pages
|
||||
once per period; a silent degrade is how we ended up with a feature that had
|
||||
been null for months.
|
||||
- **A source with zero callers is deleted or health-checked** — never left as a
|
||||
corpse to be wired up later, which is exactly what `refreshTeamStats` was.
|
||||
- **Capability coverage is a first-class report:** which (sport × capability)
|
||||
pairs have a live primary today. This is also the NFL/NBA/soccer scaling gate.
|
||||
|
||||
## 2B. FOUNDATION-FIRST RE-ORDER (Kev, 2026-07-19) — supersedes the arc order
|
||||
|
||||
New features are paused until the foundation is real. Phases run in order:
|
||||
|
||||
Reference in New Issue
Block a user