test-cli-instructions — edge-uplift round (data access required)
2026-07-25 — CURRENT ROUND: promote line_move_v1 to LIVE paper trading (see review-2026-07-25.md)
Verdict: line-move-v1 PASSED the decisive test — market_form_only 59.0%
(below the 63% floor), lineup channel adds 7.74pp with paired z=4.18,
mean-reversion-only rejected (open_line_only 53.5%). CLV leg positive:
+1.47 line points, 95% CI [1.08,1.87]. This is the program's strongest
result — Law 3 confirmed for the line market. It has earned LIVE evidence.
Backtested CLV is the entry ticket, NOT promotion — no real stakes.Step 1 — make line_move_v1 a weekly RECOMMENDATION (produce → surface → seed)
The model must become a visible, per-match recommendation each round, not
just a backtest. Three parts, all required:1a. Produce — a weekly predictor (mirror
ml/predict_weekly_margin.py):
load the current round's OPEN line + the Tuesday-clock features
(LINE_MOVE_FEATURES), run the walk-forward-trained line_move_v1, and for
each match emit: predicted move direction, model confidence (calibrated
prob), the recommended side + the open handicap to take. Write to
gold_nrl_predictionswithmodel_id='line_move_v1',market_type='line',
recommended_side,taken_line= the open handicap. Refresh AFTER the
Tuesday team lists land (that's the feature clock) and after weather regen.1b. Surface — the recommendation shows on the value board as a LINE
pick with the model chip (line_move_v1), the recommended side, the open
handicap, and the model's directional confidence. It must read as
"backline at the open" — the actionable instruction.
Keep the PAPER-trade framing (no real-stake language); guardrail tests
(canonical app + model attribution) stay green.1c. Seed — register alongside open_fav_v1 / rapm_timing_v1 in the
paper-trade setup. At the real opening line, place the recommended
direction; snapshot placement handicap + model prediction at seed time
(immutable, like paper_trades_v2). Settle line CLV at close ≤24h
post-round. Log to reports/live_evidence.md.Recommendation rule (pre-registered): recommend a line side only when
the model's directional confidence ≥ 0.60 (the backtest's operating
point). Below that, WATCH not GO — no seed. This keeps the live sample to
the picks the model is actually confident in, matching the 66.8% regime.Step 2 — weekly live CLV logging + the checkpoint rule
Per docs/edge-review-2026-07-20.md live checkpoints: 20 settled = CLV
direction read (info only); 40 settled = PARK if mean AND median CLV < 0;
100 settled = promotion review vs the canonical gates. Do NOT skip to real
money on the backtest — the two fake edges we caught were both backtests.Step 3 — keep unblocking the line-tick archive (execution timing)
The archive is still h2h-heavy; local forward staging recovered only 34
line events. Grow line + total tick capture (forward archive + R11+
re-fetch when API budget allows) so the repricing curve can tell us WHICH
point in the Tuesday window to place at. This decides execution, not
whether to bet — so it runs parallel to Steps 1–2, not before them.Report back
live_evidence.md rows per round (seeded, settled, running mean/median
CLV per strategy); no new model phases until line_move_v1 live CLV reads
at 20/40 settled. Deviations in## Deviations.2026-07-24 — superseded 2026-07-25 (decisive ablation executed; thesis survived; see review above)
Verdict on the overnight run: line-move-v1 passed every frozen
directional gate (66.8%, z=9) — but the ablations show the two lineup-news
features add only ~1pp.open_home_lineis in every variant, so the ~65%
floor is most likely opening-line MEAN-REVERSION + range-bounding, not
lineup news. The lineup thesis for the line market is UNCONFIRMED. Do not
seed or build a strategy off this yet.Step 1 — the decisive ablation (one cheap local run, no API/data)
Add two variants to
LINE_MOVE_VARIANTSindata/line_move.pyand re-run
report_strategy_shift.py --phase line-move-v1b:
1.market_form_only— drop ALL 12 lineup features, keep only
open_home_line,elo_rating_diff, rolling form, rest, playoff,
neutral. This is the baseline the current ablations never established.
2.open_line_only—open_home_linealone (the range-bounding /
mean-reversion control).
Same frozen LM1–LM4 gates. Report all five variants side by side.
Interpretation, pre-registered: ifmarket_form_only≥ ~63%, the
lineup channel adds nothing to line-move — the signal is mean-reversion,
and line-move-v1 is re-classified as a market-structure effect (likely
Law 1 restated, not a new edge). Only if the full model beats
market_form_onlyby a margin with z ≥ 2 does the lineup thesis survive.Step 2 — CLV leg (ONLY if Step 1 keeps the lineup lift)
Directional accuracy is not bankability. Bet the predicted move direction
at the OPEN line, settle at the CLOSE, report mean CLV + season-bootstrap
CI. No seeding until mean CLV > 0 out of sample.Step 3 — unblock Track B: stage line + total ticks
The repricing curve is unmeasurable because the historical backfill and
the forward archive stage h2h only (28 events, 0 line/total ticks). Fix
the staging sostg_odds_api_history/stg_match_price_tickscarry line
and total, and re-run the backfill for R11+ to capture them. This is the
binding constraint on the entire timing thesis.Report back
results-line-move-v1b.{json,md}with all five variants; if Step 1 fails
the lineup channel, say so plainly and STOP (do not proceed to Step 2).
Deviations in## Deviations.2026-07-21 (evening) — superseded 2026-07-24 (line-move-v1 executed; verdict above)
The goal, one sentence: predict WHERE THE LINE IS GOING from lineup
news, not where the margin lands — the first model target aligned with
all three laws (open soft in direction; magnitude unbeatable; lineup
info priced late). Pre-registered as Goal 2 in docs/line-v2-roadmap.md;
owner activated it tonight for an overnight autonomous run.Overnight autonomy rules (hard): ZERO API calls — every input is
already staged/local (historical file, team lists, RAPM, R11+ tick
history). Do not touch collectors, secrets, CronJobs, or the UI. Gates
below are frozen — a failed gate is a committed result, not a reason to
adjust. Reporter artifacts only; deviations in## Deviations; STOP
when both tracks are committed.Track A —
line_move_v1backtest (statistical power)Target:
sign(home_line_close − home_line_open)from the historical
file (zero-moves excluded; pushes reported). Walk-forward by season.Feature clock (precise leakage rule): allowed = anything knowable
Tuesday ~4:05pm AEST of the match week — the target round's TEAM LISTS
(they ARE the news; using them is the point, not leakage),
rapm_lineup_deltabuilt from rounds < target,
delta_availability_loss(BUILD IT if absent — spec in
line-v2-roadmap: RAPM-weighted sum of players named in r−1 with minutes
but missing from r's list, home − away), spine/starter deltas, r−1 form.
Forbidden = ANY price after the open (no close, no refresh, nothing).
Commit the leakage pre-test BEFORE the first backtest run.Gates (frozen; one scrutiny upgrade pre-registered now): directional
accuracy ≥ 53% AND ≥ (majority-class base rate + 2pp) — if line moves
skew toward favourites, beating a coin is not beating the base rate;
z ≥ 2 against the base rate; n ≥ 500 non-zero moves; 2023+ slice broken
out and reported. Report per-feature ablations (full vs minus-availability
vs minus-RAPM) so the review can see WHICH news moves lines.
Run viareport_strategy_shift.py --phase line-move-v1.Track B — the REAL target measured (report-only, small n)
From
stg_odds_api_history(R11+ price-change events, exact move
timestamps): (1) rebuild artifact A1 properly — the Tuesday repricing
curve with REAL event timing (share of each game's open→close line move
completed by T+30m/+1h/+2h/+4h/+12h after the Tuesday drop), per
market; (2) for the ~80 games available, open→Tuesday-evening line move
vs the Track-A features — REPORT ONLY (n far below gates), as the
calibration prior for whereline_move_tuesday_v1gates get set when
the archive matures. If a round's ticks aren't staged yet, stage from
Spaces first (local work, no API).Morning deliverables (what the review reads at breakfast)
results-line-move-v1.{json,md}with gates + ablations;
reports/repricing_curve_2026.md(A1 v2) with the curve table; ledger/
staging commits; Deviations. The weekend strategic review runs on these.2026-07-21 — CURRENT ROUND (supersedes below): business-plan data scrape, window-day close-out first
Context: owner purchased a Business API — and it is theoddsapi.com,
a DIFFERENT vendor from the-odds-api.com our live collectors use (header
auth, no /v4, premium /edges/ /intelligence/ /props/ endpoints, 6,667/day,
50+ books incl Pinnacle). Client shipped:data/theoddsapi_business.py.
Two coverage facts: NRL historical starts ~May 2026 (one-time 2026 grab,
not multi-season); the durable prize is the FORWARD proprietary archive
(docs/match-observation-archive.md). Owner also wants multi-sport
capture: NRL + AFL + UFC + rugby union + WNRL. New env key:
ODDS_API_BUSINESS(distinct fromODDS_API_KEY; no fallback). Every
paid step is probe-first and budget-capped. Owner sequencing rule: data
→ evidence → THEN the strategic review. Do not reorder. Standing verdicts unchanged
(open_fav_v1 KEEP/live; underdog REJECTED; magnitude models rejected
by Law 2 — see docs/edge-review-2026-07-20.md; Goal 2 deferred to the
weekend review, which now runs on this new data).Step 0 — window-day close-out (IF NOT ALREADY DONE, do this first)
- Secret re-apply for
scrapers-runtime-secrets(stale
NRL_BUCKET_SECRET_KEY+ODDS_API_KEY; fix verified ready in
go-cicd-opsdocs/cli-brief.mdround result). Add
ODDS_API_BUSINESSto the local secrets env file BEFORE running the
re-apply — one apply carries both fixes.- After 18:00 UTC, verify the first Tuesday window:
bash kubectl -n doks-ci-cd get jobs -l app.kubernetes.io/component=teamlist-window # expect 6 python scripts/reconcile_k8s_raw_runs.py --stage-pending python scripts/audit_snapshot_coverage.py --season 2026 --round 21Commit the audit output to
reports/live_evidence.md. Gate: /ops
coverage panel agrees with the CLI output.Step 1 — key placement + PROBE the new API (resolves everything below)
ODDS_API_BUSINESSinto the VM.env(rides the Step-0 secret re-apply
for the cluster; no CronJob uses it). Then run the probe — it is the
measure-don't-assume gate for the whole round:
bash export ODDS_API_BUSINESS=<key> python scripts/probe_theoddsapi_business.py \ --historical-date 2026-06-10T06:00:00ZInspect
reports/theoddsapi_probe/*.jsonand COMMIT them, then confirm
and record in a deviation note:
1./meworks (header authx-api-key, not the old?apiKey=).
2.sports.json→ the REAL keys for NRL/AFL/UFC/rugby-union/WNRL
(updateSPORT_KEYSintheoddsapi_business.pywith confirmed keys;
WNRL may not exist — record if so).
3.odds.json→ does the payload actually carry Pinnacle + Betfair +
the book count we expect.
4. rate headers → per-call cost and daily remaining (6,667/day budget).
5.edges/intelligenceshapes → note for collect-and-validate wiring.
6.historicalwith the 2026 date → confirms coverage + response shape.Decision to surface back to chat after the probe: migrate the live
NRL collectors to theoddsapi.com, or keep the-odds-api.com for live and
use the new vendor for premium + historical only. Do NOT migrate the
live pipeline unprompted — flag the recommendation with the probe
evidence.Step 2 — point the backfill at the new vendor, then dry-run + probe
scripts/backfill_odds_api_history.py+data/odds_api_history.pywere
written against the OLD API (the-odds-api.com/v4). Before any paid
historical run, repoint them atdata/theoddsapi_business.historical_odds
(header auth,/historical/odds/,sport_key=) — the probe's
historical.jsonconfirms the exact params and response shape. Keep the
budget walker (measured cost, ledger, hard stops) intact; only the fetch
function changes. Then:
bash python scripts/backfill_odds_api_history.py --season 2026 --round 12 --dry-run python scripts/backfill_odds_api_history.py --season 2026 --round 12 \ --credit-budget <cap> --min-remaining <floor>Record measured cost/call and the earliest date with data; commit an
affordability table (deviation note). Gate: no bulk grab until the
table is committed and projected spend fits the daily budget.Step 3 — ONE-TIME grab: 2026 season-to-date only (~R10 → R21)
There is no multi-season phase — only 2026 from the coverage start:
bash for R in $(seq 21 -1 10); do python scripts/backfill_odds_api_history.py --season 2026 --round $R \ --credit-budget 2000 --min-remaining <floor> || break doneStop at the first round that returns empty (coverage boundary). Commit
reports/odds_api_history_ledger.jsonafter the run — NEVER delete it.Step 3b — stand up the FORWARD archive (the real prize)
Per
docs/match-observation-archive.md— this is where the durable edge
is, since history doesn't go back. In priority order:
1. Add Pinnacle to the decision snapshots:regions=au,eu, bookmaker
keypinnacle. Confirm it returns for NRL before relying on it; keep
two-region calls to open + Tuesday window + final-hour only.
2. Final-hour capture every 5–10 min per kickoff (cron off the staged
draw, or a k8s CronJob) — the close curve, where CLV is won.
3. Wiremarket_reference.py(shipped this round: de-vig, consensus,
sharp anchor, soft-vs-sharp divergence) into staging so every snapshot
carries its derived reference columns.
4.stg_match_price_ticks+gold_match_observationdimension joining
team lists, weather, availability, final score, derived columns.
Store BOTH vendor/intelligence/fair-odds/consensus AND our
market_reference.pyvalues — disagreement is a data-quality signal.Step 3c — multi-sport capture (NRL priority; others archive-only)
Probe CONFIRMED sports on the plan: NRL, AFL, UFC (rugby union + WNRL
are NOT on the 26-sport plan — do not chase them). Extend archive
collection to AFL + UFC. CAPTURE ONLY — no modelling splits off NRL (Law
2 applies to every sport; capture is cheap, models are the scarce
resource). NRL keeps the full open/Tuesday-window/final-hour cadence; AFL
and UFC get open + pre-kickoff snapshots so the proprietary history
starts accruing now.Step 3d — convert LIVE collectors to theoddsapi (primary); keep legacy
Historical is already on the new vendor. Now migrate the LIVE path:
1.data/odds_provider.py(shipped):ODDS_PROVIDER=theoddsapiis
primary; the-odds-api.com stays the fallback. Route
collect_nrl_odds_snapshot.pythrough the active provider's client
instead of hard-importingodds_api.py.
2. Decimal everywhere: every theoddsapi request already forces
oddsFormat=decimal; wireodds_format.coerce_decimal(shipped) into
the staging normalizer so /edges//intelligence//props prices — which
may ignore the param — are still stored decimal. Add a staging test
with an American-valued fixture row proving it lands decimal.
3. Do NOT decommission the-odds-api.com CronJobs — leave the Basic key
live as the documented switch-back (season-dependent).Step 3e — collect the INTELLIGENCE (the "any intelligence we can get")
Per snapshot on decision points (open, Tuesday window, final hour),
also pull/intelligence/fair-odds/,/intelligence/consensus/, and
/edges/for NRL; land raw to Spaces and stage to
stg_odds_intelligence. Store the vendor's fair-odds/consensus ALONGSIDE
ourmarket_reference.pyvalues — disagreement is a data-quality signal.
Vendor edge scores are validation targets, never signal — run them
through CLA/CLV like any candidate before they touch a recommendation.Step 3f — dashboard rebuild (scoped in docs/dashboard-rebuild-v2.md)
Build order there: finish the /ops new-feed monitor rows (shipped this
round — add per-sport coverage + rate-limit remaining), then the
Bet-Intelligence panel (our fair prob vs Pinnacle vs consensus vs best
line + divergence, vendor edge marked unvalidated), then the
repricing-curve sparkline, then the multi-sport switcher. Each slice is
its own round behind tests; thetest_agent_guardrails.pysuite
(canonical app + model attribution) must stay green.Step 4 — staging:
stg_odds_api_historyNew script mirroring
stage_odds_ticks.py: flatten payloads
(event → bookmaker → market → outcome, keyed by the API's actual
snapshot_ts) intostg_odds_api_history. One row per
(snapshot_ts, event, bookmaker, market, side); dbt schema tests for
non-null keys and dedup on the actual timestamp.Step 5 — the three evidence artifacts the plan was bought for
Committed artifacts, in order:
- Tuesday repricing curve — avg |line move| and |h2h implied-prob
move| by time-since-drop (pre-drop, +30m, +1h, +2h, +4h, +12h, +1d,
close), per market. Sources: the 2026 historical grab (Step 3) PLUS
live tlwindow captures as they accrue — pooled, since both are 2026.
THE number the timing thesis lives or dies on.- True intraweek CLA for open_fav_v1 — confirmation measured at
the Tuesday-evening price, not the close.- line_move_v1 real backtest — target = open → Tuesday-evening
line move, placement-time lineup features, gates exactly as
pre-registered in docs/line-v2-roadmap.md Goal 2 (dir. accuracy
≥53%, z ≥ 2, n ≥ 500, 2023+ slice broken out). Run via the
strategy-shift reporter.Report back
Push after each step, not in one batch: Step 0 audit output +
live_evidence rows; Step 2 affordability table (deviation note);
Step 3 ledger commits per season; Step 4 staging + tests; Step 5
artifacts in reports/. Deviations in## Deviationssections. When
Step 5 is pushed, STOP — the reviewer session runs the strategic
review (ingest re-pave, model roadmap, Betfair-row closure) before any
further model work.2026-07-20 — superseded 2026-07-21 (window-day items absorbed into Step 0 above)
The clock: the four
scrapers-tlwindow-*CronJobs fire for the
first time Tuesday 06:05–18:00 UTC (tomorrow). They can only
succeed if the post-revert image is live — the bootstrap image lacks
theteamlist_windowlabel and the jobs will crash on the argument
parser. Everything below is ordered by that deadline.Deferred by owner decision: Goal 2 (
line_move_v1) and the
line-v2 review are REVISITED THIS WEEKEND after the round settles. Do
not start new model phases this week. The three goal-session negatives
stand as recorded (see line-v2-roadmap "Goal 2" section for context).1 — TONIGHT: go-cicd-ops registry revert, end to end
Execute
go-cicd-ops:docs/cli-brief.mdSteps 1–3 exactly (retag →
flip the two manifests + scrapers trigger-template → prove the
pipeline with a push to scrapers main). Then copy the NEW digest that
update-gitops-imagewrites intoscrapers-cronjob.yamlinto the
four image lines ofscrapers-teamlist-window-cronjob.yamland apply.
Gate before bed:kubectl -n doks-ci-cd get cronjobsshows the four
tlwindow jobs with the new digest and no ImagePullBackOff on a manual
kubectl create job --from=cronjob/scrapers-tlwindow-t0 tlwindow-dryrun.
ConfirmODDS_ROUND=21(or the round-control object) so the jobs
collect the right round.2 — TONIGHT (parallel-safe): DuckDB snapshot to the hosted board
Publish
sync/duckdb/nrl_bucket-<yyyymmdd>.duckdb+ sha256 manifest
from the VM, addDUCKDB_PATH+NRL_DUCKDB_SNAPSHOT_KEYto
scrapers-ui-service.yaml, re-smoke the six routes. Gate:
vb-match-cardrenders R21; /ops shows real GIT_COMMIT and the new
"Odds collect — teamlist_window" contract row (merged in e911999).3 — TUESDAY: verify the first window fires
After 18:00 UTC:
kubectl -n doks-ci-cd get jobs -l app.kubernetes.io/component=teamlist-window— four completions; four
_run.jsonmanifests in Spaces;reconcile_k8s_raw_runs.py --stage-pendingstages them; then
python scripts/audit_snapshot_coverage.py --season 2026 --round 21
and commit the output to reports/live_evidence.md. The /ops coverage
audit panel should agree with the CLI output — if they disagree,
that's a bug report.4 — THROUGH THE WEEK: R21 live quota (unchanged)
Thursday weather → regen → seed IN THAT ORDER; per-kickoff closes;
CLV ≤24h; observation-CLV for all picks; SGM dog+under quotes
(~8, manual CSV is fine).5 — SUNDAY: production-readiness verdict
The seven checks (07-16 round). Record "production-ready: YES/NO" +
blockers in reports/live_evidence.md. This and the R21 audit output
are what the weekend goal review will be based on — make them true.Report back
Push after each numbered item completes, not in one batch — the
reviewer session will be watching the branch through the week.2026-07-18 — superseded 2026-07-20 (weekly items absorbed into the urgent round above)
Context: R21 week. Verdicts standing: open_fav_v1 KEEP (live CLV
adjudicates); underdog overlay REJECTED (re-run done 655aee5 — await
review); LightGBM diagnostic-only; ridge KEEP; dog×under multi:
correlation CONFIRMED, edge NOT (results-dog-under-multi.md).
Priority order below is execution order.1 — R21 operating sequence (Tuesday–Sunday, the live quota)
Tuesday: team lists ≤30min after drop → verify the four
teamlist_windowsnapshots fired (needs go-cicd-ops rounds done — see
3). Thursday: weather refresh → prediction regen → THEN seed paper
trades (sequencing rule; timestamps into live_evidence.md). Per
kickoff: close snapshots. Sunday+24h: CLV settled, observation-CLV
logged for ALL model picks, not just qualifying ones.2 — collector coverage audit (new tool, run for R20 then R21)
python scripts/audit_snapshot_coverage.py --season 2026 --round 20
(then--round 21post-Tuesday). Checks open-first, close in
[−120m, kickoff], gaps ≤8h, h2h+line+total at close, teamlist_window
clusters, vm/cluster mix. Exit 1 = FAIL = live-quota incident. Commit
both outputs to reports/live_evidence.md.3 — go-cicd-ops rounds (fixes the hosted board; blocks 1's Tuesday leg)
Execute
go-cicd-ops:docs/cli-brief.mdtop to bottom: registry revert
(retag → manifest flips → trigger-template → pipeline proof) → DuckDB
snapshot env wiring (DUCKDB_PATH+NRL_DUCKDB_SNAPSHOT_KEY; VM
publishes the checksummed snapshot post-staging) → tlwindow CronJobs
image digest → six-route re-smoke. The GIT_COMMIT build-arg and
canonicalweb.mainapp are already on main here.4 — line-v2 P0 → P1 (the model work; start once 1–3 are moving)
P0 checklist from docs/line-v2-roadmap.md (coverage query,
position-resolver pytest, leakage pre-test), then the four
pre-registered P1 features in dbt: spine+lock cohesion, lock playmaker
index, games-together,delta_offball_effort, plus
delta_availability_loss(implicit injury). One reporter phase
(line-v2a), gates G-L1/G-L2 vs line_ridge_core 13.46 — no
per-feature cherry-picking.5 — observations and audits (small, alongside)
- SGM quotes: one book's dog+under same-game-multi quote per R21 game
vs the naive leg product (CSV: game, book, legs, quote, timestamp).- LightGBM data audit (carried): DuckDB training rows vs Excel extract
— regenerate-vs-retire decision input. No retrain.- origin_week_flag + rain_flag backfill status (was wiring-bugged).
6 — PRODUCTION-READINESS VERDICT (end of R21 — the round's deliverable)
The seven checks from the 07-16 round (hosted cards render, GIT_COMMIT
stamped, four Tuesday snapshots staged, sequencing order proven, CLV
≤24h, observation-CLV live, contracts green) — record
"production-ready: YES/NO" with blockers in
reports/live_evidence.md. A false YES is worse than a NO.Report back
Audit outputs + R21 evidence rows in live_evidence.md; line-v2a results
via the reporter when P1 lands; SGM CSV committed; deviations in the
results MD## Deviationssection.2026-07-17 — superseded 2026-07-18 (Sol verdict recorded; steps carried above)
Sol review verdict (commits 9a06467, d665cb8) — mostly KEEP, two
corrections applied by the reviewer on this branch:
- KEEP:
promotion_evaluator.py(fail-closed, canonical gates, stricter
extras),fetch_duckdb_snapshot.py(checksummed immutable snapshot —
this SUPERSEDES the plain-download plan; the pod needs
NRL_DUCKDB_SNAPSHOT_KEY+ manifest, see go-cicd-ops brief), round
control object (NRL_ROUND_CONTROL_KEY— kills the weekly ODDS_ROUND
configmap bump), reconcile/staging hardening + contract tests.- CORRECTED:
start_web.shservedweb.backend.app(React) — the
canonical app isweb.mainuntil the parity checklist passes; now
env-switchable viaWEB_APP, defaulting to canonical. The React
frontend build stage stays in the image (unused, harmless).- CORRECTED: the "model size" commit styled a model chip
(.vb-model-chip/.vb-model-id) but deleted the element — cards
showed no model probability or ID. Chip restored in the card header
using Sol's CSS.Step A — publish the versioned DuckDB snapshot (VM, closes the hosted board)
Sol's fetcher expects
s3://<bucket>/<key>+<key>.jsonmanifest with
a sha256. Add the post-staging publish step to the weekly flow: upload
duckdb/nrl_bucket.duckdbtosync/duckdb/nrl_bucket-<yyyymmdd>.duckdb
+ manifest, then setNRL_DUCKDB_SNAPSHOT_KEYin the scrapers-ui env
(go-cicd-ops). Gate: hosted /ops DuckDB row green via start_web.sh path.Step B — line-v2 Feature 4:
delta_offball_effort(owner-directed)Spec pre-registered in docs/line-v2-roadmap.md P1.4: defensive workrate
+ tackle efficiency + kick-return metres, minutes-weighted, trailing
4-round window, z-scored delta. Build in dbt alongside Features 1–3;
same v2a reporter phase, same gates. Do NOT extend the stats spider yet
(candidate feed logged in data-ingest-strategy).Step C — implicit availability delta (injury answer, owner-directed)
We collect NO injury data (audited 2026-07-17). Build route 1 from the
roadmap:delta_availability_loss= RAPM-weighted sum of players named
in r−1 with minutes but absent from r's team list, home − away. Data
already staged; placement-time safe. Casualty-ward scrape stays a
candidate feed — do not build it this round.Step D — run the collector coverage audit (owner-directed 2026-07-17)
New tool:
python scripts/audit_snapshot_coverage.py --season 2026 --round 20(and 21 after Tuesday). Per match it checks: open exists
and precedes everything, close lands in [−120m, kickoff], no capture
gap > 8h, h2h+line+total present at close; round-level it counts the
fourteamlist_windowTuesday clusters and reports the collector
(vm/cluster) mix. Exit 1 on FAIL. Commit the R20 output to
reports/live_evidence.md— this is the "did the cloud collector get
the right prices at the right points" answer, repeatable every round.
If R20 FAILs on close coverage, that's a live-quota incident to log.Step E — dog×under SGM quote observation (report-only follow-up)
The correlated-multi diagnostic (results-dog-under-multi.md) confirmed
the correlation (lift 1.084, CI low 1.010) but NOT the edge at naive
multiplied prices (−2.3% pooled). The open question is real SGM
pricing: for each R21 game, record one bookmaker's actual same-game
multi quote for [dog H2H × under] alongside the naive product of the
leg prices. Manual capture into a small CSV is fine (game, book, leg
prices, SGM quote, timestamp). ~8 quotes/week; after 3 rounds we know
whether books discount the combo more or less than the measured lift.
No betting logic, no strategy — observation only.2026-07-16 — steps continue (production-readiness R21 checklist stands)
Verdicts just issued: open_fav_v1 KEEP (CLA leg strong, ROI leg CIs
span zero — live CLV adjudicates from here); underdog overlay REJECTED
in 4 of 6 segments; ridge retrain KEEP (MAE 12.57); LightGBM NOT
reverted — demoted to diagnostic-only, below market baseline. R20
seeded trades stand (entry snapshot is immutable in paper_trades_v2).New standing rule (sequencing): Thursday weather refresh +
prediction regeneration run BEFORE paper-trade seeding, never after.
R20 seeded 30 min before the regen and the fresher model would have
disqualified the favourite pick — protocol was followed, but the order
was wrong. Encode this in the weekly-round/run-scrapers skill flow.Step 1 — populate the two unevaluable underdog segments (one re-run)
origin_week_flagis 0 for all 3,492 prediction rows (never populated —
fix the wiring: rounds 11–17 windows per season) andrain_flagis
absent from the predictions frame (weather coverage is now 99%+ — join
it in). Then re-run the segmentation ONCE (--phase open-fav-v1-roi2
or equivalent) reporting the SAME six segments with the SAME bar
(CLA ≥ 0.55, z ≥ 3, n ≥ 100). Whatever the result, the underdog
question closes after this pass.Step 2 — LightGBM decision input (no retrain yet)
Report only: how much training data does the DuckDB path yield vs the
stale Excel extract (rows, seasons, weather-null rate)? With that in
hand the review decides regenerate-vs-retire. Commit the numbers to the
results MD. Model binaries only land with a phase-labelled commit from
now on — no more "weather fixed".Step 3 — R20 close-out (live evidence quota)
Per-kickoff close snapshots through Sunday, CLV settled ≤24h post-round
for the 2 seeded open_fav_v1 trades,rapm_timing_v1seeding still
pending gates — record status honestly in reports/live_evidence.md.Step 4 — go-cicd-ops rounds (registry revert + team-list window jobs)
go-cicd-ops:docs/cli-brief.mdnow has TWO open items: the registry
revert (fixes the hosted board) and the NEW team-list market-window
CronJobs (Tue 06:05/06:35/08:00/18:00 UTC, labelteamlist_window) —
the measurement instrument for docs/line-v2-roadmap.md P3. Sequencing
note there: the window jobs need the image built AFTER the
VALID_LABELSchange in this repo, so registry revert first.Step 5 — line-v2 P0 prerequisites (this week, parallel-safe)
docs/line-v2-roadmap.mdis now the authority for line model work.
P0 checklist items that can start now: team-list coverage query,
position-resolver pytest (1/6/7/9/13 for ≥95% of squads across the 10
most common team-list formats), leakage pre-test for the delta columns.
No feature building until P0 is green.Step 6 — PRODUCTION-READINESS CONFIRMATION (next week, R21 — the goal)
Owner decision point: next week we confirm production is ready. The
definition — one full round runs hands-off with every instrument green:
# Check Evidence 1 Hosted board renders R21 cards six-route smoke, vb-match-cardpresent2 /ops shows real GIT_COMMIT + green DuckDB row registry revert + DB sync done 3 All four teamlist_windowsnapshots captured Tuesday + stagedreconciliation table, zero pending 4 Weather refresh → prediction regen → seeding, IN THAT ORDER timestamps in live_evidence.md 5 R20+R21 CLV settled ≤24h post-round live_evidence.md rows 6 Observation-CLV logged for ALL model picks (not just qualifying) new rows/table referenced in live_evidence.md 7 All /ops data contracts green screenshot or smoke note Record the verdict in
reports/live_evidence.mdas
"production-ready: YES/NO" with blockers listed. NO is an acceptable
answer — a false YES is not.Report back
Segmentation re-run results + LightGBM data audit in
reports/strategy_shift/; R20 CLV rows in live_evidence.md; smoke
results in ui-deployment-gaps.md. Deviations in the MD## Deviations
section — use the section this time.2026-07-15 — superseded 2026-07-16 (Steps 1–2 executed; results reviewed; deploy step still open via go-cicd-ops brief)
Context: owner review of the 07-14 round results + deploy diagnosis.
The deploy pipeline lives inHungee/go-cicd-ops(Tekton builds on push
to scrapers main →update-gitops-imagerewrites the digest in the
overlay manifests → ArgoCD syncs). The owner has upgraded DOCR to the
5-repository plan — the sharedparrillaimage repo workaround is to be
REVERTED. Verdicts recorded: bench prune DONE (owner confirmed);
open_fav_v1 CLA gate PASSED (0.737 / 0.690 on 2023+); ROI leg missing.Step 1 — fix the open_fav_v1 ROI leg and re-report
reports/strategy_shift/results-open-fav-v1.mdhas an EMPTY policies
table under the strategy section — the season-bootstrap ROI/CI never
rendered, so the verdict is one-legged. Diagnose (likely the strategy
path never feedssettle_policy/bootstrap, or the table renders before
policies attach), fix, and re-run:
bash python scripts/report_strategy_shift.py --phase open-fav-v1-roiGates unchanged from the pre-registration (CLA ≥ 0.60 — already passed;
ROI CI reported with 2023+ slice; ≥5 seasons AND ≥100 bets for any
significance claim). Seed the R20 paper trades (07-14 Step 2) only after
BOTH legs render.Step 2 — pre-registered UNDERDOG segmentation diagnostic (report-only)
Owner question: is there a business-logic overlay that rescues a dog
subset? Pooled dog picks are anti-signal — burden of proof is high.
Run ONE CLA segmentation pass over model-backs-underdog picks with these
FOUR pre-registered segments (no others, no cherry-picking; report all):
origin_week_flag = 1(favourite potentially Origin-depleted)rain_flag = 1(weather compresses margins)- dog open price ≤ 2.60 vs > 2.60 (short dogs vs longshots —
favourite–longshot bias predicts longshots are the worst side)- home dog vs away dog
Pre-registered bar (written before results, do not adjust): a segment is
only interesting at CLA ≥ 0.55 with z ≥ 3 and ≥ 100 signals — z ≥ 3
because four segments get a multiplicity discount. Anything below stays
anti-signal. This is a DIAGNOSTIC: no strategy, no seeding, regardless
of outcome. Append the table to the Step-1 results MD.Step 3 — deploy fix, REVISED for the 5-repo registry (go-cicd-ops)
The authoritative, sequenced version of this step now lives in
go-cicd-ops:docs/cli-brief.md(2026-07-15 round) — that repo now
operates the same brief pattern as this one, and its work lands
directly on main (GitOps: main is the deployed state). The kaniko
GIT_COMMIT build-arg is already applied there. The outline below is
kept for context; where they differ, the go-cicd-ops brief wins.
- Retag the current image into its own DOCR repo (from the VM):
crane copy registry.digitalocean.com/doks-ci-cd-reg/parrilla@sha256:bff5430... registry.digitalocean.com/doks-ci-cd-reg/scrapers:bootstrap
(or docker pull/tag/push if crane is absent; DOCR auto-creates the
repo on first push now the plan allows 5).- Flip the manifests in
go-cicd/overlays/prod/:
scrapers-ui-service.yamlandscrapers-cronjob.yaml(both the
odds-monitor digest ref and the team-listsparrilla:scrapers-bootstrap
tag) →registry.digitalocean.com/doks-ci-cd-reg/scrapers:bootstrap.- Flip the build target: in
go-cicd/base/tekton/configs/pipeline/trigger-template.yaml, the
scrapers-build-deploy-templateimage-repositorydefault →
registry.digitalocean.com/doks-ci-cd-reg/scrapers.- Stamp builds: in
go-cicd/base/tekton/configs/pipeline/pipeline.yaml, add
--build-arg=GIT_COMMIT=$(params.git-revision)to the kaniko
EXTRA_ARGS ofscheduled-job-build-deploy-pipeline(the one scrapers
uses; adding to the static-site pipeline too is harmless). This is
why /ops showsbuild=unknown— the ARG is never passed.- DuckDB into the UI pod — scrapers-ui is a Knative Service:
it cannot mount the RWO PVC (that PVC also only holds odds ticks, not
the staged DB). Route: VM uploadsduckdb/nrl_bucket.duckdbto
Spaces undersync/duckdb/after each staging run (add to the weekly
flow); scrapers-ui downloads it at startup (it already has SPACES_*
creds mounted) to local disk andNRL_DUCKDB_PATHpoints there —
add the env toscrapers-ui-service.yaml. Startup-download script
lives in the scrapers repo (scripts/), guarded so local runs are
unaffected.- Apply/merge, let Tekton+ArgoCD roll, then re-run the six-route
smoke — gate:vb-match-cardrenders with R20 data and /ops shows
the GIT_COMMIT stamp and a green DuckDB row.Step 4 — spine+lock cohesion (document-only this round)
Owner direction accepted: the modern lock (Yeo/Murray/Radley class) is a
fifth playmaker.docs/edge-risk-and-monitoring-plan.mdnow specs
spine+lock cohesion (1/6/7/9/13), a lock playmaker index, and cumulative
time-together as line-v2 candidates (the line model is not frozen).
No build this round — they queue behind Steps 1–3. Pre-register before
building.Step 5 — report back
Push: fixed reporter +
results-open-fav-v1-roi.{json,md}including the
underdog segmentation table (Steps 1–2); go-cicd-ops changes on a branch
there + smoke results appended todocs/ui-deployment-gaps.md(Step 3);
R20 seeding row inreports/live_evidence.mdonce both legs pass.
Failures and deviations in the results MD## Deviationssection.2026-07-14 — superseded 2026-07-15 (deploy diagnosis stands; PVC-mount fix REVISED by Step 3 above)
Context: docs-governance round (branch
claude/nrl-model-audit-edge-xysmwm) — seedocs/ui-value-board-review.md
for the value-board findings anddocs/executive-brief.mdfor the exec
position. Model verdicts are UNCHANGED from docs/edge-review-2026-07-12.md
(favourite-side confirmed; divergence killed; cpois retired; H2H frozen).Process change, effective now: cross-session communication is
standardised as skills — briefs are written withhandover-brief, results
are audited withresults-review(both in.claude/skills/, mirrored to
.codex/skills/). Read them once before starting.Step 1 — CARRIED FORWARD: report the open-fav-v1 backtest
The strategy code landed (
data/open_fav.py, reporter section in
scripts/report_strategy_shift.py) but no results file exists —
reports/strategy_shift/has nothing for this phase. Run:
bash python scripts/report_strategy_shift.py --phase open-fav-v1Gates are the pre-registered ones from the 07-12 brief, unadjusted:
filtered-subset CLA ≥ 0.60; ROI CI with the 2023+ slice broken out;
kill if filtered CLA < 0.55. Push the results whether they pass or fail.Step 2 — CARRIED FORWARD: R20 live evidence quota
reports/live_evidence.mdstill shows pending rows. Complete the R20
cycle (open snapshot → team lists → seedopen_fav_v1+rapm_timing_v1
paper trades → per-kickoff close snapshots → CLV ≤24h post-round), verify
green on /ops, and commit the R20 evidence row.Step 3 — NEW: model attribution on value-board cards (P1)
Per docs/ui-value-board-review.md:
dashboard.pyknows which model priced
each market butcard.primaryand the supporting rows never receive it.
Plumbmodel_idthrough the card context and render it as a small tag
next to the model probability (primary + each supporting row). Add a test
in tests/ asserting the partial contains the model tag when a prediction
carriesmodel_id. Gate:/partials/matchescards each name their model.Step 4 — NEW: badge honesty
Rename the value-board badge pair to
PAPER GO/WATCHat the service
layer (keep CSS classes). The title/aria quick wins are already in the
template; this makes the visible label honest too.Step 5 — NEW: authenticated smoke of the HOSTED app (VM only)
The reviewer session's egress cannot reach
scrapers-ui.doks-ci-cd.hungee.cloud(gateway 403 on CONNECT), so hosted
verification belongs to this session. The operator holds the BasicAuth
credentials — export them locally, never commit them:
bash export UI_SMOKE_AUTH="<user>:<pass>" # from operator, out-of-band for p in / /partials/matches?season=2026\&round=Round%2020 /models /strategy /ops /docs; do curl -sS -u "$UI_SMOKE_AUTH" -o /dev/null -w "%{http_code} $p\n" "https://scrapers-ui.doks-ci-cd.hungee.cloud$p" doneGate: all six return 200; the matches partial contains
vb-match-card
markup with real R20 data (not the empty state); /ops shows current DB
sync age. Record the outcomes in the ui-deployment-gaps smoke checklist.2026-07-15 update — data-not-loading DIAGNOSED (smoke completed).
Root cause confirmed via /ops and recorded indocs/ui-deployment-gaps.md:
the pod has no DuckDB at all (falls back to repo default path, not
found; every data-contract row UNAVAILABLE) and the image is stale
(build=unknown, predates the GIT_COMMIT arg). Remaining actions are
operator-level: rebuild the image with the GIT_COMMIT/IMAGE_BUILT_AT
build args, mount the PVC at/data, set
NRL_DUCKDB_PATH=/data/nrl_bucket.duckdbin the Deployment, run the
VM→PVC copy, then re-run the six-route smoke untilvb-match-card
renders. Step 5 is DONE for this session once those results are recorded.Step 6 — report back
Push:
reports/strategy_shift/results-open-fav-v1.{json,md}(Step 1),
the R20 row inreports/live_evidence.md(Step 2), the model-attribution
change + test (Step 3–4), and hosted smoke results appended to
docs/ui-deployment-gaps.md(Step 5). One branch for the round; failures
and deviations go in the results MD## Deviationssection.2026-07-12 — superseded 2026-07-14 (Steps 1–2 carried into current round; see docs/edge-review-2026-07-12.md)
Verdicts from the deep review: divergence signal KILLED (tested on full
history — the line stopped correcting toward H2H after 2018); CLA
segmentation found the edge's address: models backing the FAVOURITE at
open are confirmed by the market at 62.8–65% (z 5.5–6.9); backing dogs is
anti-signal. cpois RETIRED. rapm-v4 demoted. H2H features FROZEN.Step 1 —
open-fav-v1backtest verdict (the one model task this round)Implement the filtered strategy exactly as pre-registered in
edge-review-2026-07-12.md N1: bet at OPEN only when model edge ≥ 3pp toward
the favourite (that side's open price ≤ ~1.90), models ss_strength_v1 +
logit_rapm_v1, both must agree when both signal, shrunk quarter-Kelly
stakes. Suggested shape: a new policy or a filtered settle pass in
data/backtest.py + a--phase open-fav-v1report. Gates (pre-registered,
do not adjust): filtered-subset CLA ≥ 0.60; ROI CI reported with the
era-2023+ slice broken out; kill if filtered CLA < 0.55.
Tools you already have:closing_line_agreement_segments()in
data/calibration.py;line_h2h_divergencein data/edge_signals.py is the
killed signal — do NOT wire it anywhere.Step 2 — arm the live evidence quota (every round, starting R20)
Per docs/edge-review-2026-07-12.md N2: Monday open snapshot → team lists
≤30min after drop → seedopen_fav_v1ANDrapm_timing_v1paper trades →
per-kickoff close snapshots (schedule_close_snapshots.py) → CLV settled
≤24h post-round. Verify each item shows green on /ops afterwards; commit a
one-line evidence log per round to reports/live_evidence.md (round, trades
seeded per strategy, CLV coverage).Step 3 — report back
open-fav-v1 gates + the 2023+ slice; rounds of live evidence collected;
anything that blocked the quota. No other model phases this round.2026-07-08 review update (phase3-rapm-v2 verdict + new work):
- v2 verdict: G6 FAIL confirmed the prediction —
logit_rapm_v1is
bit-identical tologit_open_mkton every metric (0.6261 log-loss, same
733 CLA signals, same ROI to 4dp). Bit-identical is MORE than a zero
coefficient — diagnose whetherrapm_lineup_deltais actually reaching
the fitted frame: runbuild_feature_rows(...)and check
features["rapm_lineup_delta"].abs().sum() > 0and its variance per
season. If it's all zeros there is a join/wiring bug (report it); if it
has variance, the logit is genuinely ignoring it (also a result).- CLA headline (the edge): open-priced models' disagreements with the
open are confirmed by the close at 54–55% (z 2.7–3.4). The opening price
is soft — this is now the stated edge thesis on the /strategy page.logit_open_mktshrunk_kelly "+51% significant" on 39 bets was a
significance-rule gap — fixed: CIs now also require ≥100 bets.- Step 3/4 below (stats-prior RAPM) is still the main event — not run yet.
- New UI pages to smoke (ui-up / run-edgebet-ui skill):
/models
(catalogue + latest report metrics),/strategy(edge thesis, policies,
gates),/ops(pipeline status incl. per-label odds collection and
player-stats round lag). Verify /ops statuses match reality on the VM.- Weekly player stats:
weekly_2026_round.shnow rebuilds
gold_nrl_player_name_map+gold_nrl_player_round_statsevery week
(independent of SKIP_WEATHER). Verify next round's run and that /ops
shows "Player match stats" tracking the draw round.
Audience: the data-connected CLI session on the VM. Work top to bottom.
Reviewer contract unchanged: everything lands in reports/strategy_shift/
via scripts/report_strategy_shift.py and gets committed to
claude/nrl-model-audit-edge-xysmwm. Failed checks get pushed, not fixed in post.
What's new in code (this commit) — three tools aimed at finding a large
edge faster than paper-trade ROI can:
- Closing-line agreement (CLA) —
data/calibration.py::closing_line_agreement,
now in every standard report (schema v3). For each model: when it disagrees
with the opening price by ≥3pp, does the market move toward it by close?
This is the necessary condition for edge, measurable on ~3,500 matches
today instead of 100 paper trades next season. - RAPM time decay —
RAPMConfig(decay_half_life_seasons=...); old
seasons' evidence decays exponentially. - Stats-prior RAPM (offset ridge) —
_fit_rapm(..., player_priors=...)/
build_walk_forward_from_frames(..., priors_by_season=...): player ratings
shrink toward a box-score prior instead of toward zero. The mechanism is
built and tested; YOU construct the priors from
gold_nrl_player_round_stats(recipe in step 4).
Step 0 — still owed from the last plan
results-phase3-rapm-v2 was never pushed. Run it FIRST, before any of the new
options, so there is a clean leakage-free RAPM baseline to compare decay and
priors against:
git checkout claude/nrl-model-audit-edge-xysmwm && git pull
python -m pytest tests/ -q # must be green
python scripts/report_strategy_shift.py --phase phase3-rapm-v2
git add -A && git commit -m "phase3-rapm-v2: leakage-free RAPM vs ablation control" && git push
Step 1 — read the CLA table before anything else
The v2 report now includes Closing-line agreement. This reorders all
remaining work by expected payoff:
- Any model with agreement ≥ 0.53 and |z| ≥ 2 → its signal family is real;
prioritise everything touching that family. logit_rapm_v1CLA is the cheap preview of the team-list-timing hypothesis:
if lineup-driven disagreements with the open are confirmed by the close,
the Tuesday-window strategy has a live pulse. Report this number
prominently either way.- All models ~0.50 → the current features hold nothing the market confirms;
the stats-prior work in step 4 becomes the main event.
Step 2 — RAPM decay A/B (30 min)
python - <<'EOF'
import sys; sys.path.insert(0, "nrl-bet-advisor")
from data.rapm import RAPMConfig, build_rapm_walk_forward_features
import numpy as np
for label, config in [
("flat", RAPMConfig()),
("hl=1.5", RAPMConfig(decay_half_life_seasons=1.5)),
("hl=2.0", RAPMConfig(decay_half_life_seasons=2.0)),
("hl=3.0", RAPMConfig(decay_half_life_seasons=3.0)),
]:
feats = build_rapm_walk_forward_features(config)
deltas = [v["rapm_lineup_delta"] for v in feats.values()]
print(f"{label:8s} matches={len(deltas)} mean|delta|={np.mean(np.abs(deltas)):.3f}")
EOF
Pick the half-life by out-of-sample log-loss of logit_rapm_v1, NOT by
delta magnitude: temporarily set the chosen config in
backtest.py::_bulk_rapm_features' call (make it a module constant
RAPM_BACKTEST_CONFIG), re-run the reporter as --phase phase3-rapm-decay,
and keep whichever beats phase3-rapm-v2's logit_rapm_v1 log-loss. If none
do, keep flat and say so.
Step 3 — build the box-score player-value prior
Recipe (all walk-forward — priors for season S use data ≤ S−1 only):
- Per player-season, aggregate
gold_nrl_player_round_statsinto per-80-minute
rates: run metres, post-contact metres, line breaks, tackle breaks, try
involvements (tries + assists), tackles made, missed tackles, errors.
Weight by minutes; require ≥ 400 career minutes else prior = 0. - Fit stat weights by regressing MATCH margin on the differential of
team-aggregated per-80 rates (home minus away), seasons ≤ S−1, Ridge or OLS.
This converts stat lines into points-per-game currency honestly — do not
hand-pick weights, and do not use fantasy_points (arbitrary scoring). - Player prior for season S = dot(stat weights, player per-80 rates through
S−1), clipped to ±6 points (a prior should anchor, not dominate). - Build
priors_by_season = {S: {canonical_player_id: prior}}and pass it
throughbuild_walk_forward_from_frames(..., priors_by_season=...)— add a
build_rapm_stats_prior_features()wrapper indata/rapm.pymirroring the
existing builder, and aRAPM_BACKTEST_CONFIGswitch so the backtest can
select it. - Unit-test the prior constructor (≥3 tests: minutes threshold, walk-forward
cutoff respected, clipping).
Then:
python -m pytest tests/ -q
python scripts/report_strategy_shift.py --phase phase4-stats-prior
git add -A && git commit -m "phase4: stats-prior RAPM (xRAPM-style)" && git push
Success criteria (same discipline as always — decided before you look):
logit_rapm_v1 log-loss < both phase3-rapm-v2 AND logit_open_mkt (G6),
λ ≥ 0.10 (G7), and CLA ≥ 0.52 on its signals. ROI/CIs report-only.
Step 4 — arm the live channel (if not already running)
Unchanged from docs/phase3-v2-test-plan.md step 5: rapm_timing_v1 paper
trades at team-list drop, close snapshots per kickoff
(scripts/schedule_close_snapshots.py), CLV updated post-round. If phase 4's
prior model wins, use ITS deltas for the timing signal from the next round on.
Step 5 — report back (PR comment or thread)
- phase3-rapm-v2 gates + CLA table (the owed baseline)
- Decay A/B: chosen half-life and the log-loss it earned (or "flat kept")
- phase4 gates, λ, CLA — and the one-line answer: does the stats prior make
the lineup channel real? - Rounds of
rapm_timing_v1trades now collecting CLV - Any deviation from the prior recipe, in the MD deviations note
Guardrails (bitten once, now test-enforced — do not fight them)
- No closing features in open-priced models (
tests/test_leakage_guard.py). - Priors for season S from data ≤ S−1 only — the walk-forward guarantee is
YOURS in step 3; the reviewer will check the cutoff logic in your test. - No gate-threshold edits in a commit that reports results.
- CLA uses closing data as a diagnostic label; it must never become a feature.
2026-07-10 review update — CRITICAL: RAPM was never wired; phases void
Bit-identical results across phase3-rapm-v2 / decay / phase4 exposed a join
bug, now root-caused and fixed: the historical backtest frame has NO round
column (round_num = 0 for every match), so the round-keyed RAPM and lineup
caches NEVER matched. rapm_lineup_delta and ALL lineup features were zero in
every backtest ever run — including the original audit's "lineup coefficients
0.000". Decay and stats-prior A/Bs measured zeros; those reports are void.
Fixed in this commit: the RAPM builder emits kickoff-date-keyed aliases and
the backtest looks up date-first (the join pattern weather already used);
the lineup cache gets the same treatment via a draw-derived date map.
Regression tests: tests/test_rapm_wiring.py.
Run order for the VM (supersedes step 0–3 above):
git pull
python -m pytest tests/ -q
# 1. Verify the fix bites on real data (MUST be nonzero now):
python - <<'PY'
import sys; sys.path.insert(0, "nrl-bet-advisor")
from data.backtest import build_feature_rows, load_historical_source
f = build_feature_rows(load_historical_source())
print("rapm |delta| sum:", f["rapm_lineup_delta"].abs().sum())
print(f.groupby("season")["rapm_lineup_delta"].agg(["mean", "std"]).tail(8))
print("lineup nonzero rate:", (f["lineup_overall_delta"].abs() > 1e-9).mean())
PY
# 2. First REAL lineup-channel measurement:
python scripts/report_strategy_shift.py --phase phase3-rapm-v3
# 3. Then re-run the A/Bs that previously measured zeros:
# decay (set RAPM_BACKTEST_CONFIG) -> --phase phase3-rapm-decay-v2
# stats-prior -> --phase phase4-stats-prior-v2
git add -A && git commit -m "phase3-rapm-v3 + re-runs on fixed join" && git push
Expect logit_core_market / lineup_prior_year_v1 metrics to MOVE (lineup
features populate for the first time). That is the fix working, not a
regression. The roadmap (docs/edge-roadmap.md) sequences everything after.