# MLB game-by-game simulation — simulate & compare to actual

**CourtEdge MLB DK Lineup Lab** (`/mlb-dk-lineup-lab.html`) has a green **Game-by-game simulation** card directly under **Team stack ranks** (sticky on scroll). Use it to sanity-check Vegas totals and away-win rates before building stacks; after games finish, compare sim μ to real total runs.

## 5-step flow (slate day → compare tonight)

1. **Open the lab** — `/mlb-dk-lineup-lab.html`. Scroll to **Game-by-game simulation** (or jump to `#game-by-game-sim`).
2. **Load slate** — default `data/mlb-dk-slate.json`. Confirm the **Slate:** banner shows today’s date (ET). If **Stale slate** appears, run `node scripts/build-mlb-dk-slate.mjs`, deploy, **Reload slate**.
3. **Run sims** — set **Sims per game** (500–2000, default 1000). Click **Sim all games** or **Sim** on one row. Table columns: **Matchup · O/U · Spread · Sim μ · Away win% · Lean · Actual · Error**.
4. **After games finish** — paste or upload final total runs:
   - Per game on slate: `"actual_runs": 11` on the `games[]` object
   - Slate map: `"game_results": { "NYY@BOS": { "actual_runs": 11 } }` or `"game_results": { "g1": 9 }`
   - Merge array: `"games": [{ "game_id": "g1", "actual_runs": 9 }]`
   Click **Upload JSON** or paste into the box, then **Apply game results**. Re-run sim if you want fresh μ; **Error** and MAE/bias update from applied actuals either way.
5. **Rank edges** — sort **By O/U edge** (largest |μ − line|), **By upset edge** (sim away win% vs Vegas implied), or **By matchup**. Green row = closest to sim μ when actuals exist; top edge row highlighted when sorted by O/U.

## JSON example (minimal)

```json
{
  "game_results": {
    "NYY@BOS": { "actual_runs": 11 },
    "BOS@NYY": { "actual_runs": 7 }
  },
  "games": [
    { "game_id": "mlb_20250522_nyy_bos", "away": "NYY", "home": "BOS", "actual_runs": 11 }
  ]
}
```

Alternate keys the engine accepts on a game or `game_results` node: `actual_total`, `actual_total_runs`, `total_actual`, or a bare number for the game key.

## Slate fields (Vegas)

Each game in `games[]` should include implied runs (or they are inferred from hitter `team_total`):

| Field | Meaning |
|-------|---------|
| `away_total` | Away team implied runs |
| `home_total` | Home team implied runs |
| `total_runs` | Game O/U (optional; default away+home) |
| `spread` | Away spread (e.g. −1.5) — adjusts sim means |

Nightly rebuild:

```bash
node scripts/build-mlb-dk-slate.mjs   # stamps slate_date + game start_time to today (US Eastern)
```

## How sim μ is built

- Vegas-adjusted Poisson team runs with mild game-script correlation (`ρ ≈ 0.14`)
- Spread shifts away/home means; O/U scales to `total_runs` when set
- Output: total-runs histogram (O/U line marked), away win %, Over/Under lean vs line

Engine: `js/mlb-game-sim.js` → `MlbGameSim.simulateGame`, `readGameActual`.

## Testing locally

```bash
node scripts/test-mlb-dk-optimizer.mjs
```

Browser: **Reload slate** → **Sim all games** → paste actuals → **Apply game results** → sort **By error** (via Actual/Error columns).

## vs SaberSim / full-field sim

| This lab | SaberSim-style |
|----------|----------------|
| Per-game team runs / total | Full slate, player FP correlation |
| Vegas TT in → run distribution | Play-by-play / stack correlation |
| ~1k sims per game in ms | Large MME field sims (server) |
| No stack or ownership model | Portfolio dup + ROI curves |

Use game-by-game sim for totals and game environment; use **Sim top 50 for ROI** on built lineups for 10-man DK FP ranges, not correlated game scripts.
