# PGA DraftKings — Sharp GPP lineup theory (lab implementation)

This document explains how **CourtEdge PGA DK Lineup Lab** (`pga-dk-lineup-lab.html`) encodes widely discussed GPP construction ideas. It is **not** proprietary DFS advice and does not guarantee results.

## What winning large-field GPP lineups tend to share

Public post-mortems on PGA Tour DFS (RotoGrinders, Awesemo, industry podcasts) repeatedly describe winning 150-max portfolios as a blend of:

1. **Chalk anchor** — At least one highly owned golfer (often 12%+ projected ownership) who can “carry” a lineup if they contend. You need someone the field is on so you are not purely contrarian on every name.
2. **Low-owned leverage** — At least one golfer at modest ownership (often ≤8–10%) with real top-20 / win equity. This is the “low-owned star” or “wrong-wave pivot” lever that separates you when chalk busts.
3. **Ceiling-weighted ranking** — Sort and build toward **upside** (ceiling, course fit, recent form), not median projection alone. Cash cares about median; GPP cares about tail outcomes.
4. **Wave / tee-time correlation** — On split-tee events, AM vs PM waves can diverge sharply with weather. Building **minimum AM counts** (when your slate includes `tee_time` or `wave`) is a portfolio rule, not a single-lineup trick.
5. **Wind edge** — When it’s windy, you want golfers with **less wind exposure** (calmer wave, sheltered holes, or your forecast). The lab boosts `wind_exposure: low` and penalizes `high` when `wind_mph ≥ 12` or `conditions: windy`, scaled by each course’s `wind_sensitivity` in `pga-course-profiles.json` (links venues higher).
6. **Course fit** — Long courses reward distance; firm greens reward approach + putting; penal rough rewards accuracy. The lab applies static multipliers from `data/pga-course-profiles.json` when `course_id` or `tournament` matches.

## Contest field vs your entry limit (lab UI)

These are **different** controls in the lineup lab:

| Control | What it means | Lab values |
|---------|----------------|------------|
| **Contest field** | How many total DK entries are in the GPP — drives sim aggressiveness, chalk vs leverage scoring, exposure pressure | **Small** ≤1K · **Medium** 1K–10K · **Large** 10K–100K (flagship MME) |
| **Your entry limit** | How many lineups **you** build and upload for that contest | **1** (SE) · **3** (3-max) · **20** (20-max) · **150** (150-max) |

**Build GPP set** always generates **N = your entry limit** unique 6-golfer lineups. Field tier does **not** change N; it changes how those lineups are scored and diversified.

### Field-tier tuning (`js/pga-dk-engine.js` → `fieldTierGppTuning`)

| Tier | Ownership fade (λ scale) | Chalk rank bonus | Low-own rank bonus | Exposure pressure (`expoWeight`) | MC sims (portfolio rank) |
|------|--------------------------|------------------|--------------------|----------------------------------|--------------------------|
| **Small** | Milder (0.82×) | Higher (1.35×) | Lower (0.72×) | Lighter (2.35) | 400 |
| **Medium** | Baseline (1×) | Baseline | Baseline | 2.8 | 500 |
| **Large** | Stronger (1.18×) | Lower (0.72×) | Higher (1.45×) | Heavier (3.45) | 600 |

Large fields: more leverage on low-owned names in the **rank** function and stronger portfolio diversity so you are not cloning chalk across 20–150 lineups. Small fields: more chalk-friendly scoring — you can anchor on popular plays without over-penalizing ownership.

## How the lab scores lineups

| Mode | Score idea |
|------|------------|
| **Cash (SE)** | `projection − 0.14·log(1+own%)` — stable projection with mild ownership fade |
| **GPP (Sharp)** | `√proj × ceiling blend − λ·log(1+own%)` (scaled by field tier) plus bonuses for chalk (≥12% own) and low-owned (≤10% own) in the **rank** function; each lineup still must pass rule checks when Sharp rules are on |
| **Wind (windy days)** | Projection/ceiling/floor × wind fit; rank bonus for low exposure, penalty for high (`WIND_MPH_THRESHOLD` = 12 in engine) |
| **Course sim** | Monte Carlo draw per golfer between **floor** and **ceiling** (after course-fit + wind adjustment). **Full-field classic**: Thu start (ET), ~**75 make cut** after R2; each draw keeps R1–R2 points and only adds R3–R4 (weekend half of the draw) when the golfer “makes cut” per `make_cut_pct` (Bernoulli). Report mean / p90 / p99 for the 6-man set |

