Track U-deg pt2 (edge_pct scale) + the dispersion classifier as open items
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
This commit is contained in:
@@ -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
|
`LADDER_ODDS_MAX` (+400), `MIN_RUNG_PROBABILITY` (0.25), the no-odds refusal, and the
|
||||||
folded `projection > 0` check.
|
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
|
### Backtest harness
|
||||||
**None exists** (grep-verified). `migrations/006` defines `grade_outcomes` +
|
**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
|
`player_calibrated_weights` and **no code reads or writes them**. The G-b/C-cal replay was
|
||||||
|
|||||||
Reference in New Issue
Block a user