From bf8ecb45ad90166e6c703021a54729241458a140 Mon Sep 17 00:00:00 2001 From: Kev Date: Sun, 19 Jul 2026 19:08:04 -0400 Subject: [PATCH] Track U-deg pt2 (edge_pct scale) + the dispersion classifier as open items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not new work β€” logging so neither gets lost. U-deg part 2: edge_pct is on a broken scale and it is the number users actually see. Live: edge_pct 100 on a single-digit-edge prop; ledger-wide 311/604 rows (51.5%) exceed the frontend's sane cap of 40, 39 exceed 100, worst 620. Mapped the consumers, and the split is the whole problem: 13 frontend files + deskShowcase/contentTemplate/parlayScan/tierGating read the BROKEN edge_pct, and ledgerService:199 persists it to the column of an append-only table right now. NOTHING on the frontend reads ev_pct; only heroPropService does. Noted that S-b (rank board on EV) is the real remedy and should be done as one piece with the scale fix, and that EDGE_BOARD_SANE_MAX is damage control that nulls half the board. Dispersion classifier: MIN_MEAN=4 is the honest stopgap; it leaves a +/-1.0 dead for MLB low-count stats. The scale-free fix is variance/mean vs the Poisson baseline of 1.0. Logged with its explicit validation bar β€” backtest harness first (still does not exist), replay settled outcomes, show no tier degradation, report before flipping, env-gate it. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA --- specs/model-train.md | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/specs/model-train.md b/specs/model-train.md index 034a6d3..6ee663e 100644 --- a/specs/model-train.md +++ b/specs/model-train.md @@ -222,6 +222,56 @@ back as a data-backed decision, never a silent flip. `LADDER_ODDS_MAX` (+400), `MIN_RUNG_PROBABILITY` (0.25), the no-odds refusal, and the folded `projection > 0` check. +### πŸ”΄ OPEN β€” U-deg PART 2: `edge_pct` IS ON A BROKEN SCALE (next U-deg item) + +`projection == 0` is closed. **`edge_pct` is not**, and it is the one still being +read by users. Live proof from the S63 fingerprint response: `edge_pct: 100` on a +prop whose real edge is single-digit. Ledger-wide: **311/604 rows (51.5 %) exceed +`EDGE_BOARD_SANE_MAX` (40), 39 rows exceed 100 (impossible as a percentage), worst +620.** + +**Who reads what β€” this is the problem in one line: every user-facing surface reads +the broken number, and nothing reads the good one.** + +| | Consumers | +|---|---| +| **`edge_pct` (BROKEN)** β€” frontend | `Slate.tsx`, `PropRow.tsx`, `GradeCard.tsx`, `GradeResultCard.tsx`, `MobileEdgeBoard.tsx`, `TierRecord.tsx`, `DemoScan.tsx`, `SoccerGradeResult.tsx`, `slateAdapter.js`, `gradeAdapter.js`, `scan/page.tsx`, `soccer/page.tsx`, `api/scan/route.ts` | +| **`edge_pct` (BROKEN)** β€” backend | `deskShowcaseService`, `contentTemplateService`, `parlayScanService`, `tierGating` (free-tier "hook"), **`ledgerService:199` β†’ persists it to the `edge` column** | +| **`ev_pct` (GOOD)** β€” frontend | **NONE** | +| **`ev_pct` (GOOD)** β€” backend | `heroPropService` only | + +Consequences to fix together, not piecemeal: +1. The frontend's `EDGE_BOARD_SANE_MAX = 40` guard is **damage control that nulls + half the board** β€” it hides the bug rather than fixing it. +2. `ledger_entries.edge` is being written with garbage **right now**, permanently, + into an append-only table. Any future edge-based analysis inherits it. +3. **S-b (rank board on EV) is the real remedy** β€” it moves ranking off `edge_pct` + onto `ev_pct`, which is now live and correct. Do S-b and the edge-scale fix as + one piece of work. +4. Free tier is sold `edge_pct` as "the hook" (`tierGating:62`). That hook is + currently a wrong number. + +### πŸ”Ά OPEN β€” INDEX-OF-DISPERSION CONSISTENCY CLASSIFIER (modelling change) + +`CONSISTENCY_MIN_MEAN = 4` is an **honest stopgap, not the answer.** It stops the +NBA-tuned CV thresholds from stamping `boom_bust` on every low-count MLB stat +(`cv β‰ˆ 1/√mean`, so mean < 4 always trips `boom_bust`) β€” but the cost is that MLB +low-count stats get **no consistency factor at all**, leaving a Β±1.0 dead for the +sport that carries most of our volume. + +**The real fix:** classify on the **index of dispersion** (variance / mean) against +the Poisson baseline of 1.0 β€” under-dispersed (< 1) = genuinely consistent, +over-dispersed (> 1) = genuinely boom/bust. It is scale-free, so one set of +thresholds works for MLB hits (mean 0.6) and NBA points (mean 20) alike. + +**VALIDATION REQUIRED BEFORE IT SHIPS** β€” this changes grades, so per the standing +rule no weight change ships without a backtest: +1. Build the backtest harness (still does not exist β€” see below). +2. Replay settled outcomes with dispersion-based consistency vs the current floor. +3. Show hit-rate/ROI by grade tier does not degrade, and that the restored Β±1.0 + moves grades on merit rather than flooding one letter. +4. Report before flipping. Env-gate the switchover. + ### Backtest harness **None exists** (grep-verified). `migrations/006` defines `grade_outcomes` + `player_calibrated_weights` and **no code reads or writes them**. The G-b/C-cal replay was