Commit Graph

602 Commits

Author SHA1 Message Date
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
a3dc61a05a fix(rl): per-fold OUT_DIR so multi-fold G8 submissions don't collide
Concurrent submissions at the same SHA (one workflow per fold_idx
for the walk-forward G8 gate) would overwrite each other's
eval_summary.json. Adds a /foldN suffix to the output path so the
aggregator can collect 3+ distinct eval_summary.json files from
/feature-cache/alpha-rl-runs/<sha>/fold0,fold1,fold2/.

Single-fold smokes (n_folds=1) still write to /<sha>/fold0/
directly — backwards-compatible for the prior smoke pattern, just
one level deeper than before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 17:15:23 +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
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
ff9207e7bb feat(per-horizon-cfc): plumb kernel-step-trace into fxt-backtest inference path
Mirrors the alpha_train CLI flag (--kernel-step-trace <path>) into
fxt-backtest. The PerceptionTrainerConfig already accepts the path;
this commit just exposes the CLI surface and propagates through:

  fxt-backtest run --kernel-step-trace <path>
  -> BacktestHarnessConfig.kernel_step_trace_path
  -> PerceptionTrainerConfig.kernel_step_trace_path

Sweep YAML gains a `kernel_step_trace: Option<PathBuf>` base field.
Argo lob-backtest-sweep-template gains a `kernel-step-trace` workflow
parameter (default disabled; when set, --features kernel-step-trace
is passed to cargo build AND --kernel-step-trace to fxt-backtest).

Gated behind the `kernel-step-trace` Cargo feature (matching alpha_train).
When disabled (default), zero overhead.

Enables per-step JSONL diagnostic emission from inference kernels --
needed for Smoke 2 deep-dive after sampling histograms (in parallel
investigation) localize the per-horizon dynamics bottleneck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:33:44 +02:00
jgrusewski
f13d6c6dcf refactor(per-horizon-cfc): atomically remove MTER scaffolding
Per spec §2.5 and feedback_no_partial_refactor. Removes:
- LoaderMode enum (single-variant after Sequential removal → deleted entirely)
- next_sequence_sequential + sequential_file_idx + sequential_anchor_idx + last_call_was_file_boundary + is_file_boundary
- last_seen_file_boundary, train_graph_boundary_state fields
- notify_file_boundary method + boundary-state graph recapture logic
- need_attn_pool_bootstrap match — always true now (random mode always bootstraps)
- --loader-mode CLI flag + notify_file_boundary() call
- loader-mode Argo template param

Additional consumers migrated atomically (beyond the 4 files listed in
the plan): ml-alpha/tests/perception_overfit.rs,
ml-alpha/tests/multi_horizon_loader.rs, ml-backtesting/src/harness.rs,
ml-backtesting/tests/trainer_parity.rs,
ml-backtesting/tests/ring3_replay.rs — all referenced LoaderMode or the
removed config fields.

Workspace builds clean at this commit (pre-existing cudarc-cupti example
and ml-crate test errors are unrelated to MTER removal — they fail at
HEAD too). New per-horizon CfC arch lands in subsequent tasks of the
same plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:36:28 +02:00
jgrusewski
7dd953aa2b feat(intervention-b): add LoaderMode::Sequential + attn_pool gate
Loader can now serve sequences in temporal order (Sequential mode);
when active, the trainer skips attn_pool reset at non-file-boundary
sequence boundaries (next commit will use this for stateful CfC + MTER).

Random mode preserved as default; ZERO behavioral change for existing
runs. Phased commit 1/8 of intervention B per
docs/superpowers/specs/2026-05-21-crt-train-intervention-b-multi-timescale-readout.md
2026-05-21 11:30:50 +02:00
jgrusewski
6d65423e32 fix(argo): drop --decision-stride from alpha-perception template (removed from CLI per A1) 2026-05-21 09:30:25 +02:00
jgrusewski
2d5a66f6e4 feat(kernel-step-trace): runtime --kernel-step-trace CLI flag + JSONL drain
The compile-time feature kernel-step-trace gates inclusion of the ring
code. NEW: --kernel-step-trace <PATH> CLI flag on alpha_train gates
RUNTIME activation:

- Path provided   -> ring allocated, drain spawns, JSONL records written
                    to <PATH> (truncated). One record per kernel emission.
- Path omitted    -> ring not allocated, drain not spawned, zero overhead
                    even with the feature compiled in.

JSONL schema: {"step": u32, "kid": u8, "kname": str, "rt": u8,
"rt_name": str, "payload": {field: f32, ...}}. The payload field names
come from the existing per-(kid, rt) decoders in gpu_log.rs (ported to
serde_json::json! in decode_to_json).

Trainer ring fields are now Option<_>; populated only when the runtime
trace path is Some. Tick kernel, step-counter shadow, smoothness
controller pointer-passing, and Drop all become path-conditional.

The tracing::info!-based drain variant is removed (file-writer is
strictly more useful; tests migrated to assert JSONL file contents).

Argo template:
- New parameter kernel-step-trace-enable (build-time feature opt-in)
- New parameter kernel-step-trace-path (runtime CLI value)
- ensure-binary cache-busts when feature toggles
- training step passes --kernel-step-trace flag conditionally

