Files
vyndr/specs/rank-on-pwin-challenger.md
T
builtbykev ef4ac60b81 Per-sport rank guard + edge diagnostic-only display + delta report
DELTA MEASURED on live prod grades (live ordering unchanged): MLB 7/8
props move (87.5%), mean 2.5 places, TOP READ CHANGES (corey seager hits
1.5 under -> jake burger hits 0.5 over). WNBA 25/25 move, mean 4.1, max 12.
This is a large re-ordering, not a tweak.

Caveat recorded rather than buried: MLB had only 8 graded props at
measurement time. The percentages are real; the sample is one small slate.
Re-run before the flip -- it is one call.

PER-SPORT DOCTRINE ENFORCED IN CODE. WNBA moves the most and must NOT
adopt this: its p_win is anti-predictive, so ranking that board by p_win
would sort it by a signal measured to point the WRONG WAY -- worse than
the incumbent, not better. A comment would not have stopped a future flip
from going global, so FORECAST_RANKED_SPORTS = Set(['mlb']) gates the
forecast_rank stamp, with tests asserting no sport inherits MLB's result.
A sport joins only by passing its own holdout.

EDGE IS NOW DIAGNOSTIC-ONLY IN DISPLAY. MobileEdgeBoard.EdgeCell rendered
green (--g-a) for positive edge and red (--miss) for negative. Two things
were wrong: green/red IS a quality claim on a quantity that does not
predict, and ROW-GRAMMAR reserves red for settled-negative ONLY -- a
negative diagnostic is not a settled loss. Now neutral mono with a
diagnostic tooltip; header reads "MKT GAP · DIAGNOSTIC". The number is
still shown -- no display went blank. DeskShowcase neutralised likewise.

PINNACLE LOGGED, NOT ENSHRINED. Per the order, "market-not-sharp" is
PENDING-RECOVERY rather than a confirmed permanent limitation. The single
question for PropLine is in BLOCKERS.md with its evidence, and MASTER-PLAN
now carries the pending status instead of the permanent claim.

Live sorts remain byte-identical: selectTopGrades, flattenToEdgeBoard and
topGradedService all still call the incumbent.

Gates: 4,041 tests / 323 suites green; next build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-08-01 01:29:20 -04:00

