# PIFF 3.0 All-Sports Expansion Plan

## Current State
- **PIFF 3.0**: NBA only (points, rebounds)
- **PIFF 2.7**: NHL (shots, assists), Soccer (shots, tackles, clearances, etc.)
- **No PIFF**: NCAAB, MLB, NFL, MLS

## Data Readiness by League

### Tier 1 — Ready Now (all 3 data pillars: PPL + PGM + DVP)

| League | PlayerPropLine | PlayerGameMetric | DVP | Status |
|--------|---------------|-----------------|-----|--------|
| **NBA** | 1.05M rows, live | 393K rows, 53 stats | 30 teams, 6 stats | ALREADY RUNNING |
| **NCAAB** | 3.09M rows, live | 813K rows, 15 stats | 424 teams, 5 stats | READY — highest priority |
| **NHL** | 715K rows, live | 145K rows, 21 stats | 32 teams, 4 stats | READY — upgrade 2.7→3.0 |

### Tier 2 — Ready When Season Starts

| League | PlayerPropLine | PlayerGameMetric | DVP | Status |
|--------|---------------|-----------------|-----|--------|
| **MLB** | 375K rows (offseason) | 1.73M rows, 51 stats + Statcast | 39 teams, 10 stats | READY in Apr 2026 |
| **NFL** | 214K rows (offseason) | 129K rows, 52 stats + NextGen | 12 teams (sparse) | READY in Sep 2026, needs DVP build |

### Tier 3 — No DVP, use hit-rate-only model

| League | PlayerPropLine | PlayerGameMetric | DVP | Status |
|--------|---------------|-----------------|-----|--------|
| **EPL** | 149K rows, live | 422K rows, 29 stats | NONE | Can run without DVP |
| **La Liga** | 106K rows, live | 404K rows, 29 stats | NONE | Can run without DVP |
| **Bundesliga** | 86K rows, live | 306K rows, 29 stats | NONE | Can run without DVP |
| **Serie A** | 70K rows, live | 347K rows, 29 stats | NONE | Can run without DVP |
| **Ligue 1** | 75K rows, live | 304K rows, 29 stats | NONE | Can run without DVP |
| **Champions League** | 33K rows, live | 45K rows, 29 stats | NONE | Can run without DVP |

---

## Architecture: Unified PIFF 3.0 Engine

Instead of separate scripts per sport, build one parameterized engine.

### File: `scripts/piff_3_0_engine.py`

```
piff_3_0_engine.py --league nba --date 2026-02-27
piff_3_0_engine.py --league ncaab --date 2026-02-27
piff_3_0_engine.py --league nhl --date 2026-02-27
piff_3_0_engine.py --league mlb --date 2026-02-27
piff_3_0_engine.py --league epl --date 2026-02-27
```

### League Config (per-sport parameters)