Per feedback_no_feature_flags: compile-time gate retained because the
ring carries real memory cost (~2 MiB pinned) and per-step write overhead;
the specific name kernel-step-trace narrows scope to this mechanism.
Runtime gate is Option<PathBuf>, not a boolean enable_*.
2026-05-21 01:55:57 +02:00
jgrusewski
70ecfc0fe6 feat(crt-train): plumb --smoothness-base-lambda through alpha-perception template 2026-05-20 23:55:15 +02:00
jgrusewski
1a9cf80b8e fix(argo): force-checkout in ensure-binary to handle dirty Cargo.lock
The cargo-target-cuda PVC persists `$BUILD` across runs. cargo build
mutates Cargo.lock, so the next run's `git checkout $SHA` fails with
"Your local changes to the following files would be overwritten by
checkout: Cargo.lock". Same pattern alpha-perception-template uses
(`git checkout --force $SHA; git clean -fd`) handles this cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 18:17:35 +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
c7fdc617dc fix(ml-backtesting): variance-cap sample-size gate + aggregate GPU req
Three follow-ups to the cold-start floor fix:

1. Kernel: MIN_TRADES_FOR_VAR_CAP gate. After the first trade closed,
   `isv_kelly_update_on_close` set `realised_return_var = ret²` — a
   single-sample variance proxy that systematically collapses
   `cap_units = target_vol / sqrt(var × ann_factor)` near zero for
   any biased return. cap_lots → 0 → no further trades despite strong
   alpha. Gate the variance-derived cap behind `n_trades_seen >= 10`;
   below the threshold cap falls back to host-supplied `max_lots`,
   same as the pre-first-trade path. Same gate applied to both
   `decision_policy_default` and `decision_policy_program`.

   Regression: `post_first_loss_state_does_not_lock_out_further_trades`
   reproduces the exact pre-fix state from the smoke (n_trades_seen=1,
   var=103.6) and asserts the kernel still fires a long with p=0.8.

2. Aggregate: add `nvidia.com/gpu: 1` resource request. Scaleway's
   L40S device plugin mounts libcuda.so.1 into the container only on
   GPU-requesting pods; the aggregate logic is CPU-only but the
   binary's dynamic loader needs the driver libs. Cheapest correct
   fix until a separate CPU-only aggregator binary exists.

3. Smoke YAML: `max_events: 0` (exhaust loader). 100k events is
   minutes of ES.FUT, far shorter than the h6000 holding horizon the
   model was trained on. Full quarter exercises sustained trading +
   variance estimate ramp-up.

All three regression tests pass locally on RTX 3050 Ti.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:07:37 +02:00
jgrusewski
da21feb1b1 fix(ml-backtesting): cold-start Kelly + Sharpe-weight floors in decision kernel
Production smoke completed end-to-end but produced n_trades=0 across 99,969
decisions — `decision_policy_default` and `decision_policy_program` both
applied a sentinel-skip pattern: if `isv_kelly_d` had not been seeded
(pnl_ema_win == 0), each horizon's signed-size stayed zero, AND each
horizon's aggregation weight (= recent_sharpe) also stayed zero. The
cross-horizon w_sum was therefore 0, final_size was 0, every market_target
was noop. State only updates on trade close → no trade ever fires →
infinite cold-start.

Per pearl_blend_formulas_must_have_permanent_floor (`max(real, floor)`,
not blend) and pearl_kelly_cap_signal_driven_floors, replace the sentinel-
skip with a two-layer floor on each kernel:

1. Kelly fraction: `max(kelly_frac_floor, computed_kelly)` — when state
   is sentinel, falls back to the floor directly. Cap_lots falls back
   to `max_lots` when realised_return_var is sentinel.
2. Aggregation weight: `max(sharpe_weight_floor, recent_sharpe)` — lets
   cross-horizon sum produce a non-zero size before recent_sharpe is
   populated. Once a horizon shows positive sharpe it dominates.

Plumbed through `step_decision_with_latency` / `step_decision` as two
new f32 args (atomic contract change, every caller migrated). Defaults
0.20 / 0.10 chosen so a strong-conviction signal (sig_mag ≥ 0.5) fires
1 lot at cold-start under max_lots=5 while weaker signals stay flat
(see `default_kelly_frac_floor` comment for the arithmetic). Exposed
as CLI flags + sweep-grid base/cell overrides.

Regression test `decision_floor_coldstart` proves:
 - default floors (0.20/0.10) fire a 1-lot buy with p_h=0.8 and zero state
 - zero floors reproduce the original noop bug

