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

6.3 KiB
Raw Blame History

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 underjake 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:

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.