```python
LEAGUE_CONFIG = {
    "nba": {
        "stats": ["points", "rebounds"],
        "min_games": 15,
        "season_window_days": 60,
        "positions": ["G", "F", "C"],
        "dvp_enabled": True,
        "dvp_main_threshold": 0.70,
        "dvp_border_threshold": 0.60,
        "line_floors": {"points": 8, "rebounds": 4},
        "h2h_min_games": 3,
        "line_movement_enabled": True,
        "b2b_detection": True,
        "max_legs_main": 8,
        "max_legs_border": 6,
        "max_per_team": 2,
    },
    "ncaab": {
        "stats": ["points", "rebounds"],
        "min_games": 10,           # smaller sample sizes in college
        "season_window_days": 90,  # longer window, fewer games
        "positions": ["G", "F", "C"],
        "dvp_enabled": True,
        "dvp_main_threshold": 0.65,  # slightly lower — college variance higher
        "dvp_border_threshold": 0.55,
        "line_floors": {"points": 6, "rebounds": 3},
        "h2h_min_games": 2,          # conference opponents may only meet 2x
        "line_movement_enabled": True,
        "b2b_detection": False,       # college doesn't play B2B
        "max_legs_main": 8,
        "max_legs_border": 6,
        "max_per_team": 2,
        "ppl_integrity_filter": True, # NCAAB PPL needs integrityStatus='active'
    },
    "nhl": {
        "stats": ["shots", "assists", "goals"],
        "min_games": 12,
        "season_window_days": 60,
        "positions": ["F", "D"],       # no goalie props in PIFF
        "dvp_enabled": True,
        "dvp_main_threshold": 0.65,
        "dvp_border_threshold": 0.55,
        "line_floors": {"shots": 1.5, "assists": 0.5, "goals": 0.5},
        "h2h_min_games": 2,
        "line_movement_enabled": True,
        "b2b_detection": True,
        "schedule_multipliers": {      # NHL fatigue is more pronounced
            "home": 1.05, "away": 0.97,
            "road_b2b": 0.90, "home_b2b": 0.96,
            "3_in_4": 0.88,
        },
        "max_legs_main": 8,
        "max_legs_border": 6,
        "max_per_team": 2,
    },
    "mlb": {
        "stats": ["hits", "total_bases", "strikeouts", "home_runs",
                   "pitcher_strikeouts", "earned_runs"],
        "min_games": 15,
        "season_window_days": 45,      # MLB season is long, recent form matters more
        "positions": ["OF", "IF", "DH", "C", "SP", "RP"],
        "dvp_enabled": True,
        "dvp_main_threshold": 0.65,
        "dvp_border_threshold": 0.55,
        "line_floors": {"hits": 0.5, "total_bases": 0.5, "strikeouts": 3,
                        "home_runs": 0.5, "pitcher_strikeouts": 3, "earned_runs": 2},
        "h2h_min_games": 3,            # pitcher vs batter matchups matter
        "line_movement_enabled": True,
        "b2b_detection": False,         # MLB plays daily, no B2B concept
        "platoon_split": True,          # L/R pitcher splits are critical in MLB
        "max_legs_main": 8,
        "max_legs_border": 6,
        "max_per_team": 2,
    },
    "epl": {
        "stats": ["shots", "shots_onTarget", "tackles", "foulsDrawn"],
        "min_games": 8,
        "season_window_days": 90,       # soccer has fewer games
        "positions": ["FW", "MF", "DF"],
        "dvp_enabled": False,           # no DVP data for soccer
        "line_floors": {"shots": 0.5, "shots_onTarget": 0.5, "tackles": 0.5, "foulsDrawn": 0.5},
        "h2h_min_games": 2,
        "line_movement_enabled": False, # no LineMovement data for soccer
        "b2b_detection": False,
        "congestion_days": 4,           # fatigue if played within 4 days
        "congestion_mult": 0.94,
        "max_legs_main": 8,
        "max_legs_border": 6,
        "max_per_team": 2,
        "max_per_stat": 3,             # diversity cap per stat type
    },
    # la_liga, bundesliga, serie_a, ligue_1, champions_league: same as epl with league name swapped
}
```

### Core Engine Flow (same for all sports)

```
1. Load today's games from SportsGame WHERE league=X
2. Load DVP cache (if dvp_enabled)
3. Load PlayerPropLine for today's games → book lines
4. Load LineMovement (if line_movement_enabled)
5. Detect B2B / congestion from PlayerGameMetric
6. Bulk query PlayerGameMetric for candidates (min_games, season_window)
7. For each candidate:
   a. Get book line (from PPL, fallback to calculated line)
   b. Evaluate OVER direction (szn_hr, l5_hr, gap)
   c. Evaluate UNDER direction (szn_hr, l5_hr, gap)
   d. Apply DVP boost/penalty (if enabled)
   e. Apply H2H boost/penalty
   f. Apply line movement boost/kill
   g. Apply schedule adjustment (B2B/congestion)
   h. Assign tier (T1/T2/T3)
   i. Compute composite edge score
8. Deduplicate: best direction per (player, stat)
9. Select main card (DVP qualified) + borderline card
10. Compute staking (RR combos)
11. Output JSON to picks/piff30_{league}_{date}.json
```