Ownership fields: `ownership_pct` or legacy `own` on slate JSON.

## Rules you can set in the UI

- **Locks** — Names that must appear in every generated lineup (one per line).
- **Min AM wave** — 5 or 6 golfers with AM tee (`wave: "AM"` or `tee_time` before noon ET).
- **Min salary used** — Optional floor on total salary (DK sometimes publishes minimum spend; verify your contest sheet).

## Data you supply nightly

Replace `data/pga-dk-slate.json` (or upload JSON in the lab):

| Field | Required | Notes |
|-------|----------|--------|
| `name`, `salary` | Yes | From DK export |
| `projection` | Yes | Your model |
| `ceiling`, `floor` | Recommended | GPP + Monte Carlo |
| `ownership_pct` | Recommended | Sharp chalk / leverage rules |
| `tee_time` or `wave` | For AM rule | e.g. `"07:45"` or `"AM"` |
| `wind_mph`, `conditions`, `wind_notes` | Slate-level wind | e.g. `18`, `"windy"`, forecast one-liner for status bar |
| `wind_exposure` or `wind_bucket` | Per-golfer wind | `"low"` / `"medium"` / `"high"` — who gets the bad conditions |
| `make_cut_pct` or `cut_make_prob` | Classic GPP sim | 0–1 probability golfer plays R3–R4 (weekend DK scoring). If omitted, lab estimates from projection rank and OWGR when field API matches |
| `made_cut` | Showdown weekend | `true` if golfer made the cut; use with post-cut filter |
| `tournament`, `event` | Recommended | Shown in hero / status — set from **your** DK export (bundled JSON is an empty placeholder) |
| `cut_line`, `cut_score` | Classic / showdown context | Default cut **75**; optional posted cut score (e.g. `"E"`) for UI |
| `course_id` | For course fit | Optional — keys in `pga-course-profiles.json` (only when you set it on your upload). **CJ Cup Byron Nelson / TPC Craig Ranch:** `tpc_craig_ranch` (aliases: `byron nelson`, `cj cup`, …) |
| `sg_ott`, `sg_app`, `sg_putt` | Optional | Improves course-fit multipliers |

**Full-field tournament defaults** (classic slates): 4 rounds, **Thursday** start (`timezone`: ET), **cut_line** 75 for simulation. Override on slate JSON with `rounds`, `start_day`, `cut_line` if needed.

The lab still merges **`/api/pga/field`** for OWGR / SG when names match; showdown slates use JSON-only pools.

### Saturday / Sunday showdown (post-cut)

For **round 4** (or Sat/Sun DK showdown) after the cut:

1. Preset: **Showdown — Round 4 (Captain + 5 FLEX, made cut)** → `data/pga-dk-slate-showdown-r4-cpt.json` (made-cut only; rebuild via `node scripts/build-pga-dk-showdown-r4-slate.mjs`). Round 3: `data/pga-dk-slate-showdown-r3-cpt.json` + `node scripts/build-pga-dk-showdown-r3-slate.mjs`. **Saturday / Sunday** source file → `data/pga-dk-slate-showdown-weekend.json` (upload path — not Bay Hill demo).
2. Enable **Post-cut weekend (≤75)** in Rules — keeps `made_cut: true` or `make_cut_pct ≥ 0.5` (threshold in `js/pga-dk-engine.js`, default 0.5).
3. Set `captain_slot: true` on JSON when the contest is **CPT + 5 FLEX**; **Build GPP set** + **Sim all & rank** use ceiling-heavy CPT draws and **p99-weighted** ranking (top-1% proxy).
4. Pool should list only golfers still playing the weekend (~75); tag missed-cut names with `made_cut: false` or low `make_cut_pct` so the filter drops them. Upload a DK round-slate JSON in **Slate upload** or refresh the weekend file nightly.