154 lines
6.3 KiB
Markdown
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.
# RANK ON p_win — CHALLENGER DELTA + EDGE RETIREMENT
**Date:** 2026-08-01 · **Live ordering byte-identical** · challenger measured on
live prod grades · edge retired from decisions and from display quality-signalling.
**Gates:** 4,039 tests / 323 suites green · `next build` exit 0 · delta recorded ·
live sorts untouched.
---
## WHY (the measurement that dictates this)
| instrument | corr with outcome, n=200 settled MLB |
|---|---:|
| **`p_win`** | **+0.26** |
| `p_win fair_prob` (v1 single-book ruler) | 0.010 |
| `p_win fair_prob` (v2 consensus ruler) | 0.022 |
**Subtracting the market destroys the signal, under both rulers.** A quantity
that does not predict must not rank, gate, or decide.
---
## THE CHALLENGER — `rankByForecast`
**Order:** takeable-gated `p_win` → grade tier → confidence → stable input order.
**No edge term anywhere** (a test flips edge from 99 to +99 and asserts the
order does not move).
**`p_win` leads and the letter follows — deliberately.** The grade letter measured
**r ≈ 0.005** against outcomes and is **inverted** (B 52.4% < C 56.9%), while
`p_win` measures **+0.26**. Leading with the letter would sort the board by the
weaker signal and use the stronger one only to break ties.
The takeable gate is unchanged and mandatory: raw `p_win` crowns 300 chalk.
### One thing worth stating precisely
**Isotonic calibration is a MONOTONE transform, so ranking on raw `p_win` and on
calibrated `p_win` produce the SAME ORDER.** Calibration matters when `p_win` is
*displayed* or *thresholded* — it cannot change a ranking. The order asked to
"rank on calibrated p_win"; for ranking specifically, that is a no-op relative to
raw. Recorded in the code so nobody re-derives it.
---
## THE DELTA (live prod grades, nothing flipped)
`GET /api/internal/ranking-delta` · `live_ordering_unchanged: true`
| | MLB | WNBA |
|---|---:|---:|
| graded props | 8 | 25 |
| `p_win` coverage | 100% | 100% |
| **props that move** | **7/8 (87.5%)** | **25/25 (100%)** |
| mean \|move\| | 2.5 places | 4.1 places |
| max move | 5 | 12 |
| top-10 overlap | 80% | 80% |
| **top read changes** | **YES** | **YES** |
MLB #1: `corey seager | hits 1.5 under``jake burger | hits 0.5 over`
(Seager falls 1 → 6).
**This is a large re-ordering, not a tweak.** Nearly every row moves and the
headline read changes.
**Caveat, stated because it matters:** MLB's slate had only **8 graded props** at
measurement time. The percentages are real but the sample is a single small
slate — re-run the endpoint on a full slate before the flip. It is one call.
---
## PER-SPORT DOCTRINE — ENFORCED IN CODE, NOT IN A COMMENT
**WNBA moves the most (100% of rows, mean 4.1 places) and must NOT adopt this.**
WNBA's `p_win` is **anti-predictive** on its own data — it abstains. Ranking that
board by `p_win` would sort it by a signal measured to point the *wrong way*:
worse than the incumbent, not better.
A comment would not have stopped a future flip from applying this globally, so:
```js
const FORECAST_RANKED_SPORTS = Object.freeze(new Set(['mlb']));
```
`ranksOnForecast(sport)` gates the `forecast_rank` stamp, and tests assert **no
sport inherits MLB's result** — a sport joins only by passing its **own** holdout
(honest calibration AND surviving resolution).
**The WNBA number above is informational only.** It is in the report to show what
the guard is preventing.
---
## WHAT CHANGED, WHAT DID NOT
### Changed now (not rankings, so not gated on the flip)
- **`altLineScanner.compareToBookImplied`** no longer returns
`value_detected: edge > 0`. **Edge is still computed and returned** — losing the
record would be worse than mis-using it — but the verdict is `null` with
`value_basis: 'retired:edge_does_not_predict'`.
- **`scanAltLines`** no longer filters to `edge > 0` nor calls the survivor
`optimal_line`. The whole ladder returns, ranked, labelled
`price_gap_diagnostic_unvalidated`. **The module has zero callers** (verified) —
unwired like `mlbGrader.js`; left in place and made honest rather than deleted.
- **`MobileEdgeBoard.EdgeCell`** no longer renders green for positive edge and red
for negative. Two things were wrong with that: green/red **is** a quality claim
on a quantity that does not predict, and **ROW-GRAMMAR reserves red for
settled-negative only** (miss/dead/stale/faded) — a negative diagnostic is not a
settled loss. Now neutral mono with a diagnostic tooltip; the column header
reads **`MKT GAP · DIAGNOSTIC`**. **The number is still shown** — no display went
blank.
- **`DeskShowcase`** edge colour neutralised for the same reason.
### An honest asymmetry I did not paper over
**Ranking props against each other must not use edge. Choosing between RUNGS of
the same prop is inherently price-relative** — ranking rungs by model probability
alone would always pick the lowest line, because P(over 0.5) > P(over 2.5) by
construction. So the gap stays the rung key in `scanAltLines`, **explicitly
labelled unvalidated**, rather than being replaced by something that would look
principled and be degenerate.
### NOT changed (challenger-first)
- **`rankGrades`** — the incumbent (grade-first, edge as 4th key) is untouched,
and tested as untouched.
- **Every live sort** still calls the incumbent. `selectTopGrades`,
`flattenToEdgeBoard`, `topGradedService` — all byte-identical.
- **`forecast_rank`** is stamped additively on MLB snapshot grades, **before**
`stripModelPrice`, so every tier would receive the correct order without the
paid values (the `topGradedService` precedent — an ordinal travels where the
magnitude cannot). **Nothing sorts by it yet.**
- **Edge stays stored** in the ledger, as required.
---
## THE FLIP, WHEN YOU WANT IT
One decision, three edits: `selectTopGrades` and `flattenToEdgeBoard` sort by
`forecast_rank` when present; `rankGrades` drops its edge key. **Re-run the delta
on a full MLB slate first** — 8 props is not a slate.
## PINNACLE — LOGGED, NOT ENSHRINED
Per the order: **"market-not-sharp" is PENDING-RECOVERY, not a confirmed permanent
limitation.** The question for PropLine is logged in `BLOCKERS.md`:
> *Why did Pinnacle MLB player-prop coverage stop on 2026-07-31?* Captures ran
> 103,940 over the prior 10 days, then 4,022 → 0 while every other book continued.
Until answered, the plan must not record "no sharp anchor exists" as permanent.