Reconcile the record: STATE.md header + Model Train arc 1 spec (docs only)
Arc 1 (7a925f4) shipped without a spec and without a STATE.md entry — the plan lived only in a session context that was lost. Both written from the code on disk, not from memory. - STATE.md: header was stale at a8e383e; now7a925f4(pushed, NOT yet deploy-fingerprinted). New top section records de-vig + EV + takeable/ value gates + hero v2 + the value triplet, the real config values, and the 274/3289 -> 276/3306 test baseline. - specs/model-train.md (NEW, per CLAUDE.md rule #1): every knob and its ACTUAL default (TAKEABLE_ODDS_CEILING -160, TAKEABLE_ODDS_MAX +200, VALUE_EV_THRESHOLD 2, JUICE_ODDS_FLOOR -400, RARE_EVENT_LINE_MAX 0.5); the gate AS BUILT (flat price-only refusal at -400 pre-feature, NOT edge-aware, no -250 wall; the -160..+200 band never refuses a grade and is enforced on the hero alone); the triplet + hero v2; and a checklist of what is NOT built. Recorded honestly rather than assumed: EDGE_FLEX_WALL, HARD_JUICE_WALL, LADDER_ODDS_MAX and MIN_RUNG_PROBABILITY do not exist in the codebase; no frontend reads ev_pct/fair_odds/model_odds/book_odds/suppressed_reason yet; the value fields are ungated to free tier; a second EV implementation (processing/EVCalculator.js) still coexists with devig.evPct. No engine code touched. 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:
+29
-1
@@ -1,5 +1,33 @@
|
|||||||
# VYNDR — STATE OF THE WORLD
|
# VYNDR — STATE OF THE WORLD
|
||||||
### As of `a8e383e` (main, deployed + fingerprint-verified live), 2026-07-18. This file opens every future session. Update it when a train ships.
|
### As of `7a925f4` (main, pushed to gitea; NOT yet fingerprinted live), 2026-07-19. This file opens every future session. Update it when a train ships.
|
||||||
|
|
||||||
|
## MODEL TRAIN — ARC 1 (ENGINE) SHIPPED (2026-07-19, on main `7a925f4`, pushed, NOT yet deploy-fingerprinted)
|
||||||
|
Spec written retroactively: `specs/model-train.md` (arc list + what's open). Goal of the
|
||||||
|
train: make "real opportunities at takeable prices" the ENGINE, not a display filter.
|
||||||
|
Arc 1 = steps 1–6, backend only. **No frontend consumes the new fields yet** — the
|
||||||
|
triplet/VALUE marker/refusal copy are Design's Session-2 surfaces.
|
||||||
|
- **De-vig** (`src/utils/devig.js`, NEW): two-way multiplicative de-vig → fair prob +
|
||||||
|
fair American price per side + `overround`. One side unpriced → fair is NULL, never
|
||||||
|
faked. Also `americanToImpliedProb` / `americanToDecimal` / `impliedProbToAmerican`.
|
||||||
|
- **EV** (`devig.evPct`): `ev_pct = modelProb × decimal − 1`, at the graded side's
|
||||||
|
ACTUAL price. This is the ranking signal now, replacing raw |model − consensus|.
|
||||||
|
- **Takeable / value gates** (`src/config/valueEngine.js`, NEW): `TAKEABLE_ODDS_CEILING`
|
||||||
|
**−160**, `TAKEABLE_ODDS_MAX` **+200**, `VALUE_EV_THRESHOLD` **2** (%) — all env-tunable.
|
||||||
|
`takeable` = price in band; `value` = takeable AND ev ≥ threshold. Strict null guards
|
||||||
|
(`Number(null)===0` would have made a missing price "takeable").
|
||||||
|
- **Hero v2** (`heroPropService`): the hero is the highest `ev_pct` among **takeable**
|
||||||
|
A/B reads — a huge model-vs-line gap on a −900 line is trivia, not an opportunity.
|
||||||
|
Falls back to most-recent-real graded read on an empty slate (unchanged).
|
||||||
|
- **Value triplet** on every graded read: `book_odds` · `fair_odds` · `model_odds`
|
||||||
|
(+ `fair_prob`, `overround`, `devig_method`). Wired inside `analyzeViaEngine1`'s
|
||||||
|
existing p_win/Kelly block; rides the `...result` spread through snapshot / hero /
|
||||||
|
scan. Documented in `BACKEND_HANDOFF.md`.
|
||||||
|
- **Test baseline: 276 suites / 3306 tests green** (was 274/3289), `next build` exit 0.
|
||||||
|
New suites: `devig.test.js`, `valueEngine.test.js` (+ `heroPropService.test.js` rewritten).
|
||||||
|
- **Knobs NOT in this arc** (asked about, confirmed absent from the codebase):
|
||||||
|
`EDGE_FLEX_WALL`, `HARD_JUICE_WALL`, `LADDER_ODDS_MAX`, `MIN_RUNG_PROBABILITY`.
|
||||||
|
The only price wall on disk is the flat `JUICE_ODDS_FLOOR` (−400) from the
|
||||||
|
rare-event work below — it is **not** edge-aware, and there is no −250 wall.
|
||||||
|
|
||||||
## RARE-EVENT UNDER SUPPRESSION (2026-07-19, on main)
|
## RARE-EVENT UNDER SUPPRESSION (2026-07-19, on main)
|
||||||
Betting-logic audit: the CONSENSUS-vs-MODEL board flooded with "doubles u0.5 · MODEL 0.2 · +edge" fake reads (juiced rare-event unders). Report: the doubles projection is REAL per-player (MLB_LOG_FIELD doubles→doubles; values varied 0.03/0.16/0.2/0.22) — NOT a flat fallback; the issue is purely structural.
|
Betting-logic audit: the CONSENSUS-vs-MODEL board flooded with "doubles u0.5 · MODEL 0.2 · +edge" fake reads (juiced rare-event unders). Report: the doubles projection is REAL per-player (MLB_LOG_FIELD doubles→doubles; values varied 0.03/0.16/0.2/0.22) — NOT a flat fallback; the issue is purely structural.
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
# MODEL TRAIN — VALUE ENGINE
|
||||||
|
|
||||||
|
**Status:** Arc 1 (steps 1–6) SHIPPED on main `7a925f4` (2026-07-19). Arcs 2+ OPEN.
|
||||||
|
**Written retroactively** (2026-07-19) per CLAUDE.md rule #1 — Arc 1 was built from a
|
||||||
|
plan that lived only in a session context that was lost. Everything in the "SHIPPED"
|
||||||
|
sections below was read off the code ON DISK, not from that plan or from memory.
|
||||||
|
|
||||||
|
**Doctrine (Kev):** VYNDR promotes bets people actually take — roughly the **−160 to
|
||||||
|
+200** band — that ALSO carry a genuine vig-free edge. Not plus-money-only, not heavy
|
||||||
|
chalk. *Grade* answers "is this a good read"; *value* answers "does the price pay you."
|
||||||
|
They are separate fields and must stay separate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. WHAT ARC 1 SHIPPED
|
||||||
|
|
||||||
|
### 1.1 De-vig — `src/utils/devig.js` (new)
|
||||||
|
|
||||||
|
Two-way **multiplicative** (proportional / normalized-implied-probability) de-vig.
|
||||||
|
Each side's implied probability is divided by the sum of both; the sum's excess over 1
|
||||||
|
is the `overround`. Method is recorded on the payload as `devig_method:'multiplicative'`.
|
||||||
|
|
||||||
|
| Export | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| `americanToImpliedProb(a)` | `+`: `100/(a+100)`; `−`: `−a/(−a+100)`. Null on non-finite or `0`. |
|
||||||
|
| `americanToDecimal(a)` | Total-return multiple incl. stake. Null on non-finite or `0`. |
|
||||||
|
| `impliedProbToAmerican(p)` | Null outside `(0,1)`. `p>0.5` → negative (favorite). |
|
||||||
|
| `devigTwoWay(overOdds, underOdds)` | `{method, overround, over:{fair_prob,fair_odds}, under:{…}}`. **Returns `null` if EITHER side is missing/invalid** — fair values are never synthesized from one side. |
|
||||||
|
| `evPct(modelProb, american)` | `modelProb × decimal − 1`, as a percentage rounded to **one decimal**. Null on bad input. |
|
||||||
|
|
||||||
|
Rounding: probabilities/overround to 3 dp (`round3`); `ev_pct` to 1 dp.
|
||||||
|
|
||||||
|
### 1.2 Config knobs — the ACTUAL values on disk
|
||||||
|
|
||||||
|
**`src/config/valueEngine.js`** (new — the promotion gates):
|
||||||
|
|
||||||
|
| Knob | Env var | Default in code | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `TAKEABLE_ODDS_CEILING` | `TAKEABLE_ODDS_CEILING` | **−160** | Most-juiced favorite we will PROMOTE. |
|
||||||
|
| `TAKEABLE_ODDS_MAX` | `TAKEABLE_ODDS_MAX` | **+200** | Longest dog we will PROMOTE. |
|
||||||
|
| `VALUE_EV_THRESHOLD` | `VALUE_EV_THRESHOLD` | **2** (i.e. 2 % EV) | Minimum EV for the `value` flag. |
|
||||||
|
|
||||||
|
- `isTakeable(american)` → `a >= −160 && a <= +200`. **Strict**: `null`, `''`,
|
||||||
|
`undefined`, and non-finite are `false` (the `Number(null)===0` fabrication trap).
|
||||||
|
- `isValue(american, evPct)` → `isTakeable(american) && Number.isFinite(evPct) && evPct >= 2`.
|
||||||
|
|
||||||
|
**`src/config/rareEventMarkets.js`** (pre-existing, from the same-day rare-event work —
|
||||||
|
the refusal layer that sits UNDERNEATH the promotion gates):
|
||||||
|
|
||||||
|
| Knob | Env var | Default in code | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `JUICE_ODDS_FLOOR` | `JUICE_ODDS_FLOOR` | **−400** | Graded side at or past this price → **refused, not graded**. |
|
||||||
|
| `RARE_EVENT_LINE_MAX` | — (constant) | **0.5** | Line at/below which the rare-event rules apply. |
|
||||||
|
| `RARE_EVENT_STATS` | — (constant) | `doubles, triples, home_runs, stolen_bases, steals, blocks` | Backup list for props with NO odds. |
|
||||||
|
|
||||||
|
**Knobs that DO NOT EXIST** (verified by grep across `src/`, `web/src/`, `tests/`):
|
||||||
|
`TAKEABLE_CEILING` (real name is `TAKEABLE_ODDS_CEILING`), `EDGE_FLEX_WALL`,
|
||||||
|
`HARD_JUICE_WALL`, `LADDER_ODDS_MAX`, `MIN_RUNG_PROBABILITY`. If those were in the
|
||||||
|
original plan, they are **unbuilt** — see §3.
|
||||||
|
|
||||||
|
### 1.3 What the gate ACTUALLY does today
|
||||||
|
|
||||||
|
Reported as-built, not as-designed. There are **two independent layers**, and only the
|
||||||
|
first one can refuse a grade:
|
||||||
|
|
||||||
|
**Layer A — the refusal wall (`JUICE_ODDS_FLOOR`, −400).** In `analyzeViaEngine1`, the
|
||||||
|
FIRST thing that happens — before `computeFeaturesForProp`, before any projection:
|
||||||
|
|
||||||
|
```
|
||||||
|
if (isTooJuiced(rawProp)) → suppressedRareResult('juiced_no_edge')
|
||||||
|
```
|
||||||
|
|
||||||
|
Characteristics of the shipped behavior:
|
||||||
|
- **A flat price wall, NOT edge-aware.** It looks only at the graded side's American
|
||||||
|
price. No EV, no projection, no edge, no flex band participates in the decision.
|
||||||
|
- **The wall is −400, not −250.** There is no −250 constant anywhere in the codebase.
|
||||||
|
- It runs **pre-feature**, so a refused prop costs zero feature computation.
|
||||||
|
- Odds absent → `isTooJuiced` is `false` (can't judge from a price that isn't there);
|
||||||
|
the structural `RARE_EVENT_STATS` rule is the backup for that case.
|
||||||
|
- Output is a refusal, not a bad grade: `grade:null`, `insufficient_data:true`,
|
||||||
|
`suppressed:true`, `suppressed_reason:'juiced_no_edge'`, plus branded "No read —…"
|
||||||
|
copy in `reasoning.summary`.
|
||||||
|
|
||||||
|
The other two refusals in the same family: `rare_event_under` (rare stat, under, line
|
||||||
|
≤ 0.5 — pre-feature, needs no projection) and `rare_event_over_below_line` (rare stat,
|
||||||
|
over, line ≤ 0.5, and the model does NOT project above the line — checked after the
|
||||||
|
projection exists).
|
||||||
|
|
||||||
|
**Layer B — the takeable band (−160 … +200).** This gate **never refuses a grade**.
|
||||||
|
It is a *promotion* filter, applied only where a read is featured:
|
||||||
|
- Today its ONLY enforcement point is `heroPropService.pickHeroProp`
|
||||||
|
(`if (!isTakeable(g.book_odds)) continue`).
|
||||||
|
- Every graded read still carries `takeable` / `value` as flags for consumers.
|
||||||
|
- The full board shows every graded read regardless of band. Parlay Lab is exempt by
|
||||||
|
design (juiced legs combine into takeable payouts).
|
||||||
|
|
||||||
|
So: **edge-aware promotion, price-only refusal.** The `-400` wall decides what gets
|
||||||
|
graded; EV + the `−160/+200` band decide what gets promoted.
|
||||||
|
|
||||||
|
### 1.4 The value triplet
|
||||||
|
|
||||||
|
Attached in `analyzeViaEngine1`'s existing p_win/Kelly `try` block (so it inherits the
|
||||||
|
"real quantile probability × real book odds, or nothing" rule, and the whole block is
|
||||||
|
additive — a throw never breaks the read):
|
||||||
|
|
||||||
|
| Field | Source | Absent when |
|
||||||
|
|---|---|---|
|
||||||
|
| `book_odds` | the graded side's actual price (`under_odds` if under, else `over_odds`) | no side odds |
|
||||||
|
| `fair_odds`, `fair_prob` | `devigTwoWay(over, under)` for the graded side | **either** side unpriced |
|
||||||
|
| `model_odds` | `impliedProbToAmerican(pWin)` | no quantile probability |
|
||||||
|
| `overround`, `devig_method` | from the de-vig | either side unpriced |
|
||||||
|
| `ev_pct` | `evPct(pWin, sideOdds)` | no pWin or no side odds |
|
||||||
|
| `takeable` | `isTakeable(sideOdds)` | no side odds |
|
||||||
|
| `value` | `isValue(sideOdds, ev)` | no ev |
|
||||||
|
|
||||||
|
The intended render (Design's, not built): *"book −145 · vig-free −132 · model −110."*
|
||||||
|
|
||||||
|
### 1.5 Hero v2 — `heroPropService`
|
||||||
|
|
||||||
|
Old rule: largest `|projection − line|` among A/B candidates. **New rule:** among A/B
|
||||||
|
candidates, keep only those with a finite `ev_pct` **and** a takeable `book_odds`, then
|
||||||
|
take the **highest `ev_pct`**. The `gap` is still computed and returned for display, but
|
||||||
|
it no longer selects. Empty-slate fallback (most recent real graded read, any grade, by
|
||||||
|
timestamp) is unchanged. `toHero` now also passes through
|
||||||
|
`ev_pct / value / takeable / book_odds / fair_odds / model_odds`.
|
||||||
|
|
||||||
|
### 1.6 Distribution + tests
|
||||||
|
|
||||||
|
- Fields ride the existing `...result` / `...data` spreads into `grades:{sport}`,
|
||||||
|
`/api/snapshot/:sport`, `/api/hero-prop`, `/api/scan`. Documented in
|
||||||
|
`BACKEND_HANDOFF.md` ("Value Engine fields").
|
||||||
|
- `src/utils/tierGating.js` deletes `alt_lines` (below Desk) and `kelly` (below the
|
||||||
|
Kelly tier). It does **not** touch the value fields — `ev_pct`, `value`, `takeable`,
|
||||||
|
and the triplet are currently visible to **every tier including free**. That is an
|
||||||
|
unmade product decision, not a verified choice (see §3).
|
||||||
|
- Suite: **276 suites / 3306 tests**, green, `next build` exit 0. New:
|
||||||
|
`tests/unit/devig.test.js`, `tests/unit/valueEngine.test.js`;
|
||||||
|
`tests/unit/heroPropService.test.js` rewritten for the EV rule.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. LAWS THIS TRAIN INHERITS (do not relax)
|
||||||
|
|
||||||
|
- **Absent beats wrong.** One side unpriced → `fair_odds` is absent, never
|
||||||
|
extrapolated. No price → `takeable` is absent, not `false`-by-coercion.
|
||||||
|
- **`Number(null) === 0`** is the standing fabrication bug of this codebase. Every new
|
||||||
|
numeric path gets a strict guard (`valueEngine.isTakeable` is the model).
|
||||||
|
- **Grade ≠ value.** An A read with `value:false` is honest output: right read, price
|
||||||
|
gone. Never let the value flag re-letter a grade.
|
||||||
|
- **Refusal is a product surface**, not an error. Refused reads carry branded copy.
|
||||||
|
- Market values are REAL book numbers at a timestamp; only projection / grade / edge /
|
||||||
|
EV / fair price are model output and must be labeled MODEL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. NOT YET BUILT — checklist to reconcile against the full arc list
|
||||||
|
|
||||||
|
Verified absent from the codebase as of `7a925f4`. Kev supplies the complete arc list;
|
||||||
|
mark each done/open against this.
|
||||||
|
|
||||||
|
**Engine / gates**
|
||||||
|
- [ ] Edge-aware juice wall — a soft/flex band where a big enough edge can survive a
|
||||||
|
juiced price (`EDGE_FLEX_WALL`, `HARD_JUICE_WALL` — neither exists; today's wall
|
||||||
|
is the single flat `JUICE_ODDS_FLOOR` −400).
|
||||||
|
- [ ] `TAKEABLE_ODDS_*` enforced on any surface beyond the daily hero (featured rows,
|
||||||
|
top-of-board, alerts). Today: hero only.
|
||||||
|
- [ ] Alt-line ladder value gates (`LADDER_ODDS_MAX`, `MIN_RUNG_PROBABILITY` — neither
|
||||||
|
exists). The ladder in `analyzeViaEngine1` is still the S62 build: fixed shifts
|
||||||
|
`[−1, −0.5, 0, +0.5, +1]`, filtered to `line > 0`, sorted by `edge_pct`. It has
|
||||||
|
**no odds ceiling and no per-rung probability floor**, and its rungs carry no
|
||||||
|
`ev_pct` / `fair_odds` / price at all.
|
||||||
|
- [ ] EV-rank the board itself. `slateAdapter.flattenToEdgeBoard` still ranks on
|
||||||
|
`edge` (guarded by `EDGE_BOARD_SANE_MAX = 40`), not `ev_pct`.
|
||||||
|
- [ ] Reconcile the orphan `src/services/processing/EVCalculator.js` (a separate,
|
||||||
|
older EV path used only by `UnifiedOddsProvider`) against `devig.evPct` — two EV
|
||||||
|
implementations currently coexist.
|
||||||
|
- [ ] Multi-way / three-way de-vig, and alternatives to multiplicative (Shin,
|
||||||
|
power/log). Two-way multiplicative is the only method implemented.
|
||||||
|
- [ ] Best-price de-vig across books (today the triplet de-vigs whichever single
|
||||||
|
book's two-sided price came through on the prop).
|
||||||
|
|
||||||
|
**Surfaces (nothing on the frontend reads these fields yet — grep-verified zero hits
|
||||||
|
for `ev_pct` / `fair_odds` / `model_odds` / `book_odds` / `suppressed_reason` under
|
||||||
|
`web/src/`)**
|
||||||
|
- [ ] Value triplet rendered on the reveal card / board row / hero.
|
||||||
|
- [ ] VALUE marker (and the honest "A but no value" state).
|
||||||
|
- [ ] Visible-refusal moment — the "No read —…" copy is generated and shipped on the
|
||||||
|
payload but is not displayed anywhere.
|
||||||
|
- [ ] EV shown as the headline number instead of / alongside edge %.
|
||||||
|
|
||||||
|
**Product / policy**
|
||||||
|
- [ ] Tier policy for the value fields — currently ungated to free (§1.6).
|
||||||
|
- [ ] Alerts on takeable +EV reads (the config comment anticipates "future alerts").
|
||||||
|
- [ ] Ledger/record split by `value` (does the model's +EV subset beat its overall
|
||||||
|
record?). `ledger_entries` stores no EV or fair price today.
|
||||||
|
- [ ] Settlement/CLV interaction with fair price — CLV is still locked-vs-closing
|
||||||
|
LINE, not vs closing FAIR price. (Note: **C4 — CLV capture is broken** and
|
||||||
|
suppressed on public surfaces; see `specs/audit-data/clv-capture-broken.md`.
|
||||||
|
Any fair-price CLV work is blocked behind C4.)
|
||||||
|
|
||||||
|
**Verification debt**
|
||||||
|
- [ ] Arc 1 is **not deploy-fingerprinted**. Post-deploy, confirm `/api/hero-prop`
|
||||||
|
returns a hero with a takeable `book_odds` and a finite `ev_pct`, and that
|
||||||
|
`/api/snapshot/mlb` grades carry the triplet.
|
||||||
|
- [ ] Known upstream contamination: **`specs/audit-data/mlb-grade-degradation.md`** —
|
||||||
|
projection == 0 on ~9/25 MLB grades and a broken `edge_pct` scale. EV is
|
||||||
|
computed from the quantile probability, not from `edge_pct`, so it is not
|
||||||
|
directly poisoned — but hero ranking and any EV-vs-edge comparison sit on top of
|
||||||
|
that pipeline. Fix the degradation before trusting arc-1 numbers in aggregate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Arc 1 recorded 2026-07-19 from the code at `7a925f4`. Update this file in the same
|
||||||
|
commit as any arc that ships.*
|
||||||
Reference in New Issue
Block a user