## Showdown vs Classic

| | **Classic** | **Showdown (post-cut)** |
|---|-------------|-------------------------|
| **Pool** | Full field from `/api/pga/field` + slate salaries | Only `players[]` in slate JSON (weekend / round sheet) |
| **Roster** | 6 flex under $50k | **1 CPT (1.5× points)** + **5 FLEX** when `captain_slot: true`; else 6 flex |
| **Post-cut** | `make_cut_pct` for Thu–Sun sim (Bernoulli weekend half) | Toggle **Weekend / Showdown (post-cut)**: keeps `made_cut: true` or `make_cut_pct` ≥ 0.5 |
| **Sharp GPP** | Chalk + low-owned lever, field-tier own fade | **CPT leverage** (low-owned captain + ceiling), **flex** studs + bombs, **cumulative total own%** on lineup |
| **Sim rank** | Sort by **p90** (then mean) | Spike-biased draws; sort by **0.62·p99 + 0.38·p90** (top-1% proxy), CPT ceiling-heavy |
| **Entry limit** | 1 / 3 / 20 / 150 lineups | Same — **Build GPP set** works in showdown CPT mode |

**Presets:** **Showdown — Round 3 (CPT + FLEX, post-cut)** → `data/pga-dk-slate-showdown-r3-cpt.json`. Six-flex R3: `data/pga-dk-slate-showdown-r3.json`. Rebuild both from the weekend slate after each cut with `node scripts/build-pga-dk-showdown-r3-slate.mjs`.

### Tagging golfers for wind edge (nightly)

1. **Slate header** — Set `wind_mph` from your forecast (or use the lab **Conditions** dropdown: Calm / Moderate / Windy, which sets mph for scoring).
2. **Per player** — After you know which wave eats wind, set `wind_exposure`:
   - **`low`** — PM when wind builds through the day, or AM when wind is expected to fade; use whenever that golfer’s wave is clearly lighter.
   - **`high`** — Opposite wave / worst conditions.
   - **`medium`** — Split or uncertain; omit only if you accept AM=high / PM=low proxy from `tee_time` / `wave`.
3. **Aliases** — `wind_bucket` accepts the same values; `am` / `pm` map to high / low exposure.
4. **No tags?** — On windy slates the engine still applies AM=high, PM=low when `wind_exposure` is missing but `wave` or `tee_time` exists.

Example (windy R1):

```json
"wind_mph": 18,
"conditions": "windy",
"wind_notes": "NW gusts; AM into building breeze",
"players": [
  { "name": "Rory McIlroy", "tee_time": "12:30", "wave": "PM", "wind_exposure": "low" },
  { "name": "Scottie Scheffler", "tee_time": "07:10", "wave": "AM", "wind_exposure": "high" }
]
```

## Quick workflow (no hand-building)

Open **`/pga-dk-lineup-lab.html`**, load your slate (preset JSON or upload), optionally set locks / AM wave in **Rules engine**, set **Contest field** (Small / Medium / Large) and **Your entry limit** (1 / 3 / 20 / 150), click **Build GPP set**, then **Sim all & rank** to Monte Carlo every lineup and sort by simulated **p90** (with p50, mean, salary, avg own%, and projection sum in the table). **Export best CSV** downloads the top-ranked 6 for DK upload. This ranks *your* portfolio against itself using floor–ceiling draws — not a full-field duplicate simulation against real DK ownership.

## Related files

- `docs/PGA-CONTEST-OWNERSHIP.md` — field size vs cumulative ownership, CPT leverage, 100-man chalk FAQ
- `js/pga-dk-engine.js` — rules, course fit, field-tier GPP tuning, sharp scores, Monte Carlo, post-cut filter, `top10ForCourse`
- `data/pga-course-profiles.json` — venue traits
- `data/pga-dk-slate-showdown-weekend.json` — **empty placeholder** until you upload DK export; rebuild R3/R4 with build scripts
- `pga-simulator.html` — round / field toy sim (separate from DK optimizer)
- `scripts/test-pga-dk-optimizer.mjs` — smoke tests
