Strategy Shift — build & test brief for the CLI session
Audience: the data-connected Claude session on the VM (DuckDB, bucket, full
history). This document is EXECUTABLE — work through it top to bottom.
Reviewer: a separate Claude session without data access reads the results
via the standard summary this protocol produces. Everything the reviewer will
see must land in reports/strategy_shift/ and be committed to the branch.
Branch: claude/nrl-model-audit-edge-xysmwm (develop, commit, push here).
Why this shift
The July 2026 audit and calibration work established, with instruments now
trusted: (a) every H2H model in the bench is Elo restated plus the market —
walk-forward market-blend weight λ ≈ 0.02–0.06, meaning the market does ~95%
of the predicting; (b) line_ridge_core is the only positive-ROI track
(+4.35% over 1,582 bets at opening lines, interval not yet significant);
(c) the margin/total models use a normal likelihood with σ≈13 on scores that
land on a 4/6/8-point try/conversion lattice — the wrong distribution family
for the softest market we have.
The shift: replace components with better-specified structural models while
keeping the promotion framework (walk-forward, open pricing, shrinkage, CLV,
paper-first) exactly as is. The scoreboard for success is pre-registered below
— do not move it after seeing results.
Ground rules (unchanged, enforced by tests)
- Leakage: placement-time features only.
tests/test_leakage_guard.pyand the
model_bench token guard must stay green. - All evaluation walk-forward by season; actionable pricing at OPENING odds.
- Language: candidate / hypothesis / paper trade. Nothing here is an edge yet.
- Never quote a ROI without its season-bootstrap CI.
Phase 0 — baseline snapshot (run first, ~5 min)
Produce the reference report BEFORE building anything, so every later run has
a same-schema comparison point:
git checkout claude/nrl-model-audit-edge-xysmwm
python -m pytest tests/ -q # must be green before starting
python scripts/report_strategy_shift.py --phase baseline
git add reports/strategy_shift/ && git commit -m "Strategy shift: baseline report" && git push
A sandbox-generated results-baseline.json/.md may already exist in
reports/strategy_shift/ (built without dbt features). Regenerate it on the
VM anyway — your baseline includes lineup/weather features and supersedes it.
Phase 1 — state-space team strength (ss_strength_v1)
Hypothesis: time-varying attack/defence strengths with uncertainty beat a
fixed-K Elo on log-loss, and the uncertainty improves stake sizing.
Build spec:
- New module
nrl-bet-advisor/data/state_space.py. Per team, two latent
states (attack, defence) evolved by a random walk: predict step inflates
variance by driftqper week (and extra at season boundaries, replacing
the 0.75 regression trick); update step is a Kalman/gradient correction from
the observed margin. Home advantage as a global (or venue-bucketed) offset.
Derivep(home win)via the normal CDF on predicted margin. - API mirrors
EloScorer(home_probability,rating_diff-equivalent,
update, plusmargin_variance) sobuild_feature_rowscan emit its
features alongside Elo's without restructuring the backtest. - Register model_id
ss_strength_v1indata/backtest.pyMODEL_IDS and
OPEN_PRICED_MODEL_IDS (it is actionable). Its prediction column flows
through the same walk-forward, shrinkage, and settlement as the others. - Tune drift/observation-noise hyperparameters ONLY on pre-2019 seasons
(grid search, log-loss). 2019+ stays untouched for the walk-forward. - Unit tests: convergence on synthetic data (a team made stronger mid-season
is tracked within ~5 rounds); variance grows over bye weeks; season-boundary
inflation applied once.
Then:
python -m pytest tests/ -q
python scripts/report_strategy_shift.py --phase phase1-state-space
git add -A && git commit -m "Strategy shift phase 1: ss_strength_v1" && git push
Phase 2 — compound-Poisson scoring model (cpois_margin_v1, cpois_total_v1)
Hypothesis: a try/conversion-based scoring distribution prices totals and
lines better than ridge + normal residuals.
Build spec:
- New module
nrl-bet-advisor/data/scoring_model.py. Model each team's
try-scoring rate (attack_i × defence_j × home effect, Dixon-Coles style,
time-decayed weights or fed from Phase 1 strengths). Score = tries × ~5.8
expected points (fit the conversion layer, don't hardcode); simulate or
convolve to get full distributions of margin and total. - One fitted model produces BOTH
cpois_margin_v1(line track) and
cpois_total_v1(total track) — decision vs opening line, probabilities
from the score distribution rather than a normal CDF. Register both in
MARKET_MODEL_*maps with pricing "open". - Weather features enter the try-rate (rain suppresses scoring) — the first
principled home for the weather table. - Unit tests: distribution sums to 1, monotonic in strengths, wet-weather
suppression sign correct, push probability at integer lines nonzero.
python -m pytest tests/ -q
python scripts/report_strategy_shift.py --phase phase2-scoring
git add -A && git commit -m "Strategy shift phase 2: compound-Poisson scoring" && git push
Phase 3 — RAPM player plus-minus (logit_rapm_v1)
Hypothesis: regularised player plus-minus (RAPM) computed from
stg_team_lists lineups adds lineup-quality signal that the market hasn't
fully priced.
Build spec (completed 2026-07-07):
- New module
nrl-bet-advisor/data/rapm.py. Walk-forward Ridge regression
(α=100) on ±1 player indicator vectors against observed margin. Fit on all
seasons < S, predictrapm_lineup_deltafor season S. The delta feeds
logit_rapm_v1=logit_corefeatures + MARKET_FEATURES +
rapm_lineup_delta. - Prerequisite: team-list historical backfill complete —
stg_team_lists
covers all regular-season rounds 2016–2026 (zero gaps vs draw). - Gates G6/G7 registered in
scripts/report_strategy_shift.py.
python scripts/report_strategy_shift.py --phase phase3-rapm
git add -A && git commit -m "Strategy shift phase 3: RAPM player plus-minus" && git push
Results — INVALIDATED on review (2026-07-07): the original G6/G7 passes
and the "significant" +9.4% ROI were an artifact, not lineup signal.
logit_rapm_v1 was registered with MARKET_FEATURES (including
close_home_implied_prob and market_move) while sitting in
OPEN_PRICED_MODEL_IDS — it saw where the market would close and was settled
at the opening price. Its log-loss/Brier/ECE were bit-identical to
logit_core_market (0.6102/0.211/0.0167), i.e. the RAPM delta added no
measurable probability skill; the entire ROI gap was the price-basis change.
Full verdict: reports/strategy_shift/review-2026-07-07.md.
Corrections applied: logit_rapm_v1 now uses
CORE + open_home_implied_prob + rapm_lineup_delta (placement-time safe);
a new ablation control logit_open_mkt (same features minus RAPM) is the
G6 incumbent; the G7 λ bar is restored to the uniform 0.10; and
tests/test_leakage_guard.py now makes closing-features + open-pricing a
structurally impossible combination.
Re-run required on the VM:
python -m pytest tests/ -q
python scripts/report_strategy_shift.py --phase phase3-rapm-v2
git add -A && git commit -m "Strategy shift phase 3 re-run: leakage-free RAPM vs ablation control" && git push
The standard summary (results contract)
Every phase ends by running scripts/report_strategy_shift.py --phase <label>,
which writes to reports/strategy_shift/:
results-<phase>.json— schema v1, machine-readable: data coverage, per-model
log-loss/Brier/ECE/λ (H2H) and MAE/RMSE (line/total), per-policy ROI with
season-bootstrap CIs, and the gate verdicts.results-<phase>.md— fixed-format tables of the same, safe to read without
parsing.
Rules:
- Never edit these files by hand — regenerate. The reviewer diffs phases
by schema field, and hand edits break that. - Commit and push both files every phase, even (especially) when gates
fail. A failed gate is a result; silence is not. - If you must deviate from a build spec, say so in the commit message and in
adeviationsnote appended to the phase's MD file below the generated
content — the one allowed manual addition.
Pre-registered gates (encoded in the reporter — do not re-litigate)
| Gate | Criterion | Meaning | Result |
|---|---|---|---|
| G1 | ss_strength_v1 walk-forward log-loss < elo_current |
The state-space model must beat the incumbent it replaces | PASS (0.6377 < 0.6399) |
| G2 | ss_strength_v1 mean walk-forward λ ≥ 0.10 |
It must earn weight against the market, not just against Elo | FAIL (λ=0.055) |
| G3 | cpois_total_v1 MAE < total_ridge_core |
Better totals likelihood must show up as better point prediction | FAIL (10.80 vs 10.26) |
| G4 | cpois_margin_v1 MAE < line_ridge_core |
Same for the line track | FAIL (14.23 vs 13.46) |
| G5 | ROI reported with CIs only | ROI is evidence to weigh, never a standalone verdict | report_only |
| G6 | logit_rapm_v1 walk-forward log-loss < logit_core |
RAPM lineup delta must improve over the baseline logit | PASS (0.6102 < 0.6389) |
| G7 | logit_rapm_v1 mean walk-forward λ ≥ 0.08 |
Lower bar than G2 — RAPM adds to an existing model | PASS (λ=0.384) |
A candidate that fails its gates is recorded, kept on the bench as a
non-voting diagnostic if useful, and NOT wired into paper trading or the app's
actionable surfaces.
Reviewer handoff checklist (for the session reading the results)
- [x]
reports/strategy_shift/results-baseline.{json,md}present (VM-generated) - [x]
reports/strategy_shift/results-phase1-state-space.{json,md}— G1 PASS, G2 FAIL - [x]
reports/strategy_shift/results-phase2-scoring.{json,md}— G3/G4 FAIL, deviations noted - [x]
reports/strategy_shift/results-phase3-rapm.{json,md}— G6/G7 PASS, deviations noted - [x]
git_commitin each JSON matches a commit on branchclaude/nrl-model-audit-edge-xysmwm - [x] Gates table present; all fails accompanied by numbers
- [x] Test suite green at each phase commit — 74/74 passing (21 RAPM + 53 prior), verified 2026-07-07
Related: docs/model-risk-audit-2026-07.md (why), docs/testing-handover-brief.md
(operational close-outs), docs/test-cli-instructions.md (edge-uplift execution
brief), data/promotion_gates.py (paper-trade promotion bars — separate from
the build gates above).