Also moves `aggregate` step to the GPU pool because fxt-backtest is
dynamically linked against libcuda.so.1 (the ci-compile-cpu hosts
don't expose CUDA driver libs).

Verified locally on RTX 3050 Ti — workspace cargo check passes, both
regression tests pass, trunk save/load roundtrip still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:41:17 +02:00
jgrusewski
dbd500ecf2 fix(argo): mount feature-cache PVC + correct smoke paths
- Template was missing feature-cache PVC mount on run-cell; trained
  checkpoints + predecoded data live at /feature-cache/* on the
  feature-cache-pvc claim. Mount it the same way alpha-perception does.
- sweep_smoke.yaml referenced /data/* (no such mount) and a 40-char
  SHA path for the checkpoint dir. alpha-perception-runs subdirs are
  9-char short SHAs; data path is /mnt/training-data/* (matches mount).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:38:10 +02:00
jgrusewski
17ecfce48c fix(argo): training-runtime image is named foxhunt-training-runtime
Working alpha-perception-template references
`foxhunt/foxhunt-training-runtime:latest`. Our lob-backtest-sweep
template stripped the `foxhunt-` prefix and got 404 from the registry
on run-cell + aggregate steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:34:05 +02:00
jgrusewski
77d1c8e09a fix(argo): ensure-binary cp from CARGO_TARGET_DIR, not in-tree target/
CARGO_TARGET_DIR=/cargo-target redirects all build output away from
the in-tree `<crate>/target/` path, so `cp $BUILD/target/release/...`
fails after a successful compile. Mirror alpha-perception-template's
correct pattern: `cp $CARGO_TARGET_DIR/release/...`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:28:00 +02:00
jgrusewski
0567d547ce fix(argo): lob-backtest-sweep pods need component=train for GitLab egress
Pods labeled `component: backtest` only match the `argo-base-egress`
NetworkPolicy (DNS, 443, MinIO, Mattermost). The ensure-binary step
needs SSH-to-gitlab-shell on port 2222, which is gated by
`argo-train-workflow` (selects `component=train`).

Relabel to `train` so the same NetworkPolicy that lets alpha-perception
clone the repo also covers lob-backtest-sweep. Egress shape (compile,
GPU run, PVC writes) is identical to a training run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:24:22 +02:00
jgrusewski
84c32c0462 fix(infra): lob-backtest-sweep chmod 600 ~/.ssh/config
The template wrote ~/.ssh/config but didn't chmod it. OpenSSH refused
the file with 'Bad owner or permissions on /root/.ssh/config' and the
git clone failed with 'exit status 128' before fxt-backtest could
compile.

Same one-line fix that alpha-perception-template.yaml has had since
forever. Verified: lob-backtest-sweep-<new> ensure-binary now passes
the SSH check and starts cargo build.
2026-05-19 12:16:54 +02:00
jgrusewski
25c78c5913 fix(infra): lob-backtest-sweep secretName git-ssh-key -> argo-git-ssh-key
Matches the alpha-perception-template (which is the known-good
template in this namespace). Without the fix, ensure-binary +
run-cell + aggregate pods all stayed Init for ~20m with
'MountVolume.SetUp failed for volume "git-ssh-key" : secret
"git-ssh-key" not found' before the workflow timed out.

The actual secret in the foxhunt namespace is named
'argo-git-ssh-key' (see kubectl get secrets -n foxhunt). Same
template misconfig as the training-data PVC name.
2026-05-19 12:12:34 +02:00
jgrusewski
1cccb4e40e fix(infra): lob-backtest-sweep volume claim name training-data -> training-data-pvc
The template referenced PVC 'training-data' but the actual claim in
the foxhunt namespace is 'training-data-pvc' (matches the convention
used by other workflows like alpha-perception-template). Without this
fix, ensure-binary + run-cell + aggregate pods all stayed Pending
forever with 'persistentvolumeclaim "training-data" not found',
blocking every smoke / threshold-tuning / deployability sweep.

Verified: smoke sweep lob-backtest-sweep-jp48v scheduled ensure-binary
onto the ci-compile-cpu pool (autoscaler triggered scale-up 0->1
within seconds of resubmission).
2026-05-19 11:52:19 +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
jgrusewski
4514313793 infra(argo): use decimal seed value — clap u64 parser rejects hex
alpha_train --seed is clap-typed as u64, and clap's default u64
parser only accepts decimal digits. Previous default "0x4242"
hit "invalid digit found in string" at startup.

Replace with decimal equivalent 16962 (= 0x4242) in both the
submission script default and the workflow template default.

Long-term: could add a custom clap value_parser that accepts
hex/dec/oct prefixes, but for now decimal-only matches the
foxhunt convention in other CLIs (alpha_baseline, etc.).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 00:09:43 +02:00
jgrusewski
1f14b994e4 infra(argo): fix MBP-10 data path — futures-baseline-mbp10/ES.FUT
The training-data PVC actually has its MBP-10 .dbn.zst files at
/data/futures-baseline-mbp10/ES.FUT (9 files for ES futures), NOT at
/data/futures-baseline/mbp10. The latter path doesn't exist; the
prev run's bash check fired exit 1 with "MBP-10 data directory not
found".

The PVC root layout (from a probe pod):
  /data/bin/                              <- compiled binaries by SHA
  /data/feature-cache/                    <- predecoded sidecar cache
  /data/futures-baseline/{ES,NQ,ZN,6E}.FUT/  <- legacy multi-asset
  /data/futures-baseline-mbp10/ES.FUT/    <- ES MBP-10 (this is what we want)
  /data/futures-baseline-trades/ES.FUT/   <- ES trades
  /data/futures-baseline-1s/ES.FUT/       <- 1-second OHLCV
  /data/trained-models/                   <- model checkpoints

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 00:06:32 +02:00
jgrusewski
9f866fa369 infra(argo): fix train image — foxhunt-training-runtime not training-runtime
The CI pipeline (.gitlab-ci.yml stage `build-foxhunt-training-runtime`)
builds and pushes the image as foxhunt-training-runtime:latest (with
the foxhunt- prefix). Other consumers in the repo agree:
  - infra/k8s/training/image-prepuller.yaml
  - infra/k8s/training/job-template.yaml

The alpha-perception template (and the older alpha-cv template) used
training-runtime:latest without the prefix — broken since the image
never existed at that path. Kubelet kept hitting ErrImagePull /
ImagePullBackOff with "not found".

This was the next blocker after the CPU oversizing fix. Stopping the
in-flight workflow and resubmitting on the corrected template.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 00:00:28 +02:00
jgrusewski
d2bfeaa983 infra(argo): check-cache pre-stage — skip ensure-binary on cache hit
Adds a tiny alpine pod (check-cache) that runs first on the platform
pool (no autoscaler delay, ~3 sec end-to-end) and probes the
training-data PVC for /data/bin/$SHA/alpha_train. Outputs:
  - sha:   short SHA used for binary cache keying
  - cache: "hit" or "miss"

ensure-binary now has `when: cache == miss` — when the binary is
already cached for the current SHA, the entire ~4.8GB ci-builder
image pull + sccache compile cycle is skipped. Re-runs on the same
SHA now go straight from submission to training in ~30 seconds
instead of ~3 minutes.

train depends on check-cache + ensure-binary; sources the SHA from
check-cache's output (works whether ensure-binary ran or was
skipped — Argo treats `when:` skip as a satisfied dependency).

Submission script (scripts/argo-alpha-perception.sh) now pre-resolves
commit-sha=HEAD to an actual git SHA via `git rev-parse origin/<branch>`
before submission. This lets the alpine check-cache pod work without
installing git in the container.

Also removed the now-stale `ci-training-h100x2|ci-training-h100-sxm`
case branch from the SM-arch detection — those pools no longer exist
post-pool-cleanup commit a252119fd.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:56:19 +02:00
jgrusewski
3ad3971ae6 infra(argo): fix train pod CPU request — L40S has 8 vCPU not 16
Previous cpu request was 8 (limit 16), but L40S-1-48G allocatable cpu
is 7800m (8 vCPU total minus kubelet overhead). Pod couldn't fit on
the very node we wanted it on — autoscaler provisioned the L40S
cleanly but the scheduler then rejected the pod with
"Insufficient cpu" forever.

Fix: requests cpu=6 / mem=16Gi, limits cpu=7 / mem=64Gi. Leaves
~1.8 vCPU and ~27Gi memory headroom for the system daemonsets
(cilium, csi-node, nvidia driver/device-plugin/dcgm-exporter,
gpu-feature-discovery, node-bootstrap, prometheus-node-exporter,
promtail) that the L40S node hosts.

The trainer itself is GPU-bound (kernels do the work), so 6 vCPU is
plenty for the orchestration host process.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:50:40 +02:00
jgrusewski
a252119fd4 infra(kapsule): remove h100x2 + h100-sxm pools
Reasons:
- h100-sxm: Scaleway account quota is 0/0 for the SXM instance type
  (cp_servers_type_H100_SXM_2_80G). The pool was perpetually trying
  to maintain size=1 with a creation_error node, which JAMMED the
  cluster autoscaler. That blocked L40S scale-up entirely during
  today's alpha-perception cluster run.
- h100x2: more expensive than current workloads justify. Every
  training path (alpha perception, future PPO) fits on either the
  single-GPU H100 or L40S.

Removed:
- Two `scaleway_k8s_pool` resources from infra/modules/kapsule/main.tf
- Six variables (enable + type + max_size for each pool)
- Two outputs (pool_id for each)
- Corresponding inputs in infra/live/production/kapsule/terragrunt.hcl

The live cluster has the SXM pool stuck node manually deleted via
`scw k8s node delete` (this commit-session); the pool resource itself
will be destroyed on next `terragrunt apply`.

Post-cleanup pool inventory:
- platform (DEV1-L × 3)
- ci-training-h100  (H100-1-80G, max 1)  <- regular single-GPU
- ci-training-l40s  (L40S-1-48G, max 1)  <- primary training target
- ci-compile-cpu    (POP2-HC, max 4)
- ci-compile-cpu-hm (POP2-HM, max 1)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:43:12 +02:00
jgrusewski
3dc42a6827 infra(argo): correct warmup-gpu doc — cluster grace is 10+10=20m
The actual Kapsule autoscaler_config (infra/modules/kapsule/main.tf
lines 46-52) is scale_down_delay_after_add="10m" +
scale_down_unneeded_time="10m". Combined, a freshly-provisioned node
won't be eligible for scaledown for 10m + another 10m unneeded
before action, so effective grace window is ~20m.

Update the comment in the warmup-gpu template to cite the actual
config rather than the speculative "15m" value. Behavior is
unchanged — the warmup pod still exits immediately and relies on
the grace window to keep the node warm for train.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:32:33 +02:00
jgrusewski
29cf092551 infra(argo): warmup-gpu exits immediately — relies on Scaleway 15min scaledown grace
Removed the 30s sleep. The warmup pod's purpose is to make the L40S
pool autoscaler scale 0 → 1; once the pod lands on the new node
(Scheduled → Running → Succeeded), the node enters Scaleway Kapsule's
scaledown-grace window (~15 min). That single window covers the
entire range of ensure-binary durations (sccache-hit ~10s through
cold ~15 min), so train always lands on a hot node without holding
the warmup pod open.

Trimmed cpu request 100m → 50m and mem 64Mi → 32Mi: the pod runs
~one shell command then exits; tiny resource footprint = faster
scheduling and no kubelet noise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:31:40 +02:00
jgrusewski
260b07c492 infra(argo): warmup-gpu DAG branch — pipeline compile + L40S provisioning
Adds a parallel warmup-gpu task that runs concurrently with
ensure-binary. The warmup pod is a tiny CPU-only alpine container
scheduled on the gpu-pool's nodeSelector — its presence triggers
cluster-autoscaler scale-up of the L40S pool. After a 30s sleep, the
warmup pod exits; the node enters Scaleway's scaledown grace window
(~10 min), so the train pod lands on a hot node without waiting for
autoscaler provisioning.

No GPU resource request on the warmup pod — that would serialise
warmup and train on the same GPU. nodeSelector + nvidia.com/gpu
toleration are sufficient to force placement on the L40S pool.

Expected savings: ~3-5 min per cold cluster submission. First run
(this session, alpha-perception-4vl7c) showed compile took 113s
(sccache warm) with serial GPU provisioning following; subsequent
submissions should overlap the two stages.

DAG topology:
  ensure-binary ──┐
                  ├──> train
  warmup-gpu  ────┘   (only depends on ensure-binary; warmup is
                       fire-and-forget infrastructure)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:29:16 +02:00
jgrusewski
ef35b10f3e refactor(ml-alpha): drop competitive-gate machinery — stacked is default
Per user direction "no gating, this is the new default": the stacked
Mamba2 -> CfC -> heads design is THE production architecture. There's
no competing-baseline comparison to run. Validation reduces to normal
training metrics (per-horizon val AUC, train loss curve, sanity floor
of >0.5 AUC).

Deletions:
  - crates/ml-alpha/src/gate/cfc_vs_mamba2.rs (gate verdict logic)
  - crates/ml-alpha/src/gate/mod.rs
  - crates/ml-alpha/examples/alpha_gate.rs (gate runner binary)

Renames:
  - crates/ml-alpha/src/gate/auc.rs -> crates/ml-alpha/src/eval/auc.rs
  - lib.rs: pub mod gate -> pub mod eval (gate implied comparison;
    eval doesn't)

Spec amendments:
  - Drop the "Gate baseline strategy" amendment (committed earlier
    this session)
  - Reframe the stacked-architecture amendment as a "decision" not a
    "gate"; production path is unambiguous
  - Reframe Section 4 "Validation gate: CfC must meet Mamba2" -> just
    "Validation: per-horizon val AUC" with the >0.5 sanity floor

Doc cleanups: stale "Mamba2 gate baseline" mentions in build.rs and
pinned_mem.rs replaced with neutral wording. The Argo template
comment about "downstream gate consumption" becomes "for monitoring".

Test status: all 26+ ml-alpha tests pass. AUC tests (6/6) still pass
under the eval:: namespace.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:17:33 +02:00
jgrusewski
522178b2a7 infra(argo): alpha-perception workflow + submission script
Argo WorkflowTemplate at infra/k8s/argo/alpha-perception-template.yaml
runs the stacked Mamba2 -> CfC -> heads PerceptionTrainer on a single
L40S in fr-par-2. Two-stage DAG:
  ensure-binary  (ci-compile-cpu pool, sccache-backed cargo build of
                  alpha_train example, SHA-keyed binary cache under
                  /data/bin/$SHORT_SHA/)
  train          (ci-training-l40s pool, runs the cached binary against
                  /data/futures-baseline/mbp10 with predecoded sidecar
                  cache at /feature-cache/predecoded, writes
                  alpha_train_summary.json to
                  /feature-cache/alpha-perception-runs/$SHA/)

Defaults mirror the validated synthetic-overfit smoke config:
  epochs=5, seq_len=32, mamba2_state_dim=16, lr_cfc=3e-3,
  lr_mamba2=1e-3, n_train_seqs=8000, n_val_seqs=1000, seed=0x4242

Submission script scripts/argo-alpha-perception.sh wraps argo submit
with the standard L40S/H100 cuda-compute-cap mapping. --watch
follows logs.

Workflow nodeSelector pinned to fr-par-2 (consistent with the cluster
topology constraint). ttlStrategy 1h after completion;
activeDeadlineSeconds 4h cap (well above expected ~30-90 min wall).

This is the cluster entrypoint for the stacked perception design.
Once it lands a summary on MinIO, the gate runner (alpha_gate, Task
17) can consume it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:10:37 +02:00
jgrusewski
10f4bcc15b feat(alpha): decision-stride + cluster 9-fold CV workflow
Two complementary additions to validate the minute-horizon alpha
hypothesis at IBKR-realistic costs:

1. `alpha_baseline --decision-stride N`: emits a new action every N
   steps; between decisions force action=0 (wait) so an open position
   is held rather than re-decided per bar. Cuts per-bar trade counts
   ~stride× and removes the coin-flip overtrading. Local 2Q sweep
   showed stride=200 + scaled training (8K episodes × 25 envs × H=1200)
   flipped Sharpe at ¼-tick from -4.29 (per-bar, 3-fold mean) to +1.78,
   with std collapsing from ±8.8 to ±1.15. Break-even cost moved from
   <¼-tick to ~1-tick — for the first time positive at IBKR-realistic
   passive-execution frictions.

2. `alpha_train_stacker --max-rows N`: optional cap on bars consumed
   from the fxcache. Used during local 2Q smoke (--max-rows 4M against
   the 17.8M-row 9Q fxcache) to fit Mamba2 training on a 4 GB consumer
   GPU; on the cluster (--no-cap) it sees all 9Q.

3. New Argo workflow `alpha-cv`: standalone template that compiles
   alpha_train_stacker + alpha_baseline + alpha_fill_coeffs.json,
   trains the stacker on the 9Q fxcache, then runs 9 sequential
   walk-forward folds of alpha_baseline on disjoint 1.9M-bar windows
   (one per quarter). Launcher script `scripts/argo-alpha-cv.sh`
   mirrors argo-train.sh conventions.

The local 2Q test that motivated this commit is summarised inline in
the alpha-cv template comments; the verdict was "framing was the bug —
once decision cadence matches the multi-minute alpha horizon, the
strategy is positive at IBKR commission".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 18:04:34 +02:00
jgrusewski
737c2c1305 infra(argo): pin all workflow nodeSelectors to topology.kubernetes.io/zone=fr-par-2
Scaleway BSSD PVCs (cargo-target-*, sccache-*, feature-cache-pvc,
bin-cache, training-data-pvc, test-data-pvc, platform service PVCs) are
single-zone-locked at create time. The cluster's node pools in main.tf
set `region` but no `zone`, so each autoscale event picks a zone
arbitrarily. When the autoscaler spins up a node in a zone that doesn't
match an existing PVC, scheduling fails with
  "1 node(s) didn't match PersistentVolume's node affinity"
until the autoscaler eventually retries in the right zone. We hit this
on the HM pool creation (fixed manually with zone=fr-par-2) and again
on this commit's ensure-binary autoscale.

Track 1 (this commit): add `topology.kubernetes.io/zone: fr-par-2` to
every k8s.scaleway.com/pool-name nodeSelector entry across the 11 argo
workflow templates (34 entries total). The Kubernetes scheduler AND
cluster-autoscaler both honor topology keys when deciding placement /
provisioning — so future autoscale events will only spin up fr-par-2
nodes, and PVC binding is guaranteed.

Track 2 (future, structural): add `zone = "${var.region}-2"` to each
scaleway_k8s_pool in infra/modules/kapsule/main.tf so the pools never
provision in any other zone. Requires terraform-state cleanup (the
GitLab http backend currently has no states; the HM pool was created
out-of-band) and drain/recreate of any existing mixed-zone nodes —
deferred.

Verification: pool=N / paired=N coverage report shows 1:1 pool-name
to topology.kubernetes.io/zone entries in every file. kubectl apply -f
of all 11 templates returned "configured" for each.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 15:08:37 +02:00
jgrusewski
86de265fc8 infra(kapsule): add ci-compile-cpu-hm pool for 9-quarter fxcache
Two 9-quarter precompute_features runs OOM-killed on the existing
ci-compile-cpu pool (POP2-HC-32C-64G, 56Gi cgroup limit):
  - train-wq8b8: exit 137 ~12s after "OFI computed" at ~57Gi
  - train-2l6p4: exit 137 ~12s after "OFI computed" at ~52Gi peak
    (despite the into_iter + drop(feature_vectors) +
    normalize_in_place refactors landed in a27cb40a9 + 623ebfcf7,
    which trimmed ~12GB of avoidable retention)

The remaining ~52-60GB peak is the irreducible working set at the
post-OFI / pre-alpha-pipeline step:
  - 9-quarter MBP-10 snapshots (~10GB)
  - 199M front-month trades as Mbp10Trade (~13GB)
  - 17.8M bars × features + targets + OFI buffers (~14GB)
  - alpha_snapshots (move-handoff from all_snapshots, ~10GB)
  - feature/target Vecs (~7GB) + Rust allocator overhead

Adds a dedicated high-memory pool sized for this rare path:

- New scaleway_k8s_pool.ci_compile_cpu_hm (POP2-HM-32C-256G,
  32 vCPU + 256GB RAM) with size=0 + min_size=0 autoscaling. Costs
  zero when idle; autoscaler provisions one node when a pod targets
  `nodeSelector: ci-compile-cpu-hm`.
- New variables: enable_ci_compile_cpu_hm_pool,
  ci_compile_cpu_hm_type (default POP2-HM-32C-256G),
  ci_compile_cpu_hm_max_size (default 1).
- terragrunt.hcl: enable the pool, max_size=1.
- train-template.yaml: ensure-fxcache nodeSelector pinned to
  ci-compile-cpu-hm; memory limit raised 56Gi → 200Gi. The
  ci-compile-cpu pool stays as the standard CI compile target for
  ensure-binary + every other CPU-heavy task.

Apply with:
  cd infra/live/production/kapsule
  terragrunt apply -target=module.kapsule.scaleway_k8s_pool.ci_compile_cpu_hm
  kubectl apply -n foxhunt -f ../../../../infra/k8s/argo/train-template.yaml

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 11:13:03 +02:00
jgrusewski
d5896ef809 infra(argo): skip gpu-warmup on the fxcache-only path
gpu-warmup pre-provisions an L40S node so hyperopt / train-best don't
pay autoscaler latency at workflow start. When hyperopt-trials=0
(the precompute-only path used by scripts/argo-precompute.sh), both
downstream consumers are skipped via their `when:` clauses and the
warmup-provisioned GPU node would sit idle until workflow end.

Add the same `when:` clause to gpu-warmup so it skips alongside its
consumers, saving an L40S node's provisioning + idle time for every
fxcache-rebuild submission.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:35:20 +02:00
jgrusewski
8b8bb1af70 infra(argo): default GPU pool to ci-training-l40s (sm_89) per feedback_default_to_l40s_pool
SP-chain training has been standardising on L40S since 2026-05-09, but
every invocation required an explicit `--gpu-pool ci-training-l40s`
override. The 2026-05-04 train-mnpf7 incident (sm_90 cubins deployed
to an L40S device, then resubmitted with the explicit override) was
the last incident in a long line of "forgot the pool flag" friction.
`feedback_default_to_l40s_pool.md` codified the user preference; this
commit lands the default in the actual invocation paths.

Changes:

  - infra/k8s/argo/train-template.yaml: gpu-pool default H100 → L40S
  - infra/k8s/argo/train-multi-seed-template.yaml: same + cuda-compute
    -cap default 90 → 89
  - scripts/argo-train.sh: docstring / --help / compute-cap fallback
    case all flip to L40S as the bare default; H100 becomes opt-in via
    `--gpu-pool ci-training-h100` for 80 GB / sm_90 workloads
  - scripts/argo-test.sh: --help text aligned

Other architectural defaults (data-source=mbp10 per
feedback_mbp10_mandatory; imbalance-bar-threshold=20.0 per the 2026-
05-10 OOM-prevention fix) are already correct in the template.

Verified via `argo-train.sh dqn --branch sp20-aux-h-fixed --sha HEAD
--baseline --dry-run` — rendered workflow shows cuda-compute-cap=89,
no explicit gpu-pool override (template default L40S in effect).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:22:19 +02:00
jgrusewski
e9b85df72b fix(sp20): default imbalance_bar_threshold 0.5/2.5 → 20 (3 files atomic)
At threshold=0.5 the sampler produced 209M bars from 209M trade ticks (1:1
ratio, essentially per-tick) on workflow f5wnd today. Feature extraction hit
54Gi/56Gi memory limit — near OOM. The default was wrong: with EWMA bypassed
(per 1aaf94306), threshold=0.5 contracts of imbalance is below the natural
ES.FUT trade size, so every trade fires a bar.

threshold=20 produces ~5-6M bars matching the volume-bar density baseline
(5.74M bars at 100 contracts). Math: 209M / 5.74M = 36×, so threshold needs
0.5 × 36 ≈ 18 → round to 20.

Three files updated atomically per feedback_no_partial_refactor:
- config/training/dqn-production.toml: 2.5 → 20.0 (wgdc8 left it at 2.5)
- infra/k8s/argo/train-template.yaml: workflow default 0.5 → 20.0
- infra/k8s/argo/train-multi-seed-template.yaml: workflow default 0.5 → 20.0

The 1:1 bar:tick observation alongside the price-continuity proof (22 jumps
out of 209M = ~1e-7 — front-month filter works) confirms the front-month
fix on this branch (6c1ab8850 + 3b5f17913) is correct. Threshold was the
remaining mistuning.

Cache-key implication: changing the threshold invalidates all existing
fxcache files (per the cache-key architectural fix). Next dispatch on this
branch will regenerate fxcache from scratch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:53:23 +02:00
jgrusewski
abd7e533bc fix(architectural): volume_bar_size in cache key + OFI front-month filter
## Two architectural cleanups, both surfaced by the wgdc8 experiment

### Part 1: volume_bar_size in cache key

Mirrors the imbalance_bar_threshold/ewma_alpha fix from `f7718b376`. The
volume bar size constant (100 contracts/bar) was previously hardcoded and
not in the fxcache key. Tuning it would have hit the same fossilization
bug as imbalance_bar_threshold did pre-fix.

Changes:
- `Hyperparams.volume_bar_size: u64` field added (default 100, matches
  `DEFAULT_VOLUME_BAR_SIZE` for backwards compat).
- TrainingProfile loader reads `volume_bar_size` TOML key.
- `calculate_dbn_cache_key_full` signature 7 → 8 args. Hashed via
  `to_le_bytes()`. Test `test_cache_key_includes_volume_bar_size`
  added; passes alongside the 5 existing tests.
- 4 callers updated atomically (per `feedback_no_partial_refactor`):
  `discover_and_load`, `data_loading.rs:146`, `train_baseline_rl.rs:599`,
  `precompute_features.rs:259,720`.
- `data_loading.rs:279` now passes `self.hyperparams.volume_bar_size`
  to `build_volume_bars` instead of the hardcoded `DEFAULT_VOLUME_BAR_SIZE`.
- New `--volume-bar-size` CLI arg on both binaries (default 100).
- New Argo workflow params `volume-bar-size` (default "100") and
  `data-source` (default "mbp10") on both `train-template.yaml` and
  `train-multi-seed-template.yaml`. Threaded into precompute + trainer
  invocations.
- `scripts/argo-train.sh` exposes `--volume-bar-size <n>` and
  `--data-source <s>` for ad-hoc overrides.

### Part 2: OFI front-month filter (latent bug fix)

`crates/ml/examples/precompute_features.rs:539-557` (the OFI/VPIN/Kyle's
Lambda computation branch when MBP-10 + trades data is available) was
loading trades unfiltered for per-bar microstructure feature computation.
The volume bar formation path filters front-month per-file (line 354), but
the OFI path did not.

Effect pre-fix: during contract rollover windows (e.g., ESZ24 → ESH25),
OFI per-bar microstructure features included trades from BOTH contracts
simultaneously, distorting VPIN, Kyle's Lambda, and trade imbalance
signals. Severity in production: small (front-month dominates ES.FUT
volume by 10-100×) but real and present in every prior MBP-10+trades
production run.

Fix: mirror the per-file `filter_front_month` call from the volume bar
path. Volume bar formation and OFI computation now both see the same
in-month trade tape. Added log line shows raw vs filtered count per file
for transparency.

## Why bundled

Both fixes touch trade-data plumbing in `precompute_features.rs` and the
fxcache key contract. Per `feedback_no_partial_refactor`, related
architectural cleanups land atomically. Both surfaced from the same
wgdc8 audit; bundling avoids two cache-key-invalidating commits in
sequence (each would force full fxcache regen).

## Compatibility

- `volume_bar_size` defaults to 100 → existing wgdc7-equivalent runs
  reproduce, but with a *new* fxcache key (the f7718b376-era cache file
  is unreachable; harmless, can GC manually).
- OFI fix is strictly more correct; no opt-out needed. Existing models
  trained on contaminated OFI features may show slight feature
  distribution drift on first cache regen — expected, not a regression.
- `data_source = "ohlcv"` Argo param now possible; routes precompute
  through volume bar branch directly. wgdc8 experiment uses this to test
  bar resolution sensitivity at volume_bar_size=500 (5× DEFAULT).

Tests: 6/6 feature_cache tests pass. Workspace + examples compile clean.
Audit-doc: `docs/dqn-wire-up-audit.md` updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:55:33 +02:00
jgrusewski
f7718b3761 fix(architectural): include bar formation params in fxcache key + actually USE imbalance bars
## The bug (audit 2026-05-09)

`crates/ml/src/feature_cache.rs:calculate_dbn_cache_key_full` hashed only
`(symbol, data_source, dbn_filenames+sizes)` — NOT `imbalance_bar_threshold`
or `imbalance_bar_ewma_alpha`. Combined with `precompute_features.rs:346`
unconditionally calling `build_volume_bars` regardless of `data_source`,
this meant:

1. 14 audited production runs (Apr 11–May 8) all collided on the same
   fxcache key (`a3f933aa...` / `c07c960a...`) regardless of TOML
   `imbalance_bar_threshold` value
2. The imbalance-bar code path was reachable only via fxcache MISS, which
   never happens in production because `ensure-fxcache` always populates
   first
3. Every "tuning" of `imbalance_bar_threshold` across 16+ SP runs was a
   silent no-op — the system was actually running volume bars at
   DEFAULT_VOLUME_BAR_SIZE (100 contracts/bar)

## The fix (this commit)

**Part A — cache key includes bar formation params:**
- `calculate_dbn_cache_key_full` signature: 5 args → 7 args. Two new f64
  params hashed via `to_le_bytes()`.
- 4 callers updated atomically (per `feedback_no_partial_refactor`).
- 2 new unit tests (`test_cache_key_includes_bar_threshold`,
  `test_cache_key_includes_bar_alpha`) pin the contract.

**Part B — precompute_features actually USES data_source:**
- New CLI args `--imbalance-bar-threshold` (default 0.5) and
  `--imbalance-bar-ewma-alpha` (default 0.1) on both train_baseline_rl
  and precompute_features.
- `precompute_features.rs:346` now branches: when
  `data_source == "mbp10"` AND `mbp10_data_dir.is_some()`, calls
  `mbp10_to_imbalance_bars` instead of `build_volume_bars`.

**Argo plumbing:**
- `train-template.yaml` + `train-multi-seed-template.yaml`: new workflow
  parameters threaded into BOTH precompute and trainer invocations so
  both compute the same fxcache key.
- `scripts/argo-train.sh`: new CLI flags for ad-hoc overrides.
- ensure-fxcache regen path: removed `rm -f /feature-cache/*.fxcache`
  (with bar-params now in key, parallel experiments coexist).

## Effects going forward

- Tuning `imbalance_bar_threshold` actually changes bar density
- Configuring `data_source = "mbp10"` actually produces imbalance bars
- Multiple parallel experiments at different thresholds coexist on PVC
- `dqn-production.toml: imbalance_bar_threshold = 0.5` no longer ignored

Default values match prior production behavior → existing wgdc7-equivalent
runs reproduce, just with a *new* fxcache key (the old volume-bar cache
file is still on disk but won't be hit; harmless, can GC manually).

Audit-doc: `docs/dqn-wire-up-audit.md` updated with full context.
Tests: 5/5 feature_cache tests pass, full workspace + examples compile clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:04:50 +02:00
jgrusewski
04df20bdc3 infra(argo): smoke populates ensure-binary cache for next train run
The smoke template already compiled `train_baseline_rl`. Now also build
`evaluate_baseline` + `precompute_features` and copy + strip all 3 to
`/data/bin/$SHORT_SHA/` after PASS — exactly the layout that
train-multi-seed-template.yaml's `ensure-binary` cache check
(lines 235-246) looks up by SHA. A smoke-then-train sequence at the same
SHA now hits the cache and skips the ~6-min compile, exiting in the
~1-min pod-startup baseline.

- Drop readOnly:true on training-data PVC mount (cargo writes binaries
  into /data/bin/$SHORT_SHA; read-side fxcache + market data unchanged)
- Build all 3 example binaries in a single cargo invocation (sccache-warm)
- Idempotent SHA-keyed dest dir; PASS-only so broken bins never cache

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:30:06 +02:00