# Projections M3 Milestone 2 Ship Summary

Milestone 2 turns the M2 projection stack into the first real season-long betting-facing surface: team-talent scaling is stabilized, free futures markets are ingested daily, projection-vs-market fair-price gaps are computed with devigging, and the new `/betting/futures` page makes the output readable without slipping into tout language.

## Ladder Summary

| Step | Outcome | Primary artifact / report |
|---|---|---|
| Team-talent scaling fix | League-aware scaling replaced pooled scaling so AL/NL run environments no longer drift apart before the simulator and futures layers consume them | `data/derived/projections/team_talent_2026.parquet`, `data/derived/projections/standings_2026.parquet`, `data/derived/projections/playoff_odds_2026.parquet` |
| Futures / team-totals market ingestion | Free-source season futures are now captured into a normalized daily parquet instead of being manually inspected | `data/derived/betting/futures_markets_20260422.parquet`, `scripts/shared/fetch_futures_markets.py` |
| Devig + fair-price gap engine | Outright futures markets are now power-devigged and compared against Mithrandir projection probabilities with uncertainty context | `data/derived/betting/futures_edges_20260422.parquet`, `scripts/shared/build_futures_fair_price_gaps.py` |
| `/betting/futures` page | The public site now exposes the first season-long betting surface with explicit no-signal handling and uncertainty-first framing | `/betting/futures`, `src/pitcher_card_engine/web/templates/betting_futures.html` |
| Verification + ship summary | Scheduler coverage now includes the derived futures-gap build, and the full M3 M2 stack is documented here | `scripts/ops/schedule.py`, `scripts/ops/health_check_thresholds.yml`, this document |

## Commits Shipped

| Commit | Description |
|---|---|
| `9b0dac5` | Fix team-talent league-consistency scaling |
| `8b43e9b` | Add free futures and team-totals market ingestion |
| `fa55d90` | Add devig + fair-price gap engine |
| `69241c6` | Add `/betting/futures` web surface |
| `current ship commit` | Verification pass, scheduler hook for futures gaps, and ship summary |

## Team-Talent Scaling Fix: Before / After

The Step 1 fix replaced one pooled league-wide scaling factor with league-aware scaling anchored to per-league historical run environments. Replaying the old pooled method against the current 2026 feature frame gives this before/after comparison:

| League | Before RS mean | After RS mean | Before RA mean | After RA mean |
|---|---:|---:|---:|---:|
| American League | 721.4 | 731.6 | 720.9 | 731.6 |
| National League | 724.8 | 713.8 | 725.4 | 713.8 |

Why that matters:

- Before the fix, AL and NL team means were being pulled toward one pooled center even when their projected environments differed.
- After the fix, runs scored and runs allowed stay internally balanced within each league, which keeps playoff odds and futures probabilities from inheriting a league-baseline distortion.

Downstream sanity after the fix:

- Division winner odds sum to `1.0` inside all six divisions.
- AL and NL pennant odds each sum to `1.0`.
- World Series odds sum to `1.0`.

## Market Coverage

Current free-source coverage for the daily futures snapshot:

| Source | division_winner | pennant | world_series | win_total | mvp | cy_young | rookie_of_the_year |
|---|---:|---:|---:|---:|---:|---:|---:|
| FanDuel | 30 | 30 | 30 | 30 | 91 | 80 | 59 |
| SportsGameOdds free | 0 | 0 | 0 | 0 | 0 | 0 | 0 |

Operational read:

- FanDuel is currently the only free source contributing usable futures rows in this milestone.
- SportsGameOdds free remains a tracked gap rather than a hidden failure.

## Futures Gap Artifact Snapshot

Current output:

- Market snapshot: `data/derived/betting/futures_markets_20260422.parquet`
- Gap snapshot: `data/derived/betting/futures_edges_20260422.parquet`
- Rows in `futures_edges_20260422.parquet`: `350`
- Market families covered: `7`

Sample rows from the current gap parquet:

| Market | League | Entity | Market fair % | Projection fair % | Gap | Context |
|---|---|---|---:|---:|---:|---|
| Division winner | AL | NYY | 56.2% | 68.7% | +12.6 pts | Stronger projection than market |
| World Series | MLB | NYY | 9.8% | 19.3% | +9.5 pts | Meaningful outright gap |
| Win total over 79.5 | AL | BAL | 68.8% | 77.7% | +8.9 pts | Team win band clears the book number |
| Cy Young | AL | Garrett Crochet | 0.9% | 15.3% | +14.5 pts | Large projection-vs-market separation |

