Commit Graph

171 Commits

Author SHA1 Message Date
jgrusewski
99b747ad2c feat(surfer): turnover optimization resolves capacity — deployable momentum spec
Weekly rebalance + 5d weight-smoothing + top-30 liquid: gross Sharpe +0.80->+0.94
(smoothing cuts whipsaw), turnover 30%->10.6%, capacity dead-by-$20M -> net +0.76/$5M,
+0.58/$20M, +0.38/$50M, viable ~$100M. top-30 = breadth/liquidity optimum. The
small-capacity verdict was a daily-rebalance artifact. DEPLOYABLE SPEC: crypto XS
momentum, top-30 liquid perps, 20d signal, weekly rebal + 5d smoothing, market-neutral,
net Sharpe ~0.6-0.8 at $5-20M, survivorship-confirmed. Single edge (no validated diversifier).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:20:12 +02:00
jgrusewski
a2cbed4a83 feat(surfer): capstone — low-vol fails battery; momentum is small-capacity
(2) Low-vol FAILS its full battery (standalone ~0/neg all windows, coin-bootstrap
frac>0=0.45 coin-flip) -> its earlier diversifier value was a winsor artifact ->
NO validated second edge, momentum stands alone. (1) Slippage/capacity model
(sqrt-impact eta=1, daily rebalance): book gross +0.62 but net M +0.41 / M +0.22 /
0M -0.11 / 0M -0.50 -> real but small-capacity (~$1-5M), lives in smaller alts.
Favorable caveats: eta=1 conservative, daily rebalance of 20d signal wastes turnover.
Key untested lever: weekly rebalance + smoothing + top-20 liquid -> ~5x less turnover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:17:33 +02:00
jgrusewski
7edeec9106 feat(surfer): proper (a)robust-construction + (b)diversifier research
(a) All 6 momentum constructions collapse to ~0.2 under aggressive +/-20% winsor
(momentum leans on large moves); volscaled/winsorinput degrade least, none robust to
the cap. (b) LOWVOL is genuinely uncorrelated (corr -0.13) and lifts the stressed book
+0.19->+0.34; TStrend correlated; carry/reversal/size don't help. Book=momentum+lowvol.
Honest deployable bracket ~0.2 (harsh winsor) to ~0.6 (fair); +/-20% blanket winsor
double-counts (death-spiral already excluded) so truth ~0.5. Deciding next: per-coin
ADV slippage model, not blanket winsor. Corrected the script's over-claiming verdict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:12:55 +02:00
jgrusewski
ca3877c328 feat(surfer): execution-realism stress — honest momentum magnitude ~0.4-0.6
Realism layers on PIT mom_20 TOPK=50: robust to 20bp (+0.59) and liquidity floor
(top-50 already deep, no effect); death-spiral exclusion +0.60. Winsorize +/-20%/day
cuts to +0.50 and weakens 2021/22 -> edge leans partly on large moves. COMBINED
realistic stress +0.18 (CPCV~0); HARSH negative. winsor likely over-conservative ->
true deployable Sharpe ~0.4-0.6, not the 0.76 headline. Real but modest edge; recent
years strongest. Deploy needs signal-robustness refinement + diversifying 2nd edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:07:31 +02:00
jgrusewski
7495eaa286 feat(surfer): point-in-time survivorship test PASSED — momentum is real
Built survivorship-free universe: 135 perps incl 28 known-dead (LUNA/SRM/MATIC...),
universe rebuilt daily as top-K by trailing dollar-volume (dead coins in while trading,
drop out after crash), no lookahead. mom_20 TOPK=30: full +0.76, IS+0.85/OOS+0.56,
CPCV-med +0.73, DSR 0.86, POSITIVE EVERY YEAR 2020-2026 incl 2022 +0.72. Counterintuitive:
survivorship was HIDING the edge (survivor-only 2022 -0.40 -> PIT +0.72) because momentum
shorts the dying coins and profits from crashes. Decisive survivorship confirmation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:04:19 +02:00
jgrusewski
0d6d58428d feat(surfer): momentum robustness battery — crypto XS momentum is a robust real edge
Lookback sweep smooth+positive 5-120d (peak mom_20 DSR 0.95); coin-bootstrap 200
random half-universes frac>0=1.00, 5th-pct +0.34 (survivorship-proxy passed);
vol-scaled approx raw; survives 2x cost; IS+0.89/OOS+0.88. Crypto cross-sectional
momentum (>=45-coin breadth) passes every test that killed carry. One weakness:
negative 2019/2022 (momentum-crash) -> regime overlay. Develop-grade strongly met.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:18:59 +02:00
jgrusewski
0f1a04d16d fix(surfer): universe-robustness exposes carry as cherry-picked; momentum is robust
Broadened crypto universe 28 majors -> programmatic top-80 (64 usable), added
funding-coverage/history filter. Carry COLLAPSED (+0.82->-0.03) and stayed dead even
on broad-but-clean subset (fundcov>0.9, 45 coins): carry was cherry-picked to majors,
not robust. Cross-sectional MOMENTUM is the robust edge: XS_mom_30 IS+0.89/OOS+0.88,
CPCV-med +0.75, DSR 0.75, survives 2x cost, holds across 64- and 45-coin universes.
Momentum needs breadth (weak on 28 correlated majors). Same regime failure (2019/2022).
Methodology win: broadening killed a cherry-picked claim before a build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:16:56 +02:00
jgrusewski
af5e6d523b feat(surfer): broaden crypto sweep + regime/cost robustness — carry crash found
13 crypto signals, deflated by cumulative 30. Carry family all develop-grade, survive
2x cost; per-year reveals THE failure mode = 2022 deleveraging carry-crash (carry_7
-1.2 in 2022, +0.4..+2.1 other years). Momentum OOS-negative (decayed); chasing rising
funding strongly negative. Carry is the robust core; its one tail (carry crash) is
exactly what the ML regime overlay is built to manage -> the two threads unite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:10:40 +02:00
jgrusewski
cb402ab81f feat(surfer): crypto carry+momentum sweep — FIRST real edge (funding carry)
Batch 2 of the deflated signal sweep: cross-sectional funding carry + momentum on
28 major Binance USDT perps (2019-26, funding baked into return, 10bp cost, deflated
by cumulative 25 trials). XS_carry_7 = Sharpe +0.82, IS+0.77/OOS+0.93 (consistent),
CPCV-median +0.78 (robust across 45 paths), DSR 0.71. First signal all session that is
positive + IS/OOS-consistent + CPCV-median-positive. Develop-grade met, not yet deploy
(DSR<0.95, 5th-pct<0). Caveats: survivorship (current majors), confirm point-in-time.
Forward-paginated funding fetch (fundcov ~1.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:05:20 +02:00
jgrusewski
1283812c54 feat(surfer): deflated signal-sweep engine + Batch 1 (futures factor zoo = null)
Broad signal search judged against the FULL search (Deflated Sharpe deflated by
N_trials) — the multiple-testing discipline that makes breadth safe. Batch 1: 17
signals (XS/TS momentum, reversal, low-vol, seasonality) on 22-futures daily ->
ZERO survivors (best DSR 0.29); CPCV+deflation correctly kill the lucky cells.
SEAS_overnight invalid on 24h futures (equity-cash anomaly). Next batches: crypto, COT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:54:06 +02:00
jgrusewski
1cf38232e8 fix(surfer): full-16y MFT settle — regime-adaptive surfer significantly negative
Pulled 16y ES ohlcv-1m continuous (year-chunked, $20 credits, no 504) + light
to_ndarray loader (to_df OOM'd at 20GB). On 1.1M 5-min bars the 1.3y +0.50/t=0.67
top-5% hint collapsed: regime-adaptive top-5% = -0.52 ticks/trade t=-2.51
(significantly negative); ALL cells/signals/convictions significantly negative.
Decisive: no capturable intraday directional edge for crossing/non-colocated setup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 12:15:37 +02:00
jgrusewski
fa700c7b5f feat(surfer): MFT regime-adaptive quality test (intraday, GPU, 4GB-safe)
Tests the actual surfer thesis: intraday 5-min decisions/15-min holds, flat-by-close,
regime-adaptive (ride trend / fade range / stand aside), quality-over-quantity
(top-conviction only). Clean ES OHLCV-1m -> 5-min bars, per-trade net edge in ticks,
IS/OOS + t-stat. Result: regime-adaptive top-5% = +0.50 ticks/trade OOS, sign-consistent,
but t=0.67 (not significant); all else significantly negative. Adaptive+selective is the
only non-losing structure (thesis directionally right, within noise). 4GB-safe: cumsum
rolling-std (no unfold) + alloc cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:08:16 +02:00
jgrusewski
2bb2b50fd1 fix(surfer): correct roll handling (held-contract return stitching) + cost stress + dual gates
The +0.32 'edge' was a roll-zeroing artifact (discarded held-contract roll-day P&L).
Correct roll stitching + 2bp cost: floor Sharpe ~0.05 (sign) / 0.06 (cont) / 0.14
(cont+band), all NEGATIVE at 2x cost. Marginal, implementation-dominated edge.
Adds FLOOR_SIGNAL mode (sign|cont), no-trade band, 2x-cost stress, develop/deploy gates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 01:43:06 +02:00
jgrusewski
8df1c7eea2 feat(surfer): Phase 0 GPU floor + validation harness — first real (weak) OOS trend edge
PyTorch-GPU diversified-trend floor (TSMOM 1/3/12mo + inverse-vol + 10% vol-target)
+ CPCV/Deflated-Sharpe validation, over 22 CME futures x 19.7y (Databento GLBX
ohlcv-1d via budget-capped fetcher, ~$32 credits). Verdict: Sharpe +0.32, CPCV
median +0.30, IS +0.36/OOS +0.08 (sign-consistent), Deflated Sharpe ~0.92 at honest
n_trials. Real but weak edge; fails deploy-grade gates (correct), passes edge-exists.
Includes roll-neutralization fix (max-vol outright + zero roll-day returns) that
eliminated 988%/day continuous-contract artifacts (RB vol 550%->31%). Plus
install_torch_gpu.sh. Data gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 01:31:37 +02:00
jgrusewski
44cbeef321 feat(scripts): cross-asset trend/lead-lag harness (6E/ES/NQ/ZN OHLCV)
Per-instrument time-series momentum + diversified trend portfolio + lead-lag,
honest IS(2024)/OOS(2025) Sharpe on non-overlapping daily P&L.

Verdict on 2y/4-instrument data: diversified trend portfolio IS Sharpe +1.98
(L10) -> OOS -0.50; per-instrument TSMOM sign-flips IS<->OOS; lead-lag ICs <0.11.
No robust edge. Conclusion: 4 instruments x 2y is underpowered by 1-2 orders of
magnitude for systematic edge discovery -- the bottleneck is data/universe, not
model. Real progress needs a broad futures universe + decades, or a less-efficient
order-book market (crypto).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:39:31 +02:00
jgrusewski
292147090f feat(scripts): lower-frequency directional IC harness (intraday + daily + backtest)
Tests minutes-to-days directional predictability on ES OHLCV-1m where moves
dwarf the spread: momentum/reversion IC (Pearson/Spearman/NW-t/OOS) + a
non-overlapping daily contrarian backtest (IS/OOS Sharpe).

Verdict on 2y ES (2024-25): intraday IC~0; daily mean-reversion IC looks large
(-0.44 Spearman @20d) but is overlap-inflated + regime artifact -- honest
non-overlapping backtest sign-flips IS<->OOS => no stable edge. Confirms no
robustly capturable directional edge on 2y ES across all measured horizons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:58:18 +02:00
jgrusewski
343e90af54 feat(scripts): queue-aware passive-MM fill model (decisive viability test)
FIFO queue priority via price-level episodes: join back of queue, only trades
advance, fill when same-side aggressor volume clears the queue. Re-measures
markout + OFI-conditioning on ACTUALLY-WON (adversely-biased) fills.

Verdict (Q1+Q2 2024 ES): ~40% fill rate; won-fill markout -0.50..-0.55t
(winner's curse vs optimistic -0.40t); naive AND OFI-conditioned passive MM
both NEGATIVE (-$1..-$2/fill). The optimistic Q2 OFI tilt was a win-every-queue
artifact. Combined with the crossing result: no capturable seconds-horizon ES
edge for a non-colocated participant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:28:05 +02:00
jgrusewski
f6745400ef feat(scripts): passive market-making markout/adverse-selection harness
Measures passive-MM economics on clean ES MBP-10: half-spread earned vs
adverse-selection markout at horizons, plus OFI-conditioned selective quoting.
Verdict on 2024 ES: adverse selection ~0.4 tick eats ~80% of the 0.5-tick
half-spread => naive passive MM is break-even (optimistic, win-every-queue;
realistically negative). OFI-conditioned selective quoting shows a consistent
edge in Q2 (+$0.5/fill) but not Q1 => promising but unconfirmed; decisive
next test is a realistic queue/fill model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:21:30 +02:00
jgrusewski
7b33533bc8 feat(scripts): OFI information-coefficient signal-validation harness
Measures best-level Order-Flow-Imbalance (Cont-Kukanov-Stoikov) predictive
edge on clean ES MBP-10 vs forward returns: contemporaneous R², forward IC
(Pearson/Spearman), sign-accuracy, Newey-West t-stat across horizons, and a
CROSSING verdict (predicted move in ticks vs the 1-tick round-trip cost).
No training/RL. Verdict on clean 2024 ES: OFI fwd IC tiny (~0.02, Q2 NW-t 3.7),
predicted moves 0.01-0.06 ticks at 1-10s << 1-tick crossing cost (~20-100x gap)
=> spread-crossing directional strategy is structurally unprofitable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:14:26 +02:00
jgrusewski
e57b076577 feat(scripts): reward-pnl alignment measurement harness (RA-G3 gate)
Computes Pearson(rewards.sum, trading.realized_pnl_usd_delta) + sign-agreement
from a diag.jsonl. Exit 0 if Pearson >= 0.70 (RA-G3), 1 below, 2 on bad input.
Reproduces the baseline 0.2775 / sign 0.655 measurement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:23:29 +02:00
jgrusewski
f593b1e617 feat(infra): plumb FOXHUNT_BAND_ENABLED through alpha-rl template
Mirror of the FOXHUNT_USE_MULTI_HEAD_POLICY pattern (c5036af03).
Adds --band-enabled flag to argo-alpha-rl.sh and band-enabled
workflow parameter wired into the container env block.

Required for Phase 4-C cluster verification of the full Tier 3
no-transaction-band stack (commits e41a7320865c328d3f).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 09:05:25 +02:00
jgrusewski
c5036af030 feat(infra): plumb FOXHUNT_USE_MULTI_HEAD_POLICY through alpha-rl template
Adds use-multi-head-policy workflow parameter (default "0") wired
into the container env block as FOXHUNT_USE_MULTI_HEAD_POLICY.
Adds --use-multi-head-policy flag to argo-alpha-rl.sh that passes
through to the workflow.

Enables Phase 2A-E cluster verification with the multi-head policy
mixture (commits 0b3e40150c8c81ab7e). Without this flag, the
cluster runs the legacy single-head path identical to pre-Phase-2A
behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 16:46:33 +02:00
jgrusewski
cfaa420bd4 fix(ml-alpha): Phase 0 — per-step authoritative USD pnl in diag + fix mislabeled fields
The diag's `trading.*_pnl_*_usd` fields were systematically mislabeled, making
the eval verdict signal untrustworthy. Three quantities shared the `_usd`
suffix but were not in USD:

  - `eval_summary.total_pnl_usd` (USD ground truth, $50/pt ES applied)
  - `trading.realized_pnl_cum_usd` (cumulative SHAPED reward at close events;
    pts × lots × Phase-5 shaping; NOT USD despite the suffix)
  - `trading.pnl_cum_usd` (mathematically nonsense — reads rewards_d after
    apply_reward_scale AND rl_popart_normalize whiten it in-place; dividing
    by current_scale un-does only the first transform)

At baseline mid-smoke (b=128, 2000+500, seed=42): eval_summary.total_pnl_usd
= -$4,457,625 while diag.trading.realized_pnl_cum_usd = +$469k and
diag.trading.pnl_cum_usd = +$12.9k. 346x ratio, sign-opposite. Same
mechanism as `pearl_grwwh_eval_catastrophic_collapse` ($234M cluster
discrepancy). Every Pearson(reward, Δpnl_usd) computation against these
fields was shaped-vs-shaped tautology, not pnl alignment.

This commit:

  DELETE `trading.pnl_cum_usd` (mathematically nonsense post-popart).

  RENAME `trading.realized_pnl_cum_usd` -> `trading.shaped_reward_close_event_cum`.
  Truthful name; the underlying values are post-Phase-5 shaped reward
  summed at close events, useful for gradient-signal diagnostics but never
  to be confused with USD pnl.

  ADD `trading.realized_pnl_usd_delta` and `trading.realized_pnl_usd_cum` in
  diag.jsonl / eval_diag.jsonl. Source: new per-batch float buffer
  pnl_step_close_usd_d on LobSimCuda, zeroed via raw_memset_d8_zero before
  each pnl_track_step launch, written by pnl_track.cu's close branch as
  realised_pnl_usd_fp / 100.0 (same arithmetic as eval_summary's
  TradeRecord aggregator, applying ES $50/pt). Single-writer per block —
  no atomicAdd per feedback_no_atomicadd.

  Direct readback via read_slice_d_into<f32> from sim.pnl_step_close_usd_d()
  after step_with_lobsim_gpu returns (main stream already synchronized via
  self.stream.synchronize() at pnl_track_step exit). NOT routed through
  diag_staging double-buffer — a prior implementation tried this and
  broke determinism (cross-stream race between main-stream
  raw_memset_d8_zero + pnl_track_step write and diag-stream
  raw_memcpy_dtod_async read). Direct same-stream read avoids the race
  entirely and stays mega-graph compatible (the readback runs AFTER
  per-step pipeline finishes, outside any captured CUDA graph).

  Cumulative tracked Rust-side in alpha_rl_train.rs; reset at train->eval
  boundary so realized_pnl_usd_cum tracks the eval window only.

  scripts/tier1_5_verdict.py upgrade:
  - signal_reward_alignment reads trading.realized_pnl_usd_cum
  - signal_eval_pnl falls back to realized_pnl_usd_cum
  - signal_consistency upgraded from WARN-at-5% to KILL-at-$50 with a new
    consistency_kill_usd threshold key; cross-source disagreement is now
    a hard kill not a warning

  tests/eval_diag_emission.rs: bumped EXPECTED_LEAVES 711 -> 712 (-1 deleted,
  -0 renamed, +2 added); added invariant that cum == running_sum(delta) and
  regression check that legacy pnl_cum_usd / realized_pnl_cum_usd fields are
  absent from the schema; allowed n_eval_steps + 1 row count for the
  drain-row pattern.

Falsification gate (load-bearing, must hold every smoke from this commit
forward): `diag.last_eval_row.trading.realized_pnl_usd_cum` matches
`eval_summary.total_pnl_usd` within $50.

Validation (mid-smoke b=128, 2000 train + 500 eval, seed=42, RTX 3050):
  - cargo build --release --example alpha_rl_train -p ml-alpha: exit 0
  - cargo test -p ml-alpha --test eval_diag_emission: PASS (712 leaves)
  - local-mid-smoke.sh: exit 0, drain row at step 2500
  - cross-source consistency: eval_summary=-$4,457,625.00 vs
    diag.realized_pnl_usd_cum=-$4,457,625.18 -> delta=$0.18 (exactly fp/100
    f32 rounding noise; well within $50 tolerance)
  - ./scripts/determinism-check.sh --quick: PASS — all checksums.* leaves
    match across all 200 rows (rel-tol 1e-5, abs-tol 1e-7)
  - Pre-commit hook on staged diff: PASS

Memory pearls created in this session document the diagnostic chain:
  - pearl_diag_pnl_fields_are_shaped_reward_not_usd (the mislabeling root)
  - pearl_reward_signal_anti_aligned_with_pnl ADDENDUM 2026-06-02b
    (correction to the 2026-06-02 ADDENDUM — the "+0.93 Pearson at HEAD"
    finding was a measurement artifact because both sides were in shaped
    pts x lots units, not USD)
  - pearl_advantage_kernel_is_done_gated_td_not_gae (related signal-density
    gap — Phase 1.5 follow-up)
  - pearl_phase5_term_4_is_almost_potential (Ng-Harada-Russell analysis of
    Phase 5 shaping terms — Phase 1 follow-up)

Unlocks: TRUE Pearson(rewards.sum, Δpnl_usd) can now be measured per-step
at HEAD. The eval-collapse investigation (next phases: Ng-Harada-Russell
shaping cleanup + GAE upgrade) is now falsifiable with bit-deterministic
verdicts grounded in authoritative USD pnl.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 19:55:31 +02:00
jgrusewski
629ebd667c feat(ml-alpha): deterministic same-seed training + Tier 1.5 fast-dev-cycle
Two same-seed runs now produce bit-equal eval_summary.json, alpha_rl_train_summary.json,
and diag.jsonl (modulo wall-clock elapsed_s). The 5-phase falsification chain landed:

  Phase 2   PER tree-rebuild: __threadfence is NOT a grid-wide barrier; multiple blocks
            raced across sum-tree levels. Fix: Grid=(1) Block=(1024) + __syncthreads
            in rl_per_tree_rebuild.cu.

  Phase 2.3 cuBLAS GEMM_DFALT + TF32 default-math allowed split-K non-deterministic
            accumulation at 3 sites. New crates/ml-alpha/src/cublas_determinism.rs
            applies CUBLAS_PEDANTIC_MATH via FOXHUNT_DETERMINISTIC env toggle
            (0=TF32 prod, 1=PEDANTIC dev default, 2=DEFAULT_MATH control).

  Phase 2.6 Two bugs surfaced sequentially in the backward kernel chain:
            (1) rl_iqn_tau_cos_features had a multi-block r/w race on prng_state[batch]
                — all N_TAU=32 blocks read seed; only tau_idx==0 wrote back; no
                inter-block barrier. Fix: split into READ-ONLY rl_iqn_tau_cos_features
                + new sibling rl_iqn_advance_prng_state launched on same stream
                (kernel-launch ordering = grid-wide barrier).
            (2) OutcomeHead::new called near_zero_xavier without scoped_init_seed,
                falling back to time+thread-id RNG. Stayed dormant until first done
                event activated non-sentinel labels and divergent weights flowed via
                grad_h_t_outcome into encoder gradient. Fix: add seed param + install
                scoped_init_seed(dqn_seed.wrapping_add(0x0CE0)) guard.

Validation (./scripts/determinism-check.sh --quick, RTX 3050, b=128, 200+50 steps):
  - All 200 rows of checksums.* leaves match (rel-tol 1e-5, abs-tol 1e-7)
  - eval_summary.json, alpha_rl_train_summary.json byte-equal between runs
  - diag.jsonl byte-equal modulo elapsed_s
  - Eval pnl identical run-A vs run-B at seed 42

Pre-fix baseline (Phase 2.5 measurement): same-seed eval pnl spread $450k
($187k vs -$261k). Post-fix: $0 spread.

Speed cost: ~1.5ms/step amortised; ~10-15% slower than TF32 production
(PEDANTIC tax — acceptable in dev, toggle to FOXHUNT_DETERMINISTIC=0 for prod).

Mapped-pinned discipline: all 11 NEW memcpy_dtoh sites in diagnostic dump methods
+ per-step checksum readback use a new pub(crate) helper
read_slice_d_into<T: Copy>(stream, src, dst) — MappedRecordBuffer + raw
memcpy_dtod_async + raw_stream_sync + volatile read. Generic over T (f32, f64,
i32, u32, u8). Satisfies feedback_no_htod_htoh_only_mapped_pinned + hook guard.

Bundled Tier 1.5 fast-dev-cycle infrastructure (spec
docs/superpowers/specs/2026-06-02-fast-dev-cycle.md):
  - scripts/local-mid-smoke.sh        b=128, 2000+500, ~10min on RTX 3050
  - scripts/determinism-check.sh      runs mid-smoke twice, diffs checksums
  - scripts/tier1_5_verdict.py        behavioral kill verdict
  - AdamW checkpoint save/load (crates/ml-alpha/src/trainer/optim.rs)
  - IntegratedTrainer checkpoint save/load (resume from checkpoint)
  - 15 Phase 1 checksum leaves in build_diag_value
  - Env-gated dump methods (FOXHUNT_DETERMINISM_DEBUG_PER/MAMBA2/RL/BACKWARD)
    for future divergence-chasing — never run in production

Documentation:
  - docs/superpowers/specs/2026-06-02-determinism-foundation.md
  - docs/superpowers/specs/2026-06-02-fast-dev-cycle.md
  - docs/superpowers/plans/2026-06-02-determinism-foundation-implementation.md
  - docs/superpowers/notes/2026-06-02-determinism-phase{1,2,2.2,2.5,2.6}-*.md
  - Adjacent specs/plans/notes from the analytical chain that surfaced determinism
    as the load-bearing blocker (eval-summary, eval-boundary, regime-observer,
    multi-head policy, regime-invariance, Phase 3 IQN-complement post-mortem)

Unlocks: every controller / architecture / reward-shaping A/B from this commit
onward attributes outcome differences to the change, not random-init kernel-race
drift cascading through training x eval LOB-sim trajectories. The eval-collapse
investigation (pearl_reward_signal_anti_aligned_with_pnl, multi-head spec,
regime-invariance spec) is now testable with trustworthy verdicts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:56:00 +02:00
jgrusewski
823a15f282 feat(scripts): alpha-rl-run.sh production wrapper + session memory updates
One-command production run: push, submit, monitor, download, analyze.
Memory: cudarc overhead pearl, nsys workflow reference.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:39:53 +02:00
jgrusewski
ef23dfb3d9 refactor(rl): delete host-side PER + diag-every band-aid (greenfield prep)
Remove: ReplayBuffer, Transition, NStepEntry, push_to_replay,
sample_and_gather, n_step_buffer, replay.rs, --diag-every flag.
PER call sites stubbed with todo!() — replaced by GPU PER in next commits.

Also fixes pre-commit hook to allow todo!() macro (per CLAUDE.md:
"todo!() macro is OK for runtime stubs") while still blocking
TODO/FIXME comment markers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 00:18:11 +02:00
jgrusewski
45ab5dd066 fix(rl): remove CudaSlice::clone() from graph capture regions
CudaSlice::clone() allocates device memory, which is forbidden during
CUDA stream capture. Inline the launch_l2_norm and
launch_ema_update_per_step calls to avoid the &mut self borrow conflict
that forced the .clone() workaround.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 23:24:47 +02:00
jgrusewski
2a97578163 fix(argo): add alpha-rl to argo-train.sh model validation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:26:19 +02:00
jgrusewski
0056be88ec fix(argo): skip fxcache for alpha-rl (reads MBP-10 directly)
alpha_rl_train reads MBP-10 data with its own predecoded cache —
it never uses the fxcache pipeline. Skip the 15-minute feature
extraction step entirely for model=alpha-rl by depending only on
ensure-binary + gpu-warmup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:25:48 +02:00
jgrusewski
cc4c47f471 audit(rust-consts): catch literal-vs-const drift + cleanup BOOK_LEVELS=10
Audit script (audit-rust-consts.sh) scans Rust src/examples for numeric
literals mirroring structural kernel-side consts (N_ACTIONS, Q_N_ATOMS,
HIDDEN_DIM, MAX_UNITS, BOOK_LEVELS). Closes the layer-3 gap noted in
feedback_use_consts_not_literals_for_structural_dims:

  Layer 1: kernel `#define` allowlist  → audit-isv
  Layer 2: Rust `pub const` canonical  → exists (e.g. N_ACTIONS in rl/common.rs)
  Layer 3: Rust literals mirroring (2) → audit-rust-consts (this commit)

Honors `// audit-ignore: <SYMBOL>` per-line markers and skips `[u8; N]`
byte-buffer patterns (high false-positive class — almost always I/O
scratch, not structural dims).

Cleanup driven by first run (19 real flags, no grandfathering):
* New canonical: `BOOK_LEVELS` in `ml-alpha/src/cfc/snap_features.rs`
  (10 book levels = same place as `Mbp10RawInput` struct)
* `ml-backtesting/src/lob/mod.rs`: redefine as `pub use` re-export from
  ml-alpha (single source of truth; ml-backtesting depends on ml-alpha
  via `Mbp10RawInput` already)
* 19 sites switched literal `10` → `BOOK_LEVELS`:
  - snap_features.rs:44-47 (struct fields)
  - data/loader.rs:872-876, 960 (Mbp10Snapshot → Mbp10RawInput convert)
  - data/aggregation.rs:161 (level-wise aggregation loop)
  - trainer/perception.rs:2750-2756, 6272-6278, 6686-6690, 7247-7253
    (snapshot → batch staging loops)
  - tests/lob_sim_fuzz.rs:21, lob_sim_integrated_fuzz.rs:22 (duplicate
    const → use ml_backtesting::lob::BOOK_LEVELS)
* 5 sites marked `// audit-ignore: BOOK_LEVELS — <reason>`:
  - harness.rs:572,574,594 (conviction-bucket histograms, 10 ≠ depth)
  - multi_horizon_labels.rs:489,557,564 (10-element test price vecs)

Re-run after fixes: 0 suspect literals flagged. PASS.
2026-05-24 17:39:40 +02:00
jgrusewski
d3175711b9 feat(rl): SP20 P4 — N_ACTIONS 9→11 with HalfFlat actions
Action enum extended:
  a9  = HalfFlatLong   (close ⌈|pos|/2⌉ of long position, no-op if not long)
  a10 = HalfFlatShort  (close ⌈|pos|/2⌉ of short position, no-op if not short)

`actions_to_market_targets.cu` extended with a9/a10 handlers:
  HalfFlatLong (pos > 0):  side=1 sell, size=max(1, (position_lots+1)/2)
  HalfFlatShort (pos < 0): side=0 buy,  size=max(1, (|position_lots|+1)/2)

Round-up division ensures min 1 lot closes — single-lot positions
fully close on HalfFlat (the half rounds up to 1).

N_ACTIONS=9 → 11 propagated to all 10 .cu kernels:
  argmax_expected_q, bellman_target_projection, dqn_distributional_q,
  log_pi_at_action, ppo_clipped_surrogate, rl_action_kernel,
  rl_entropy_coef_controller, rl_pi_action_kernel, rl_q_pi_agree_b,
  rl_q_pi_distill_grad

Rust-side N_ACTIONS const bumped to 11 in src/rl/common.rs.

CLI alpha_rl_train.rs action_hist + windowed_act_hist refactored
to reference `N_ACTIONS` const instead of literal 11. Caught DURING
this commit's dogfood: an intermediate state had `[0u32; 11]` but
left `(0..9).contains(&a)` unchanged — HalfFlat samples silently
dropped (a9/a10 showed 0% in diag despite P_MIN=0.02 floor
guaranteeing 2% each). Fix uses N_ACTIONS const everywhere; new
pearl `feedback_use_consts_not_literals_for_structural_dims`
codifies the meta-pattern (Rust code mirroring kernel structural
dims must reference the const, NEVER duplicate the literal —
audit-isv only scans .cu files, this class of bug is currently
unaudited in .rs).

Audits PASS:
  audit-isv: all kernel #defines allowlisted (BOOK_LEVELS,
             ACTION_*, structural dims)
  audit-wiring: all 4 actions in manifest (TrailTighten, TrailLoosen,
                HalfFlatLong, HalfFlatShort) have consumers

Local 1k-step smoke (RTX 3050 Ti, 13.5s):
  * Exit 0, 0 NaN/inf, 16000/16000 samples accounted for
  * Action distribution: all 11 used in 7-11% range
  * HalfFL=7.99%, HalfFS=7.51% — π samples them under multinomial
  * Trail=19.34% — agent continues to value trail-stop actions

Trail-stop check (rl_trail_stop_check.cu) currently still routes
force-close through a3/a4 (FlatFromLong/Short) rather than per-unit
partial-flat via a9/a10. That routing upgrade is SP20 P5b follow-up
work — it requires the per-unit close_unit_index buffer wiring per
spec §3 P5. Adding a9/a10 to the action space is the foundational
prerequisite; consumer kernel uses come with P5b.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 17:17:33 +02:00
jgrusewski
20c835713b fix(rl): wire TrailTighten/TrailLoosen + SP20 P1+P5 foundation
scripts/audit-wiring.sh dogfood pass flagged a7 (TrailTighten) and
a8 (TrailLoosen) as actions with no consumer anywhere in the
codebase (canonical pearl_dead_trail_stop_actions_a7_a8). Fix
bundles SP20 P1 (per-unit trade state buffers) and P5 (trail-stop
kernels) since they're the same architectural work.

Three new kernels:

  rl_unit_state_update.cu   — per-batch trade state machine. Runs
                              AFTER fill+extract_realized_pnl_delta.
                              Detects open/close/reverse position
                              transitions and populates unit slot 0
                              with entry_price, entry_step, lots,
                              initial_r, trail_distance. Slots 1-3
                              allocated for SP20 P7 pyramid expansion
                              but unused this commit.

  rl_trail_mutate.cu        — handles a7/a8 actions. Mutates ALL
                              active units' trail_distance bounded
                              by ISV [MIN, MAX] with symmetric
                              reciprocal adjust rate per SP20 §4.12:
                                a7: trail = max(MIN, trail × rate)
                                a8: trail = min(MAX, trail / rate)

  rl_trail_stop_check.cu    — per-batch per-unit breach check. Reads
                              shared lobsim best book (bid/ask),
                              computes mid, compares to each active
                              unit's (entry ± trail). On breach,
                              OVERRIDE actions[b] to FlatFromLong
                              (a3) or FlatFromShort (a4). Force-close
                              routes through existing flat plumbing
                              per pearl_stop_checks_run_at_deadline_cadence.

                              SP20 v3 §3 P5 calls for routing close
                              via partial-flat (a9/a10) so only the
                              at-risk unit closes — that needs P4
                              (N_ACTIONS=11). For now, ANY unit's
                              breach closes ENTIRE position via full
                              FlatFromLong/Short.

Per-batch per-unit buffers (8 new in trainer):
  unit_entry_price_d   [B × 4]  f32
  unit_entry_step_d    [B × 4]  i32
  unit_lots_d          [B × 4]  i32
  unit_initial_r_d     [B × 4]  f32
  unit_trail_distance_d[B × 4]  f32
  unit_active_d        [B × 4]  u8
  pyramid_units_count_d[B]      i32
  unit_prev_pos_lots_d [B]      i32  (state-machine tracker, separate
                                      from extract_realized_pnl_delta's
                                      prev_position_lots_d for clean
                                      kernel composability)

4 new ISV slots (494-497):
  RL_TRAIL_MIN_INDEX           — trail distance floor (seed 0.001)
  RL_TRAIL_MAX_INDEX           — trail distance ceiling (seed 100.0)
  RL_TRAIL_K_INIT_INDEX        — initial trail multiplier (seed 2.0, Turtle 2N)
  RL_TRAIL_ADJUST_RATE_INDEX   — tighten ratio (seed 0.9; symmetric reciprocal for loosen)

RL_SLOTS_END: 494 → 498.

LobSim exposes bid_px_d() + ask_px_d() public accessors. RlLobBackend
trait extended with the two accessors; the LobSimCuda impl wires
through.

Override stack ordering per SP20 §2.3:
  1. rl_pi_action_kernel       (sample)
  2. rl_trail_mutate           (a7/a8 → mutate, before stop check)
  3. rl_trail_stop_check       (per-unit breach → override action)
  4. actions_to_market_targets (execute, including overridden flat)
  5. step_fill_from_market_targets
  6. extract_realized_pnl_delta
  7. rl_unit_state_update      (detect post-fill transitions)

Audit infrastructure refined as part of dogfooding:
  * audit-isv allowlist extended for BOOK_LEVELS (structural book
    depth) and ACTION_* prefix (enum-mirror constants — these are
    structural API contracts matching src/rl/common.rs::Action positions)
  * audit-wiring action-handler regex now matches BOTH literal
    `action == <idx>` and `action == ACTION_<UPPER_SNAKE>` patterns,
    and treats != as a handler too (a guard against the action is
    valid wiring)

Both `audit-isv.sh` and `audit-wiring.sh` PASS cleanly with the
full manifest. audit-diag scheduled for first SP20 phase that adds
diag fields (this commit deliberately keeps diag exposure minimal
— full per-unit + trail diag blocks come with SP20 P13).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 16:47:53 +02:00
jgrusewski
40855bfd62 docs(sp20): trader-grade trade management spec + audit infrastructure
Adds SP20 — full production trader-management system in one
greenfield commit (3-4 weeks of implementation work to follow):

  * Tier 0: multi-resolution time-scaled market features (3 horizons)
  * Tier 1: trade-arc awareness (4 features per batch)
  * Tier 2: per-unit trail-stop (entry + trail + stop per unit)
  * Tier 3: pyramiding + partial profit-taking (HalfFlat actions,
            N_ACTIONS=9→11)
  * Tier 4: Forward-Return-Distribution head + confidence gate +
            per-batch anti-martingale sizing + position heat cap +
            vol-adjusted defaults

Spec went through critical-review pass (v1→v2→v3):
  * v1: 3 tiers, side-channel features, single-gate acceptance
  * v2: 5 tiers added partial-flat + anti-mart + multi-res + checklist
  * v3: foundational fixes for 4 CRIT + 6 SIG + 6 MIN findings
        (per-unit pyramid state, encoder-input injection vs side-channel,
         FRD head replaces survivor-biased checklist, override stack
         ordering, per-batch anti-mart, real-time multi-res scales,
         P-1 ceiling falsification gate, multi-tier acceptance)

§0 Foundational Principles (NEW, non-negotiable):
  * §0.1 every numerical constant ISV-resident (no hardcoded #defines
         in new kernels; structural-dim exception only)
  * §0.2 every kernel/slot/head/action fully wired in same commit
  * §0.3 diagnostics baked in at birth (every observable in JSONL)
  * §0.4 per-phase ship-gate: all three audits must pass

Audit infrastructure shipped with the spec:
  * scripts/audit-isv.sh      — greps new .cu for hardcoded #defines
  * scripts/audit-wiring.sh   — verifies kernels/slots/heads/actions
                                have producer + consumer in code
  * scripts/audit-diag.sh     — runs local 100-step smoke, validates
                                manifest-listed jq paths present in JSONL
  * scripts/audit-manifest/   — per-phase append manifests (kernels,
                                slots, heads, actions, diag-fields)

Naming discipline: audit scripts and manifest are SP-agnostic (no
`sp20-` prefix) per new pearl `feedback_no_sp_or_version_prefixes_in_file_names`
— they'll serve future SPs too. SP numbers belong only in
docs/superpowers/{specs,plans}/ filenames.

Audit scripts dogfooded — already caught two real violations on
existing code that the formal review missed:
  * audit-isv: KL_EMA_ALPHA=0.05f hardcoded in rl_q_pi_distill_grad.cu
  * audit-wiring: TrailTighten action (a7) has no handler in any
                  kernel (per pearl_dead_trail_stop_actions_a7_a8)

These violations are SP20 P5/P10 fix targets.

User decision recorded in spec §3 P-1: ceiling-falsification phase
intentionally skipped — SP20 is the architectural launchpad for
the broader trader system regardless of whether current arch could
be pushed further at 1M steps. P-1 may be revisited as standalone
work after SP20 ships.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 16:28:39 +02:00
jgrusewski
9c6c280bd8 fix(rl): anti-collapse probability floor + argo b_size=16 default
Two fixes for alpha-rl-9k9x6 (commit 3737feb66) findings:

(1) rl_pi_action_kernel.cu — per-action probability floor P_MIN=0.02
    with renorm after softmax. Structural guarantee H(π_sample)
    bounded below regardless of logits. 9k9x6 evidence:
      - π collapsed to action 1 (100% sample rate) by step 2500
      - action_entropy EMA → 0.001 by step 10000 (uniform=2.197)
      - entropy_coef controller pegged at COEF_MAX without effect
        because PPO update couldn't escape δ-function fixed point
      - 0 trades closed in 50000 steps; position monotonically
        accumulated to -45871 lots
    With P_MIN=0.02, worst-case H(π_sample) ≈ 0.77 nats — well
    above the observed collapse and well below uniform.
    Per `pearl_blend_formulas_must_have_permanent_floor`.

(2) argo template + script — N_BACKTESTS default 1 → 16. The CLI
    default lift in commit 3737feb66 was overridden by both the
    argo script and the wftmpl `value: "1"`. 9k9x6 actually ran
    b_size=1 despite being framed as the b_size=16 test.
    Per `pearl_b_size_1_signal_starvation_blocks_q_learning`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 11:26:37 +02:00
jgrusewski
87a22d12c9 feat(rl): walk-forward G8 eval phase + fold split (MVP, manual fan-out)
Adds the minimum-viable implementation of the R9 multi-fold G8 gate
per `pearl_single_window_oos_is_not_oos` ("a single window is NOT
out-of-sample"). The trainer can now:

  1. Slice the MBP-10 file list into K equal-sized blocks
     (`--n-folds K --fold-idx k`).
  2. Train on blocks [0..=k] (passed to MultiHorizonLoader).
  3. Run a separate eval phase of `--n-eval-steps` on block [k+1]
     using a second loader instance.
  4. Drain LobSim trade records gated by a pre-eval head checkpoint
     so train-phase trades don't contaminate the eval summary.
  5. Compute profit_factor + sharpe + drawdown via existing
     `ml_backtesting::artifacts::compute_summary`.
  6. Write `eval_summary.json` alongside `alpha_rl_train_summary.json`.

## Manual fan-out (this MVP)

The dispatcher (`scripts/argo-alpha-rl.sh`) gains three new flags
that thread through the Argo template into the CLI: `--fold-idx`,
`--n-folds`, `--n-eval-steps`. To run a 3-fold G8:

  ./scripts/argo-alpha-rl.sh --n-folds 3 --fold-idx 0 --n-eval-steps 200
  ./scripts/argo-alpha-rl.sh --n-folds 3 --fold-idx 1 --n-eval-steps 200

(With n_folds=3 the valid fold indices are 0 and 1 — the third block
is the eval window for fold 1. n_folds=K accepts fold_idx ∈ [0, K-2].)

Each submission produces one `eval_summary.json` at the resolved
output dir; the per-fold profit_factor is the value to aggregate.
Manual aggregation for now — automated DAG matrix fan-out + an
in-cluster aggregator pod is a follow-up commit. The aggregator
will mean ± SD the per-fold PFs and gate on `PF > 1.0`.

## What's NOT pure eval

The eval loop calls `step_with_lobsim` (same as train) — Adam steps,
PER updates, controller adaptations all still fire during eval. At
b_size=1 the per-step learning effect is small relative to the
train-phase-accumulated policy, so the eval PF approximates the
OOS performance of the train-end policy. A clean pure-eval mode
(forward + LobSim step only, no backward/Adam/PER) is a follow-up
architectural change; documented inline at the eval phase block.

## Default behaviour unchanged

`--n-folds=1` (default) skips the eval split entirely and uses all
files for training — identical to the prior single-window smoke.
The R9 prior smokes ran in this mode. Default `--fold-idx=0` and
`--n-eval-steps=0` keep prior smoke runs binary-compatible.

## Template + dispatcher changes

  * `alpha-rl-template.yaml`: adds 3 new workflow parameters
    (`fold-idx`, `n-folds`, `n-eval-steps`) and threads them into
    the train container's `alpha_rl_train` invocation.
  * `argo-alpha-rl.sh`: adds matching CLI flags with explicit
    documentation of the multi-fold dispatching pattern.

## Verified gates

Local sm_86 build + dispatcher syntax clean. Tests unchanged
(the walk-forward path is exercised by cluster smokes, not unit
tests — the loader-slicing logic is straightforward index math).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 17:14:11 +02:00
jgrusewski
ce1e13519b fix(rl): mapped-pinned for all R7d/R8 CPU↔GPU paths + diag JSONL + guard
Two concerns in one commit since they're entangled:

## 1. feedback_no_htod_htoh_only_mapped_pinned violations

R7d (PER push/sample) + R8 (CLI binary) + the new per-step diag dump
shipped with 8 raw `stream.memcpy_htod` / `stream.memcpy_dtoh` calls.
The rule is explicit: "mapped-pinned only for CPU↔GPU; tests not
exempt." A raw `stream.memcpy_*` on a regular `&[T]` / `&mut [T]` is
NOT mapped-pinned — the source/dest slice isn't page-locked, so the
CUDA driver does an internal blocking HtoD/DtoH that stalls the
stream.

Refactored all 8 violations to use the mapped-pinned + DtoD pattern
(cuMemHostAlloc DEVICEMAP — host writes via `host_ptr`, kernel reads
`dev_ptr`, DtoD between them via `cudarc::driver::result::memcpy_dtod_async`).
New shared helpers in `trainer/integrated.rs`:

  * `read_slice_i32_d` — DtoH for `i32` device buffers via
    `MappedI32Buffer` staging. Counterpart to the existing
    `read_slice_d` (f32 version).
  * `write_slice_f32_d` — CPU→GPU upload for `f32` via
    `MappedF32Buffer.write_from_slice` + DtoD into destination.
  * `write_slice_i32_d` — CPU→GPU upload for `i32` via
    `MappedI32Buffer.host_slice_mut().copy_from_slice` + DtoD.

`pub fn` wrappers (`read_slice_*_d_pub`) expose the f32/i32 helpers
to the CLI binary so the per-step diag DtoH uses the same canonical
pattern.

Call-site refactors:

  * `push_to_replay`: 4× `stream.memcpy_dtoh` → `read_slice_*_d`.
  * `sample_and_gather`: 3× `stream.memcpy_htod` → `write_slice_*_d`.
  * `step_with_lobsim` pre-PER ISV refresh: raw `memcpy_dtoh` →
    `read_slice_d` (424 floats per step).
  * `step_with_lobsim` post-Q PER priority TD readback: raw
    `memcpy_dtoh` → `read_slice_d` (b_size floats per step).
  * `step_synthetic` ISV mirror refresh: raw `memcpy_dtoh` →
    `read_slice_d` (pre-existing pre-R9 violation; fixed in the
    same commit since it's the same pattern in the same file).
  * Init-time (one-shot) `prng_state` upload: raw `memcpy_htod` →
    inline mapped-pinned DtoD (custom because cast through i32 for
    the u32 buffer).
  * Init-time (one-shot) `atom_supports` upload: raw `memcpy_htod`
    → `write_slice_f32_d`.
  * `examples/alpha_rl_train.rs` per-step diag DtoH (3 calls) →
    `read_slice_*_d_pub`.

## 2. Pre-commit guard gap — diff-aware HtoD/DtoH check

The existing GPU hot-path guard (`scripts/gpu-hotpath-guard.sh`)
EXPLICITLY skips memcpy_htod/dtoh on the assumption that such calls
only appear in `cuda_pipeline/` (where mapped-pinned is the
convention). That assumption was falsified by R7d/R8 — the guard
shipped 8 violations green.

Added `check_no_raw_htod_dtoh` to `scripts/pre-commit-hook.sh` (the
real file behind the `.git/hooks/pre-commit` symlink). The check is
DIFF-AWARE: it greps only the `+` lines of `git diff --cached -U0`,
so pre-existing violations elsewhere (143 sites across the codebase)
don't block commits touching unrelated files. NEW additions of
`\.memcpy_(htod|dtoh)\(` are flagged with a clear error pointing at
the mapped-pinned alternative. Suppress per-line with `// gpu-ok:
<reason>` (same convention as the existing guards).

Pre-existing violations in `ml-alpha/src/aux_heads.rs`,
`mamba2_block.rs`, `cfc/`, `data/`, etc. are a separate cleanup —
not blocked by this commit's check because the diff-aware filter
ignores anything that was already on `HEAD~1`.

## Verified gates (post-fix, local sm_86)

  G1  isv_bootstrap                 unchanged
  G3  controllers_emit              unchanged
  G4  target_soft_update            unchanged
  G6  r7d_per_wiring                unchanged (PER round-trips all
                                     mapped-pinned now)
  R3  ema/advantage (3 tests)       unchanged
  R4  action kernels (3 tests)      unchanged
  end integrated_trainer_smoke      unchanged

Mapped-pinned is semantically equivalent to raw memcpy_htod/dtoh —
just routed through page-locked staging so the driver doesn't have
to do its own internal pinning. Behaviour identical; the cost shifts
from "driver hidden HtoD per call" to "mapped-pinned alloc + DtoD
per call." For the smoke (b_size=1, 1000 steps), the cost difference
is in the microseconds.

## Per-step diag JSONL (separate concern, same commit)

Added `--diag-jsonl <PATH>` flag to `alpha_rl_train.rs` (default:
`<out>/diag.jsonl`). After each `step_with_lobsim`, writes one JSON
record capturing:

  * step number, elapsed wall time
  * all 5 head losses + λs
  * all 7 RL controller outputs (γ τ ε coef n_roll per_α scale)
  * all 5 per-head learning rates (lr_bce/q/pi/v/aux)
  * all 7 EMA inputs the controllers consume
  * replay buffer length
  * per-step reward stats (sum, max, min, abs_max)
  * per-step done count
  * per-step action histogram (9 action classes)

Critical for cluster smoke debugging — the prior CLI only flushed
an `eprintln` progress line every N steps (default 100), making
in-flight controller drift / replay stagnation / reward explosion
invisible until they produced a NaN abort. The JSONL is line-
buffered + flushed every `log_every` steps so `tail -f` shows
progress live.

The stderr progress line is also beefed up to include γ / ε / per_α /
reward_scale / dones / rew_sum at each tick so a casual `argo logs`
inspection sees the controller behaviour without parsing JSONL.

## Why R9 cluster submission needs this

Without the diag dump, an R9 1000-step smoke is "blind" — only the
final summary tells us what happened. With the dump, post-hoc
analysis can answer:
  * Did the controllers adapt or stay at bootstrap?
  * Did the reward scale stabilise or saturate?
  * Did the PER buffer fill?
  * Was the action histogram dominated by any one action?
  * Where did the per-head losses converge to?

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 14:14:55 +02:00
jgrusewski
1168f3ea83 feat(rl): R8 — alpha_rl_train CLI + Argo template + dispatcher
Closes the rebuild plan's R8 scope: production runner shape for the
integrated RL trainer. Three artifacts wired end-to-end:

  1. `crates/ml-alpha/examples/alpha_rl_train.rs` — clap CLI driving
     `IntegratedTrainer::step_with_lobsim` against MBP-10 windows
     loaded via `MultiHorizonLoader::next_sequence_pair` (R2) for
     true `(s_t, s_{t+1})` adjacency. Per `feedback_mbp10_mandatory`,
     `--mbp10-data-dir` is required — no synthetic-data fallback in
     the production path.
  2. `infra/k8s/argo/alpha-rl-template.yaml` — WorkflowTemplate
     mirroring alpha-perception's DAG (check-cache → ensure-binary →
     train; warmup-gpu parallel). Binary cache slot is
     `/data/bin/<sha>/alpha_rl_train` (distinct from `alpha_train`
     so the two binaries coexist at the same SHA).
  3. `scripts/argo-alpha-rl.sh` — dispatcher with three rebuild-plan
     guards baked in.

## Dispatcher guards (per the rebuild plan's feedback list)

`feedback_default_to_l40s_pool` (2026-05-09): default `--gpu-pool`
is `ci-training-l40s` (sm_89). H100 (sm_90) is opt-in for production
scale-up only. Cubins must match the device, so the dispatcher
derives `cuda-compute-cap` from the pool name and threads it into
the workflow params.

`feedback_argo_template_must_apply` (2026-05-21 canonical incident):
`argo submit --from=wftmpl/<name>` reads the cluster CRD, NOT the
on-disk YAML; unknown `-p` parameters silently no-op without a prior
`kubectl apply`. Dispatcher applies the local template BEFORE every
submission (overrideable via `--skip-template-apply` for the rare
case where you've already applied manually).

`feedback_push_before_deploy` (2026-05-20 canonical incident): the
in-cluster `ensure-binary` pod fetches source from `origin/<branch>`,
NOT the local working tree. Submitting before `git push` deploys the
last-pushed SHA, which can lag local diff by N commits. Dispatcher
verifies `git rev-parse HEAD == git rev-parse origin/<branch>` and
hard-errors with the explicit push command otherwise. Bypass via
`--skip-push-check` (only when intentionally deploying a previously-
pushed SHA via `--sha`).

## CLI: gate G8 (NaN abort)

Per `feedback_stop_on_anomaly` + `feedback_kill_runs_on_anomaly_quickly`,
the CLI checks every per-head loss (l_bce / l_q / l_pi / l_v / l_aux
/ l_total) for finiteness after each `step_with_lobsim` call.
Non-finite at any step → write summary with `nan_abort_step` set →
`process::exit(2)`. R9's cluster smoke tail-watcher kills the
workflow on the non-zero exit code, satisfying gate G8 from the
rebuild plan.

## CLI: knobs that ARE on the CLI

Structural / boundary parameters only (per
`pearl_controller_anchors_isv_driven`: every adaptive knob lives in
ISV, not CLI flags):
  * `--mbp10-data-dir / --predecoded-dir / --out` — I/O paths.
  * `--n-steps` — wall-budget control (1000 R9 smoke / 50k+ prod).
  * `--seq-len / --n-backtests / --per-capacity` — structural
    sizing. seq_len threads into the loader's multi-resolution
    `1:<seq_len>` config; n_backtests into both LobSimCuda and
    PerceptionTrainerConfig.n_batch.
  * `--seed` — reproducibility per
    `pearl_scoped_init_seed_for_reproducibility` (forks deterministic
    sub-seeds for dqn / ppo / per).
  * `--instrument-mode` — MBP-10 filter (all / front-month / id=N).
  * `--gpu-idx` — CUDA device selection.

What's NOT on the CLI: γ / τ / ε / entropy_coef / per_α / reward_scale
/ per-head LRs — all live in ISV[400..417] and are driven by R5's
controllers from EMA-tracked diagnostics. Per the rebuild plan
A1: "every adaptive bound is signal-driven, not tuned."

## Cluster smoke entry point

```bash
# R9 validation smoke (after pre-cluster local CUDA tests green).
./scripts/argo-alpha-rl.sh --n-steps 1000 --instrument-mode front-month

# Production scale-up (gated by R9's multi-fold pass).
./scripts/argo-alpha-rl.sh --n-steps 50000 --n-backtests 32 \
  --per-capacity 100000  # GPU sum-tree R-future when capacity > 4096
```

## What's NOT in this commit

The R9 cluster smoke run itself is out of band — this commit ships
the entry points. R9 will execute the pre-cluster validation
checklist + first 1000-step smoke + multi-fold walk-forward G8 gate
per the rebuild plan §"Cluster smoke discipline".

The summary JSON's schema is intentionally narrow (final-step losses
+ replay len + completion state + NaN abort marker). R-future may
add per-epoch breakdowns + per-ISV-slot snapshots once the cluster
smoke tells us which diagnostics are actually load-bearing for kill
decisions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 13:30:10 +02:00
jgrusewski
f68e0a1d0d revert(loader): multi-resolution default '1:32' (single-scale) after htpp6 falsification
The Phase 1 multi-resolution layout (10 raw + 10 agg@30 + 12 agg@100)
regressed ALL horizons in alpha-perception-htpp6 (2026-05-22):
- auc_h100:  0.681 -> 0.512  (-0.169)
- auc_h300:  0.617 -> 0.506  (-0.111)
- auc_h1000: 0.576 -> 0.526  (-0.050)

Hypothesis falsified. Root cause: Mamba2+CfC SSM encoder already used
all 32 raw ticks effectively via state-recurrence; replacing 22 raw
ticks with arithmetic-mean aggregates destroyed within-window
microstructure variance (the actual h100 signal) AND broke temporal
continuity that the recurrence relies on. Δt Fourier encoder
couldn't compensate.

Architectural pearl: SSM/RNN/CfC + multi-resolution input is
incompatible without separate-encoder-per-scale or explicit scale
tokens. Transformer-style positional encoding tolerates scale-mixing;
recurrent state updates assume consecutive positions.

Reverts default to '1:32'. Adds explicit single_scale_32() constructor
for callers (harness, tests). Keeps default_three_scale() in code with
deprecation note for future sub-variant experiments. Production
defaults across alpha_train CLI, Argo template, dispatcher script,
ml-backtesting harness now match the proven baseline.
2026-05-22 21:21:21 +02:00
jgrusewski
b1bfae2367 feat(argo): replace --seq-len with --multi-resolution in alpha-perception
Default '1:10,30:10,100:12' (32 positions, 1510-tick context).
Greenfield migration — no legacy seq-len fallback. Operators
override per-run via './scripts/argo-alpha-perception.sh --multi-resolution 1:32'
for the parity-check config.
2026-05-22 20:45:21 +02:00
jgrusewski
78a9e08358 feat(loader): InstrumentFilter::FrontMonth for cross-quarter ES.FUT data
Replaces Option<u32> instrument_id_filter with InstrumentFilter enum {All,
Id(u32), FrontMonth}. FrontMonth runs a two-pass detect over the DBN
stream: pass 1 counts instrument_ids and collects SymbolMapping records,
picks the dominant id, validates it resolves to an ES contract via regex
ES[FGHJKMNQUVXZ]\d{1,2}; pass 2 streams the filtered records.

Motivated by alpha-perception-k54wd: a single-id filter on parent-symbol
ES.FUT data caught Q1 2024 (kept=73M) but kept=0 for Q2-Q9 because ES
front-month rolls quarterly (ESH4 -> ESM4 -> ESU4 -> ESZ4 ...). FrontMonth
self-tunes across the rolls without needing a per-file id table.

Sidecar keys distinguish modes: mbp10 / mbp10_instr<id> / mbp10_front_month.
CLI flag renamed --instrument-id -> --instrument-mode {all,id=N,front-month}
with matching parameter rename in argo-alpha-perception.sh + template.
2026-05-22 17:38:41 +02:00
jgrusewski
11c658d3fb feat(argo): plumb --instrument-id flag through alpha-perception script + template
Adds new workflow parameter instrument-id (default empty string = no filter).
Script forwards via --instrument-id CLI flag to alpha_train when non-empty.
EXTRA_FLAGS template logic appends --instrument-id only when set.

kubectl apply must run first (per feedback_argo_template_must_apply) so the
cluster CRD reflects the new parameter before argo submit.
2026-05-22 15:57:47 +02:00
jgrusewski
0d9fbc16b0 refactor(per-horizon): N_HORIZONS 5→3 — sweep configs + generator script
5 sweep YAMLs updated to reference the new checkpoint filename:
- config/ml/sweep_smoke.yaml: trunk_best_h6000.bin → trunk_best_h1000.bin
- config/ml/sweep_perhoriz_diag.yaml: same
- config/ml/sweep_threshold_tuning.yaml: same
- config/ml/sweep_deployability.yaml: same
- config/ml/sweep_smoke_perhoriz_cfc.yaml: same + 3 comment updates
  (WIN-gate criteria, header doc, best-checkpoint annotation)

scripts/generate_sweep_variants.py:61 updated atomically — without this
the next regeneration of sweep_deployability.yaml would silently
re-introduce trunk_best_h6000.bin.

Argo workflow templates (alpha-perception, alpha-cv, lob-backtest-sweep)
did NOT need text changes — they're already horizon-agnostic:
- They forward CLI flags via {{workflow.parameters.*}} to binaries
- Don't grep alpha_train_summary.json inline
- Don't reference per-horizon field names
- early-stop-metric default is "mean_auc" (horizon-agnostic)

Intentionally left:
- alpha-cv-template.yaml stacker-horizon: "6000" (unrelated TFT lookback)
- alpha-cv-template.yaml horizon: "1200" (DQN execution horizon in bars)
- lob-backtest-sweep-template.yaml ci-training-h100 (GPU pool name)

NOTE: kubectl apply of workflow templates is deferred to Task 10 (push
+ dispatch). Verified all 5 sweep YAMLs parse with python3 yaml.safe_load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:40:13 +02:00
jgrusewski
ba2850b448 feat(argo): run-sweep template + batched-mode in argo-lob-sweep.sh
Closes the operational gap from P6: when a sweep grid YAML carries
sim_variants, argo-lob-sweep.sh now emits ONE run-sweep-batched task
instead of N fan-out run-cell tasks. The new run-sweep template
invokes `fxt-backtest sweep` against the full grid (base64-encoded
inline as Argo parameter to avoid YAML special-char encoding traps).

The binary's P6 sweep() function handles cell × variant fan-out
internally via BatchedSimConfig::from_grid, so one pod processes all
4 windows × 140 variants sequentially. Trade-off: no inter-window
parallelism in this rev (4 quarters sequential in one pod ≈ firm-bound
2h wall per spec §9). 3-pod scale-out is P7 future work — needs the
script to split the YAML into per-window sub-grids and emit one
run-sweep task per sub-grid.

Backward-compat: legacy (no sim_variants) flow unchanged. The dry-run
of sweep_smoke.yaml continues to emit run-cell-* fan-out tasks; the
dry-run of sweep_deployability.yaml emits one run-sweep-batched task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 18:15:22 +02:00
jgrusewski
9d4fda36ab feat(ml-backtesting): batched-cell sweep schema + 140-variant runner (P6)
Sweep YAML now supports the batched flow per spec §3.3 + Task 6:
- SweepBase.sim_variants: Vec<SimVariant> — list of (cost, latency,
  threshold, ...) variants. When non-empty, each cell runs ONE harness
  at n_parallel=variants.len() with BatchedSimConfig::from_grid instead
  of the legacy one-harness-per-cell fan-out.
- SweepBase.data_template: Option<String> — when set with `{window}`
  placeholder, each cell's `window` field interpolates the per-cell
  data path. Replaces single scalar `data` for the windowed flow.
- SweepCell.window: Option<String> — window identifier (e.g., "2025-Q2").
- SimVariant: threshold + cost_per_lot_per_side required (the spec's
  primary axes); other fields optional overrides on top of SweepBase
  scalars.

New runner pieces:
- BatchedSimConfig::from_grid(&[ResolvedSimVariant]) in
  crates/ml-backtesting/src/sim/batched_config.rs.
- ResolvedSimVariant — per-variant fully-resolved sim params.
- resolve_sim_variants(&SweepBase) in main.rs — layers per-variant
  overrides over base scalars.
- run_batched_cell() in main.rs — builds the harness with
  sim_config_override + variant_names plumbed through. Writes per-
  backtest artifacts to sim_<variant_name>/ subdirs (spec §3.3).

Harness side:
- BacktestHarnessConfig gains variant_names + sim_config_override
  Option fields. When sim_config_override is Some, harness uses that
  directly instead of building from_uniform off scalar cfg. When
  variant_names is Some, write_artifacts uses sim_<name>/ instead of
  cell_NNNN/ subdirs. Both None preserve legacy single-cell behaviour
  (smoke, fixtures unchanged).

YAML configs:
- config/ml/sweep_threshold_tuning.yaml: 1 cell (W0) × 8 sim_variants
  (p60-p95 in 5pt steps) with cost=0.125 (1-tick anchor). Threshold
  pre-registration pass.
- config/ml/sweep_deployability.yaml: 4 cells (W1-W4) × 140 variants
  each (7 costs × 4 latencies × 5 thresholds). Generated by
  scripts/generate_sweep_variants.py — placeholder threshold values
  (p60-p95) until threshold-tuning publishes calibrated absolutes to
  config/ml/v2_prod_thresholds.json.

Deferred to P7 (operational glue):
- argo-lob-sweep.sh adaptation for the batched flow (cells = windows,
  not sim-variants; one Argo task per window invokes `fxt-backtest sweep`
  end-to-end inside the pod rather than `fxt-backtest run`).

Regression: all 7 existing CUDA tests pass through the new harness
construction path (sim_config_override = None → from_uniform fallback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:41:20 +02:00
jgrusewski
ed0d40f469 fix(scripts): argo-lob-sweep awk anchor + sweep_smoke YAML schema
argo-lob-sweep.sh's awk substitution for # __SWEEP_CELLS__ matched BOTH
the actual marker (line 110, whitespace-indented) AND the docstring
reference at the top of the template that mentions `# __SWEEP_CELLS__`
inside a sentence (line 8). Result: cell task was injected twice,
once before `apiVersion:` (breaking kubectl apply with 'invalid object'
validation error) and once at the correct DAG location.

Fix: anchor the match to lines that START with whitespace + the marker
(^[[:space:]]+# __SWEEP_CELLS__), so the docstring sentence (which has
'# The' first) no longer matches.

sweep_smoke.yaml: rewrite from the spec's idealised (axes:/windows:)
schema to the actual base:/cells: schema that fxt-backtest sweep +
argo-lob-sweep.sh parse. Hardcodes SHA 58b5ebbd3 (the production
training run); bounds smoke runtime via max_events=100000.

Verified: smoke sweep workflow lob-backtest-sweep-hxwmq submits cleanly
and starts running.
2026-05-19 11:24:20 +02:00
jgrusewski
62b1fc0965 infra(argo): lob-backtest-sweep workflow + argo-lob-sweep.sh (C19)
Cluster fan-out for the `fxt-backtest sweep` single-machine path.
Reads the same grid YAML format as the binary; runs each cell on a
dedicated GPU pod in parallel; aggregates at the end on a CPU pod.

infra/k8s/argo/lob-backtest-sweep-template.yaml:
  WorkflowTemplate `lob-backtest-sweep` with three job templates:
    ensure-binary  — cache-or-compile fxt-backtest by short-SHA into
                     /mnt/training-data/bin/<sha>/. Mirrors the
                     train-multi-seed-template.yaml ensure-binary
                     shape but for a single binary.
    run-cell       — single GPU pod (ci-training-l40s default per
                     feedback_default_to_l40s_pool.md). Receives
                     cell-name + every Run arg via inputs.parameters.
                     Writes artifacts to <sweep-root>/<sweep-tag>/<cell>/.
    aggregate      — CPU pod runs `fxt-backtest aggregate <sweep-dir>`
                     producing aggregate.parquet + pareto_frontier.json
                     at the sweep root.
  DAG marker `# __SWEEP_CELLS__` replaced at submission time with N
  WorkflowTask stanzas (one per cell), and the aggregate's
  `dependencies: [ensure-binary]` is rewritten to include every
  run-cell-* dep — so aggregate waits for ALL cells.

scripts/argo-lob-sweep.sh:
  Companion submission script following the argo-train.sh pattern.
  Parses the grid YAML via python3 + PyYAML (no `yq` dependency —
  yq isn't used elsewhere in foxhunt scripts; python3+PyYAML is
  universal in our CI images). Emits per-cell WorkflowTask stanzas
  + aggregate dependency list, awks them into the template, then
  `kubectl apply` + `argo submit`. Supports --dry-run for offline
  rendering and --watch for live log following.

Defaults match the spec / pearl set:
  - sm_89 / ci-training-l40s default (override via --gpu-pool
    ci-training-h100 for sm_90 + 80 GB)
  - data root /mnt/training-data/futures-baseline/ES.FUT
  - sweep results under /mnt/training-data/sweeps/lob-backtest/<tag>/
  - sweep-tag defaults to <basename of grid>-<short-sha>

Verified locally:
  - bash -n syntax-check passes
  - --help renders
  - --dry-run against the existing
    config/ml/sweep_decision_stride_example.yaml renders a valid
    workflow with 4 cells + correct aggregate dependency list

Live submission is operational work that needs cluster access to
verify; the rendered YAML follows the same conventions as the
existing argo-train.sh workflows that ship in this repo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:18:09 +02:00
jgrusewski
70d5fc29cf feat(ml-alpha): decision-stride loader + CLI + Mamba2 dt_s scaling (Phase 2A)
Decision-stride S lets a length-K sequence span ((K-1)*S + 1) raw
snapshots instead of K consecutive ones — expands the effective
time-window covered by each sequence at the same K-positions compute
cost. With K=64 and S=4, the window covers 256 ticks (~5s on ES MBP-10
at 20ms-tick) instead of 64 ticks (~1.3s).

Loader (crates/ml-alpha/src/data/loader.rs):
- `MultiHorizonLoaderConfig.decision_stride: usize` (default 1, must
  pre-existing call sites add the new field).
- `next_sequence` reads snapshot at `anchor + k * stride`; labels at the
  same indices (labels stay in absolute-snapshot horizons regardless of
  stride, e.g. h=6000 always means "predict 6000 raw snapshots forward").
- `prev` snapshot for microstructure features (prev_mid, prev_ts_ns)
  now points to the prior K-position (`anchor + (k-1)*stride`), NOT the
  consecutive-snapshot prior, so `Δt = ts_ns - prev_ts_ns` carries the
  actual elapsed time between K-positions (consumed by Mamba2's dt_s and
  the planned Phase 2C TGN Fourier features).
- New `#[ignore]` real-data test: `loader_stride_4_yields_correct_spacing`
  asserts Δt monotonicity at stride=4.

Mamba2 dt_s (crates/ml-alpha/src/trainer/perception.rs):
- `PerceptionTrainerConfig.decision_stride: usize` plumbs the stride
  through. dispatch_train_step + evaluate_batched now use
  `dt_s = decision_stride as f32` so Mamba2's selective scan
  `exp(-dt * sigmoid(a))` reflects the real elapsed time. With stride=1
  the behaviour is identical to before.

CLI (crates/ml-alpha/examples/alpha_train.rs):
- `--decision-stride <S>` flag (default 1) wired into both train and val
  loaders + PerceptionTrainerConfig.

Argo workflow:
- `decision-stride` parameter on the template (default "1") +
  `--decision-stride` script flag + propagation into the train pod's
  alpha_train invocation.

Synthetic smoke (tests/perception_overfit.rs):
- `stacked_trainer_loss_shrinks_with_stride_4` proves the trainer-level
  dt_s=4.0 keeps the Mamba2+LN+CfC+GRN chain numerically stable.
  Converges 0.32 → 0.0000 (matches stride=1 smoke trajectory — dt_s
  scaling didn't break the SSM dynamics).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:04:38 +02:00
jgrusewski
eb51c0f9cd feat(ml-alpha): walk-forward CV via file-list-driven loader
mhzs7 reported val mean_auc=0.726 on 3a196382f — but the trainer
constructed both train and val MultiHorizonLoader with the SAME
`mbp10_root: cli.mbp10_data_dir`. The two loaders only differed by
seed. So val sequences were held-out-by-anchor from the same files
train sampled from; not temporally OOS. Per
`pearl_single_window_oos_is_not_oos.md` a single-window result that
doesn't enforce time-ordered separation can collapse across true
walk-forward folds.

Refactor: drop `mbp10_root` from `MultiHorizonLoaderConfig` (which
forced caller to share the dir between train and val). New API takes
an explicit `files: Vec<PathBuf>` — the loader preserves the order
given and does no internal shuffle, so callers control temporal
ordering. Added `discover_mbp10_files_sorted(root)` helper that
enumerates a dir and sorts by filename (chronological under the
`ES.FUT_<YEAR>-Q<n>.dbn.zst` convention).

alpha_train.rs splits the discovered files by 3 new CLI flags:
  --cv-fold <k>            (default 0)
  --cv-n-folds <N>         (default 1 — single fold)
  --cv-train-window <W>    (default 0 — auto)

Single-fold default (cv_n_folds=1): train on all files except the
last, val on the last file. This replaces the old "same files for
both" bug; even runs that don't think about CV now get a temporal
split by default.

Sliding-window CV (cv_n_folds > 1): fold k trains on files
[k..k+W] and validates on file [k+W]. With 9 quarterly files
(2024-Q1..2026-Q1) and `--cv-n-folds 3`, the natural layout is:

  fold 0: train 2024-Q1..2024-Q4 (W=4) → val 2025-Q1
  fold 1: train 2024-Q2..2025-Q1       → val 2025-Q2
  fold 2: train 2024-Q3..2025-Q2       → val 2025-Q3
  blind holdout: 2025-Q4, 2026-Q1

Threaded the flags through scripts/argo-alpha-perception.sh and
infra/k8s/argo/alpha-perception-template.yaml so each fold submits
as an independent workflow.

Updated tests/multi_horizon_loader.rs to the new API:
  loader_yields_seq_with_valid_labels — exercises discover + load.
  loader_errors_on_empty_files       — replaces missing-root test.
  discover_errors_on_missing_root    — pinpoints the discover step.

Honors:
  - feedback_no_partial_refactor.md — every consumer migrated atomically.
  - feedback_no_legacy_aliases.md — no `mbp10_root` shim left behind.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:36:48 +02:00
jgrusewski
26d91a816c feat(alpha_train): configurable early-stop metric (default mean_auc)
cnjfl evidence: val_loss and mean_auc disagree.
  val_loss best at e3 (0.5592)
  mean_auc best at e4 (0.7670 — new h300 + h6000 peaks)

For downstream trading, ranking quality (AUC) matters more than
probability calibration (BCE loss). New default is mean_auc-based
early stopping, but val_loss/none remain selectable.

AUC is noisier than loss epoch-to-epoch (1-2pt bounces are common
even when long-horizon AUCs are still drifting up under
auto-horizon-weights), so patience defaults bump from 3 → 5.

CLI:  --early-stop-metric {val_loss|mean_auc|none}   default mean_auc
      --early-stop-patience N                         default 5

Argo template parameters added with matching defaults.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:48:21 +02:00
jgrusewski
affb0e24cf infra(argo): plumb --batch-size + --auto-horizon-weights through template
Adds two new workflow parameters with backward-compatible defaults
(batch-size=1, auto-horizon-weights=false) so existing submissions
behave identically. Both flags are forwarded to alpha_train CLI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:44:24 +02:00