### PPL Join for Player Names

PIFF 3.0 NBA currently uses `PlayerPropLine.marketName` to extract player names via regex. For the unified engine, use the canonical join:

```sql
SELECT DISTINCT pp."canonicalPlayerId", cp."displayName", pp."propType", pp."lineValue"
FROM "PlayerPropLine" pp
JOIN "CanonicalPlayer" cp ON pp."canonicalPlayerId" = cp.id
WHERE pp.league = $1 AND pp."gameStart"::date = $2
  AND pp."integrityStatus" = 'active'
  AND pp."periodType" = 'fullgame'
ORDER BY pp."snapshotAt" DESC
```

### DVP-Less Mode (Soccer)

When `dvp_enabled: False`, skip DVP gating entirely. Instead:
- All candidates go to "main card" (no borderline split)
- Raise HR thresholds slightly to compensate: szn_hr >= 0.80 (vs 0.75 with DVP)
- Add stat diversity cap (max_per_stat) to prevent one prop type dominating

---

## Implementation Phases

### Phase 1: NCAAB (1-2 days)
- Highest impact — NCAAB has the most PPL data and DVP coverage
- Copy NBA PIFF 3.0 logic, swap config params
- Key differences: no B2B, longer season window, lower min_games
- PPL needs `integrityStatus = 'active'` filter (NCAAB has 96% quarantined dupes)
- Output: `picks/piff30_ncaab_{date}.json`

### Phase 2: NHL upgrade 2.7→3.0 (1 day)
- Replace fixed lines with real PPL book lines
- Add direction-agnostic evaluation (currently overs only)
- Add goals as a stat (currently only shots + assists)
- Keep NHL-specific schedule multipliers
- Output: `picks/piff30_nhl_{date}.json`

### Phase 3: Soccer DVP-less 3.0 (1-2 days)
- Replace 2.7 soccer runner with unified engine
- DVP disabled, higher HR thresholds
- Multi-league: EPL, La Liga, Bundesliga, Serie A, Ligue 1, CL
- Congestion detection instead of B2B
- Stat diversity cap
- Output: `picks/piff30_{soccer_league}_{date}.json`

### Phase 4: MLB (when season starts ~Apr 2026)
- Full PIFF 3.0 with DVP, strong Statcast data
- Platoon splits (L/R pitcher) as unique feature
- Pitcher-vs-batter H2H lookup (beyond standard H2H)
- Output: `picks/piff30_mlb_{date}.json`

### Phase 5: NFL (Sep 2026)
- Build DVP data pipeline first (currently only 12 teams)
- Full PIFF 3.0 once season starts
- Position-specific adjustments (QB, RB, WR, TE different profiles)

---

## Orchestrator Update

Update `piff_3_0_all.py` to run all active leagues:

```python
ACTIVE_LEAGUES = ["nba", "ncaab", "nhl", "epl", "la_liga", "bundesliga",
                  "serie_a", "ligue_1", "champions_league"]
# Add "mlb" in April, "nfl" in September

for league in ACTIVE_LEAGUES:
    run_piff_engine(league, today)
```

## PIFF Loader Update (Rainmaker Backend)

Update `piff.ts` to load all league files:

```typescript
const LEAGUES = ['nba', 'ncaab', 'nhl', 'epl', 'la_liga', 'bundesliga',
                 'serie_a', 'ligue_1', 'champions_league'];

for (const league of LEAGUES) {
  const legs = loadPiffFile(`piff30_${league}_${today}.json`);
  // merge into map by team abbreviation
}
```

---

## Expected Impact
- **NCAAB**: 424 teams with DVP data → richest prop analysis in college basketball
- **NHL**: Direction-agnostic unders will be a new edge (currently overs only)
- **Soccer**: 7 leagues getting data-backed props instead of pure Grok
- **All sports on Rainmaker**: Every prop unlock will have PIFF data injected into Grok prompt