Null coverage notes in the current gap artifact:

- `41` rows still have null projection-implied values.
- Concentration:
  - `rookie_of_the_year`: `36`
  - `mvp`: `4`
  - `cy_young`: `1`
- These are mostly player-name / projection-universe gaps, especially speculative ROY names, and are intentionally surfaced as `No Signal` instead of being fabricated away.

## Scheduler / Verification Read

Forced verification cycle:

- Command: `python scripts/ops/schedule.py --force --once`
- Cycle artifact: `outputs/ops/scheduler_cycle_20260422T183532.json`

Relevant futures tasks from the forced cycle:

| Task | Result | Metric |
|---|---|---:|
| `fetch_futures_markets` | Success | `350` rows |
| `build_futures_fair_price_gaps` | Success | `350` rows |

Why the scheduler changed in Step 5:

- Before this verification pass, the scheduler refreshed `futures_markets` but did not rebuild `futures_edges`.
- Milestone 2 now adds `build_futures_fair_price_gaps` as a daily scheduled task at `05:05`, immediately after the season projection refresh and before later product-facing jobs.

Health-check status after the forced cycle:

- New futures tasks passed the health check.
- The only remaining health failure in the cycle was carried state from `run_daily_mithrandir` with `metric_below_min<1>`, which is unrelated to the futures lane and predates this milestone.

## Spot Checks

Three spot checks from the current devigged output looked sensible:

1. AL East division market:
   - `NYY` devigged market fair = `56.2%`
   - This reads like a real heavyweight division favorite, not a broken 70%+ devig artifact.
2. World Series outright:
   - `NYY` devigged market fair = `9.8%`
   - This sits in a believable title-favorite range for a top-tier contender.
3. Season win total:
   - `BAL over 79.5` market fair = `68.8%`, projection fair = `77.7%`
   - The relationship between the market and the sim-derived projection is directionally coherent and not numerically absurd.

One nuance worth being candid about:

- `LAD` division-winner pricing devigged to `88.5%`, which is extremely aggressive but comes directly from a highly concentrated one-book market rather than from a normalization bug.

## UI / Product Surface

New public route:

- `/betting/futures`

What the page looks like:

- Same dark-shell, panel, and odds-bar language as the projections and betting hubs
- Four sections:
  - Division Winners
  - Pennants and World Series
  - Win Totals
  - Awards
- Each row shows:
  - projection fair %
  - market fair %
  - gap
  - uncertainty context
  - read label: `Wider Gap`, `Watch`, or `No Signal`

Why the framing matters:

- The hero copy explicitly says a gap is not a bet signal.
- If the uncertainty band still crosses the market on win totals, the row is labeled `No Signal`.
- There are no “bet this,” “+EV,” or “edge found” calls to action on the new futures page.

Verification on copy:

- A targeted grep across the new futures template found no `+EV`, `bet this`, or `bet now` language.

## Known Debts

- SportsGameOdds free is still not contributing usable futures rows in this milestone. We track that gap rather than pretending it exists.
- Win totals currently use FanDuel’s surfaced over ladder rather than a true over/under pair, so the fair-price treatment is intentionally conservative and one-sided.
- Awards uncertainty is still contextual rather than a fully conformal odds interval. The current page shows band context, not a full probabilistic awards interval.
- `41` futures rows still have null projection probabilities because the projection universe and free futures board do not line up perfectly yet, especially in ROY markets.
- The overall site health check still carries the pre-existing `run_daily_mithrandir` threshold miss.

## What Milestone 2 Explicitly Does Not Do

- No new betting model training
- No direct “betting advice” surface
- No paid consensus futures feed
- No cross-book futures consensus yet beyond what free data supports
- No true futures CLV tracking yet
- No fresh season-level re-derivation of player projections

## Honest Read On Ship State

M3 Milestone 2 is the first revenue-adjacent betting surface that actually feels connected to the projection system instead of parallel to it. The biggest win is not one specific gap row; it is that the full path now exists end to end: season projections feed team talent, team talent feeds playoff and awards probabilities, free markets feed a normalized futures board, and the site presents the result in a restrained, uncertainty-first way. The main remaining weakness is data coverage depth, not system shape.
