Commit Graph

5385 Commits

Author SHA1 Message Date
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
ed34d356a8 docs(per-horizon-cfc): kickoff — spec + plan + historical record
Spec: docs/superpowers/specs/2026-05-21-per-horizon-cfc-inference-design.md
Plan: docs/superpowers/plans/2026-05-21-per-horizon-cfc-inference-plan.md

Spec went through 2 critical-review passes (32 total findings, all resolved).
Bucketing source: CfC.tau (per-channel, trained, log-uniform init at HIDDEN_DIM=128).
Atomic refactor reverts MTER scaffolding. 5 ISV-driven controllers. All-on-device
transition (no bulk DtoH). Single fused per-branch kernel. Compact ragged heads_w_skip.
Validated via 3 Argo smokes (training stability, CRT.diag inference differentiation,
fxt-backtest end-to-end).

Also committing historical record: superseded MTER spec/plan, intervention A
plan, ISV λ controller spec/plan, GPU log ring spec/plan. Per spec §5.3 these
stay as audit trail; not in build path.

Plan has 18 tasks, full TDD with bite-sized steps, ready for
subagent-driven-development execution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:05:28 +02:00
jgrusewski
f22f3f9488 fix(intervention-b): recapture train graph on boundary-state change
Task 1's attn_pool gate (commit 7dd953aa) was inside the captured-graph
region, so the host branch's decision was frozen at step-2 capture time.
In Sequential mode this would lock the captured graph to whichever
boundary state happened to hold at step 2, breaking subsequent file-
boundary detection — violating pearl_no_host_branches_in_captured_graph.

Fix: track the boundary state at capture time as a new
`train_graph_boundary_state: Option<bool>` field. On each step_batched,
if the current `last_seen_file_boundary` diverges from the captured
state, drop the cached graph and re-capture on the next step.

- Random mode: gate is unconditionally true; state never diverges;
  zero recaptures.
- Sequential mode: ~8 recaptures per epoch (one per file boundary).
  Each recapture costs the same as the original step-2 capture (~50ms).
  Negligible.

Verified clean cargo check -p ml-alpha --all-targets.
2026-05-21 11:39:29 +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
b5bed9f805 fix(crt-train): sqrt K-ratio in λ controller (tames h6000 bombardment)
Local trace at base_lambda=0.1 showed the linear ratio
{1, 0.3, 0.1, 0.03, 0.005} = HORIZONS[0]/HORIZONS[h] gave h6000 a
200x stronger target-undershoot signal than h30. The controller
slammed h6000 with smoothness gradient (peak λ[h6000]≈60) while h30
saw nearly none. h6000 val_auc collapsed to 0.514 (near-random)
while h100/h300 improved.

Replace with sqrt(HORIZONS[0]/HORIZONS[h]) = {1, 0.548, 0.316, 0.173,
0.0707}. Caps the differential at ~14x. Verified locally at base=0.1:
- h6000 val_auc recovered to 0.599 (vs 0.514 collapse, vs 0.621 no-smooth)
- jitter ratio h6000/h30 = 0.51 (meaningful differentiation, was 0.84 unforced)
- λ[h6000] equilibrium = 0.7-3 (was 4-60 with linear ratio at same base)

The design target (h6000 jitter = 7% of h30) is less aggressive than
linear (was 0.5%) but produces a tractable training equilibrium that
preserves h6000 predictive capacity. Both 500-step local AND the full
40k-step L40S retrain will tell us where it lands at scale.
2026-05-21 09:14:49 +02:00
jgrusewski
cd0fe8549c fix(gpu-log): use std::thread for drain task (no tokio runtime needed)
The drain task previously used tokio::spawn + tokio::time::sleep,
gated by Handle::try_current().is_ok(). alpha_train's main() is
synchronous (not #[tokio::main]) so Handle::try_current() returned
Err and the drain task was silently skipped — the trace file was
never created.

Refactor to std::thread::spawn + std::thread::sleep. No runtime
required; spawn is unconditional; Drop joins synchronously instead
of aborting; BufWriter flushes on every drain cycle so even short
runs (< 1 sec) capture their tail. Existing gpu_log_ring_invariants
tests migrated from #[tokio::test] back to plain #[test].

Also resolves an exit-1 issue on alpha_train shutdown — likely a
tokio task abort panicking through the synchronous Drop path.

Local smoke (alpha_train --kernel-step-trace ...):
- EXIT=0
- step_trace.jsonl: 1497 lines (~500 steps × 3 smoothness records)
- valid JSONL, schema matches smoothness_controller emission contract
2026-05-21 02:08:55 +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
fa41b39ca4 refactor(gpu-log): rename feature cuda-diag-log → kernel-step-trace
Per user direction: feature-gating is appropriate for per-step diagnostic
logging (real perf/memory cost) but the name should be specific to the
mechanism, not a generic "diag-log" catch-all. kernel-step-trace
describes what the feature provides — per-step records written to the
ring by kernels.

Pure rename, no behavioral changes. Touched: Cargo.toml feature decl,
lib.rs cfg attribute, perception.rs (~25 cfg attributes including
not(feature) pairs), gpu_log.rs + smoothness_lambda_controller.cu doc
comments, gpu_log_ring_invariants.rs file-level cfg + ignore-attribute
text.
2026-05-21 01:44:29 +02:00
jgrusewski
adc3506d3e feat(gpu-log): wire smoothness_lambda_controller as first ring producer
Atomic refactor wiring the GPU log ring's first producer end-to-end:

- cuda/gpu_log_helpers.cuh (new): extract LogHeader/LogRecord/LogRing
  struct definitions + the log_record() device __forceinline__ helper
  into a shared header. Single source of truth — other kernels include
  this rather than redefining the structs.
- cuda/gpu_log_ring.cu: refactor to include gpu_log_helpers.cuh; retain
  only the gpu_log_tick kernel.
- cuda/smoothness_lambda_controller.cu: include gpu_log_helpers.cuh, add
  trailing (LogRing*, const int*) args, capture pre-EMA jitter +
  post-EMA jitter + target + excess_ratio into shared mem, and emit
  three records per call (RT_INPUT, RT_STATE, RT_OUTPUT) gated on
  non-null ring pointer.
- trainer/perception.rs: feature-gated (cuda-diag-log) ring allocation +
  step counter (mapped-pinned host shadow), gpu_log_tick launch FIRST
  inside the captured graph, two new pointer args on the smoothness
  controller launch (null when feature off), step-counter DtoD shadow
  alongside the other telemetry shadows, drain task spawn (skipped when
  no tokio runtime — sync tests still construct the trainer), and a
  feature-gated Drop impl that aborts the drain task on shutdown.
- tests/smoothness_lambda_controller_invariants.rs: pass null pointers
  for the two new kernel args; the kernel's nullptr guard preserves
  pre-existing behaviour.
- build.rs: rerun-if-changed on cuda/gpu_log_ids.h and
  cuda/gpu_log_helpers.cuh so header edits trigger cubin rebuilds.

Verified: cargo build / check --all-targets clean both with and without
the cuda-diag-log feature; 4/4 smoothness controller invariant tests
pass; 9/9 perception_overfit integration tests pass under the feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 01:36:18 +02:00
jgrusewski
17cddf7f4a test(gpu-log): invariant tests for ring allocator + drainer 2026-05-21 01:21:55 +02:00
jgrusewski
339eaca983 feat(gpu-log): Rust-side LogRing, drain task, decoder registry
Task 5 of the GPU log ring plan. Adds `gpu_log` module behind the
`cuda-diag-log` feature gate:

- `LogRecord` (#[repr(C)], 64 B) mirroring the C-side layout in
  `gpu_log_ring.cu`; const compile-time size assertion catches drift.
- `LogRing::alloc()` allocates and zero-initialises a 32k-slot
  mapped-pinned ring (2 MiB pinned).
- `alloc_step_counter()` allocates the device-side i32 step counter.
- Decoder dispatch table with per-(kid, rt) decoders for
  `KID_SMOOTHNESS_CONTROLLER` × {RT_INPUT, RT_STATE, RT_OUTPUT}
  emitting structured tracing::info! events.
- `spawn_drain_task()` polls a mapped-pinned step-counter shadow at
  500 ms cadence, walks new slots, validates magic + step, dispatches
  to decoders. Emits tracing::warn! when the drainer falls behind the
  ring window (no silent record drops).

Kernel ID + record-type constants are a manual mirror of
`crates/ml-alpha/cuda/gpu_log_ids.h`; drift is caught by the const
size assertion at compile time and (subsequently) by build.rs.
2026-05-21 01:16:55 +02:00
jgrusewski
b1eaef5db2 refactor(gpu-log): generic MappedRecordBuffer<T>; MappedF32Buffer as alias 2026-05-21 01:11:22 +02:00
jgrusewski
92841bc5aa feat(gpu-log): add cuda-diag-log Cargo feature flag 2026-05-21 01:07:30 +02:00
jgrusewski
12e059c6ab feat(gpu-log): add gpu_log_ring.cu device helpers + tick kernel 2026-05-21 01:06:54 +02:00
jgrusewski
cd94a34122 feat(gpu-log): add gpu_log_ids.h — single source of truth for ring IDs 2026-05-21 01:04:50 +02:00
jgrusewski
0f664cf361 test(crt-train): invariant tests for smoothness_lambda_controller 2026-05-21 00:34:06 +02:00
jgrusewski
29ab397689 feat(crt-train): wire ISV-driven λ controller; remove SMOOTHNESS_LAMBDA_RATIO 2026-05-21 00:30:47 +02:00
jgrusewski
011a6b3cf2 build(crt-train): register smoothness_lambda_controller.cu in build.rs 2026-05-21 00:25:26 +02:00
jgrusewski
19c24d7f5a feat(crt-train): add ISV-driven smoothness_lambda_controller kernel 2026-05-21 00:23:45 +02:00
jgrusewski
70ecfc0fe6 feat(crt-train): plumb --smoothness-base-lambda through alpha-perception template 2026-05-20 23:55:15 +02:00
jgrusewski
60d0b62b8d feat(crt-train): add --smoothness-base-lambda CLI flag + telemetry 2026-05-20 23:51:20 +02:00
jgrusewski
c9aa0c2a98 feat(crt-train): launch output_smoothness kernel after BCE in step_batched 2026-05-20 23:45:51 +02:00
jgrusewski
4a32186a5f fix(crt-train): migrate all PerceptionTrainerConfig literals to new smoothness_base_lambda field
Task 5 added smoothness_base_lambda to PerceptionTrainerConfig but only
updated the struct definition + Default impl. Eight test sites in
perception_overfit.rs and one site in alpha_train.rs construct the
config via explicit field list (no ..Default::default() spread), so
they broke with E0063 missing-field errors under cargo check
--all-targets.

Per feedback_no_partial_refactor.md: when a contract changes, every
consumer migrates atomically. This commit adds smoothness_base_lambda:
0.0 to all 9 sites so the workspace compiles cleanly. The
alpha_train.rs value of 0.0 is a placeholder — Task 7 will replace it
with cli.smoothness_base_lambda once the CLI flag is added.
2026-05-20 23:40:45 +02:00
jgrusewski
b83c14cf44 feat(crt-train): allocate smoothness buffers + upload λ in PerceptionTrainer 2026-05-20 23:38:33 +02:00
jgrusewski
3e07bf41b8 test(crt-train): finite-diff validate output_smoothness kernel grad 2026-05-20 23:31:40 +02:00
jgrusewski
ac7c52b5be feat(crt-train): add SMOOTHNESS_LAMBDA_RATIO const in heads.rs 2026-05-20 23:27:18 +02:00
jgrusewski
a342126543 polish(crt-train): branchless boundary loads + remove misleading factor==0 comment
Code-quality reviewer flagged two non-blocking polish items in the
output_smoothness kernel's backward pass:

- I1: thread-divergent if-gated loads contradicted the file's stated
  branchless-design intent. Fixed via clamp-then-load: at k=0 / k=K-1
  read self for the spurious neighbour; lm/rm multipliers zero the
  contribution. Every lane executes both loads; no divergence on
  boundaries.
- I2: the "Skip work when factor == 0" comment described a non-feature
  (no such branch existed). Removed.

Behaviour-equivalent change — same loss and same gradient at every
position; the change only affects warp-execution patterns at the K=0
and K=K-1 strips.
2026-05-20 23:24:33 +02:00
jgrusewski
3e1d66f865 build(crt-train): register output_smoothness.cu in ml-alpha build.rs KERNELS 2026-05-20 23:22:45 +02:00
jgrusewski
bf5ecd109b feat(crt-train): add output_smoothness CUDA kernel (forward + analytic grad) 2026-05-20 23:16:17 +02:00
jgrusewski
faa9a73c10 spec(crt-train): output smoothness retraining intervention (intervention A)
Per project_crt_diag_findings.md — CRT.diag + diag.2 empirically
falsified all three hypotheses about why the model's per-event output
doesn't track horizon-specific dynamics. Labels ARE differentiated per
horizon, AUC=0.66 IS per-event measured, but per-event predictions
behave identically across all 5 horizons (2.5-event mean run length
raw, 3.3-event after aggressive Wiener-α EMA smoothing). h6000
predictions should change every thousands of events, not every 3.3.

Diagnosis: training dynamics issue. The BCE loss provides no signal
pushing toward horizon-coherent predictions. Adjacent-event labels at
horizon K share K-1/K of the forward window so SHOULD produce similar
predictions, but the loss doesn't require this.

Intervention A (this spec, minimum-scope): output smoothness
regularizer
  L_smooth[h] = λ[h] × mean_t (p[h](t) - p[h](t-1))²

With horizon-weighted λ (stronger for h6000 than h30): forces h6000
predictions to change slowly while leaving h30 responsive.

Implementation surface:
  - multi_horizon_heads.cu backward: extend grad_probs with smoothness
    gradient
  - perception.rs trainer step: prev_probs_d buffer, per-horizon (p[t]
    - p[t-1])² accumulation
  - heads.rs: SMOOTHNESS_LAMBDA constant per horizon
  - alpha_train.rs: --smoothness-base-lambda CLI flag

Validation gate (CRT.train Gate):
  MUST: per-horizon AUC ≥ baseline - 0.02 (no signal destruction)
  WIN: h6000 mean_run_len ≥ 100 events; h6000/h30 ratio ≥ 10× (horizons
       actually differentiated post-training)
  STRETCH: CRT.1 controller smoke shows mean PnL CORRELATED with
       conviction (vs anti-correlated in lnfwd)

Future work if intervention A doesn't pass WIN:
  B: horizon-conditional output structure (architecture change)
  C: curriculum on horizon (multi-day training)
  D: different label generation (majority-vote vs single-point)

Status: Design — awaiting user review before plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:59:49 +02:00
jgrusewski
0e17fd4f2d diag(crt-2): per-horizon alpha-input EMA test — hypothesis investigation
Driven by ffr59 (commit b44a97ff9) findings: all 5 horizons flip
direction every 2.5 events; win rate flat 24% across conviction; mean
PnL anti-correlated with conviction. Hypothesis: per-event alpha output
is high-frequency noise on top of a slower signal. If true, smoothing
input alpha BEFORE the conviction formula should reduce direction flips
and recover signal.

Adds Wiener-α adaptive EMA on raw alpha_probs[h] for each horizon,
applied BEFORE the multi-horizon conviction formula. Floor at 0.1
(stronger than the 0.4 floor on the output-side conviction EMA — this
tests whether INPUT smoothing has different impact than OUTPUT smoothing).

Three new device slots:
  - alpha_ema_per_b_per_h (per-horizon EMA state)
  - alpha_diff_var_per_b_per_h (variance of changes)
  - alpha_sample_var_per_b_per_h (variance of value)

The CRT.diag Group A direction-flip counter still reads RAW alpha_probs
so we have a head-to-head comparison: raw flip rate vs smoothed flip rate.
Group E adds the smoothed-direction counter + mean run length.

End-of-run log adds one line per horizon:
  crt_diag h<X> smoothed: flips=Y mean_run_len=Z events (vs raw F / M)

If smoothed mean_run_len >> raw mean_run_len: hypothesis is RIGHT, the
input had signal under the noise. Next step would be to make this an
operational EMA in the controller.

If smoothed and raw are similar: hypothesis is WRONG, per-event output
is genuinely noisy. Next step would be to investigate model training
(horizon collapse) OR the AUC=0.66 measurement definition.

Either way, definitive result from one smoke run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:34:35 +02:00
jgrusewski
b44a97ff94 diag(crt): empirical measurement battery for signal × controller × cost analysis
Pure-instrumentation commit. Zero behavior change. The Gate CRT.1 smoke
p9cnk (commit 1e656948b) produced 222k trades and 29688% drawdown,
worse than the failed CRT.A. The structural fixes (multi-horizon
conviction, no-trade band, composite exit) all work as designed — but
the trade cadence is fundamentally too fast for the signal/cost ratio.
Before another round of threshold tuning, measure the structural truth.

Adds device-side counters dumped at smoke close (single end-of-run
memcpy_dtoh batch in read_diagnostics(), NOT per-event):

Group A — per-horizon signal persistence:
  - flip_count[h]:        total direction-sign flips
  - sum_run_length[h]:    cumulative events between flips (u64)
  - run_length_hist[h]:   5-bucket log-scale histogram (1-9, 10-99,
                          100-999, 1k-9.9k, 10k+)
  Output: mean_run_len per horizon = signal half-life proxy

Group B — conviction smoothed-EMA distribution:
  - conv_hist[10]:        10-bucket histogram over [0, 1)
  Output: shape of conviction distribution at decision time

Group C — hold-time distribution:
  - hold_hist[6]:         <1s, 1-10s, 10-60s, 1-10m, 10-60m, >1h
  Output: empirical distribution of trade hold times

Group D — outcome by entry-conviction:
  - outcome_n[10]:        trades per conviction bucket
  - outcome_sum_pnl[10]:  cumulative realized PnL per bucket (price-units)
  - outcome_n_wins[10]:   wins per bucket
  Output: validates "high conviction = better trades" hypothesis OR
          surfaces signal miscalibration

Per-backtest memory: ~330 bytes. Negligible vs existing state.

All counters single-writer-per-block (threadIdx.x == 0 convention; no
atomicAdd per feedback_no_atomicadd). All updates kernel-internal (no
host roundtrip per event). The end-of-run dump uses memcpy_dtoh in
read_diagnostics() — called once at harness termination from the
post-stop_ctrl_counters block, never per-event. Per
feedback_no_htod_htoh_only_mapped_pinned, the hot path is untouched.

N_HORIZONS = 5 (heads.rs canonical {30, 100, 300, 1000, 6000}); the
plan text says 4 but the workspace constant is 5 — used the code value.

Output at end of cluster smoke as a `crt_diag` log block:
  crt_diag h30: flips=X mean_run_len=Y events
  crt_diag h30 run_length_hist: 1-9:N 10-99:N 100-999:N ...
  ...
  crt_diag conv_ema_hist: [0.0-0.1]:N [0.1-0.2]:N ...
  crt_diag hold_time_hist: <1s:N 1-10s:N ...
  crt_diag outcome_by_entry_conv[0.0-0.1]: n=N win_rate=X.X% mean_pnl_pu=Y.YYY
  ...

The output drives the next round of CRT.1 threshold design (delta_floor,
composite exit factors, min-hold cooldown) from data instead of guesses.

Per pearl_adaptive_not_tuned and pearl_controller_anchors_isv_driven:
thresholds will become ISV-derived in CRT.2; CRT.1 must first establish
defensible defaults from the empirical signal characteristics.

Verified: stop_controller (22/22 pass), decision_floor_coldstart (3/3
pass), threshold_and_cost (3/3 pass), parallel_sim_correctness (1/1
pass), lob_sim_integrated_fuzz (3/3 pass). Two lob_sim_fixtures
failures (fix_decision_alpha_buy_close, fix_decision_program_h4_only)
were pre-existing at HEAD 1e656948b — not caused by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:56:07 +02:00
jgrusewski
1e656948b3 arch(crt-1): composite exit_signal safety circuit-breaker (C1.4)
Per spec §4.3 + plan C1.4. Safety backup for the primary exit path
(target → 0 from collapsed multi-horizon conviction, C1.2). Catches
the edge case where conviction stays bounded above zero but the trade
is deep in unrealized loss, or short/long horizons disagree for many
events while threshold-gate logic still permits sizing.

Three terms, max-of-three composite:
  degradation_term  = (1 − conv_ema_now / conviction_at_entry) / 2.0
  disagreement_term = disagree_consecutive_events / 32.0
  pnl_decay_term    = max(−pnl_adjusted_conv_ema, 0) / 1.0
  exit_signal       = max(deg, dis, pnl_decay)

Fires force-flat (market_targets = (3, 0)) when exit_signal >= 1.0.

State-update flow:
  - pnl_track open branch: write conviction_at_entry (offset 20),
    conviction_ema (offset 24, initialised to entry value),
    pnl_adjusted_conviction_ema (offset 44, = 0),
    peak_unrealized_pnl (offset 48, = 0). Takes new conviction_ema_per_b
    read-only arg to snapshot at open.
  - decision kernel intra-event (when pos open): update offsets
    24/44/48/56 (disagreement counter) via update_open_trade_trajectory.
  - composite_exit_check device helper: reads offsets 20/24/44/56,
    writes (3, 0) on fire. Sibling of stop_check_isv with same return
    semantics; called after trajectory update so all four reads see
    the freshly-written values.

Fields not read in CRT.1 (offsets 28-43 per-horizon EMA, 52 degradation
counter, 60 horizon_idx_dominant) are NOT written by the open branch —
alloc_zeros covers init. Per feedback_no_hiding: don't populate unused
fields. Also fixes a latent C1.1 bug: pnl_track close branch read
horizon_idx from offset 20 (now conviction_at_entry, f32); moved the
read to offset 60 per the documented layout.

Threshold-bootstrap defaults (will be ISV-derived in CRT.2 alongside
the rest of the controller anchors per pearl_controller_anchors_isv_driven):
  degradation_factor  = 2.0  (50% conviction decay vs entry → ≤0.5 term)
  disagreement_factor = 32.0 (32 events of short/long disagreement)
  pnl_decay_factor    = 1.0  (pnl_adj_conv structurally ∈ [-1, +1])

Under these defaults disagreement_term is the only term that can fire
on its own — degradation_term caps at 0.5 (conv_ema/conviction_at_entry
≥ 0) and pnl_decay_term caps at 1.0 in the limit. The three terms still
add evidence in superposition; CRT.2 will retune factors against ISV
percentiles. Unit-test coverage validates the disagreement path
(composite_exit_signal_fires_on_disagreement).

Per pearl_no_host_branches_in_captured_graph — all composite logic
lives inside the decision kernel; no host roundtrip in the per-event
hot path. Per feedback_no_htod_htoh_only_mapped_pinned — no new
memcpy or stream.synchronize() in step_pnl_track or
dispatch_latent_market_orders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:15:40 +02:00
jgrusewski
84793ea46e fix(crt-1): add delta_floor: 0.0 to 3 missing test UniformSimParams initializers
The C1.3 commit 7c850a6c0 missed three test files that construct
UniformSimParams literals. cargo check reported E0063 missing-field
errors. Added delta_floor: 0.0 (band disabled, preserves pre-C1.3
behavior in tests) to:
  - parallel_sim_correctness.rs
  - decision_floor_coldstart.rs
  - lob_sim_integrated_fuzz.rs

Per feedback_no_partial_refactor: the atomic refactor must include
every consumer in one logical change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:01:44 +02:00
jgrusewski
7c850a6c02 arch(crt-1): no-trade band in seed_inflight — sparse action at signal cadence (C1.3)
Per spec §4.0 + plan C1.3. Without this gate, every fractional change
in target_lots seeds an order via target-delta semantics; combined with
the continuous controller invocation (every event after A1) this
generates hyperactivity even with the multi-horizon §4.4 conviction
formula (C1.2). v2 Gate-1 failures confirmed: scalar EMA + amplification
clamp + per-event controller = 155k trades on a 2M-event smoke.

The band absorbs fractional target adjustments so most events produce
no order. When the signal genuinely shifts and target moves enough to
cross the band, the kernel seeds. Continuous evaluation, sparse action.

Greenfields atomic refactor — single config knob threaded through every
layer in one commit:
  SweepBase.delta_floor: f32  (YAML, default 1.0 = 1 lot)
  SimVariant.delta_floor: Option<f32>  (per-variant override)
  ResolvedSimVariant.delta_floor: f32
  UniformSimParams.delta_floor: f32
  BatchedSimConfig.delta_floor: Vec<f32>
  LobSimCuda.delta_floor_d: CudaSlice<f32>
  seed_inflight_limits_batched kernel param + skip-if-below-floor logic

New accessor: LobSimCuda::read_inflight_count(b) — counts active != 0
limit slots for backtest b. Not cfg(test); follows existing read_limit_slot
pattern.

Test: no_trade_band_blocks_micro_delta verifies that a second decision
with the same strong-bullish alpha (same target, effective = in-flight
lots) produces delta=0 and does not re-seed. max_lots=1 keeps the
arithmetic unambiguous.

Existing tests: all UniformSimParams struct literals updated with
delta_floor=0.0 (band disabled) so existing behaviour is preserved.
harness.rs from_uniform path uses delta_floor=1.0 (production default).

Hot-path discipline: the delta_floor_d upload happens once per run in
the existing config-upload block (alongside threshold, cost, max_hold_ns);
the kernel reads the device slot per-event without any host roundtrip. No
memcpy_htod / dtoh / dtov / synchronize introduced on the per-event path.

Per pearl_controller_anchors_isv_driven, this floor is currently a
config constant; CRT.2 (Phase 2) makes it ISV-derived from rolling
spread cost / signal volatility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:59:50 +02:00
jgrusewski
632296021c arch(crt-1): multi-horizon ISV-weighted conviction replaces scalar EMA + rescale (C1.2)
Per spec §4.4 + plan C1.2. Replaces v2 A2 (1d889d2de) + A2.1
(fe2498769) scalar approach that failed Gate 1 catastrophically on
smokes vjmwc and lkrdf (155k trades, 9100% drawdown, Sharpe -15.7).

The structural fix: direction comes from per-horizon SIGNED sum, so
disagreeing horizons cancel. The scalar EMA on max(|p-0.5|) could only
smooth magnitude — it could not smooth direction jitter. Only this
multi-horizon weighted formula can.

Formula (spec §4.4):
  weight_h           = max(isv[h].net_edge, ε) / (isv[h].var + cost²)
  weighted_h         = magnitude_h × weight_h × direction_h
  conviction_signed  = sum(weighted_h) / sum(|weight_h|)
  conviction_ema     = Wiener-α EMA(|conviction_signed|)   (reuses A2 slots)
  target_lots        = round(sign(conviction_signed) × conviction_ema × max_lots)

Pearl conformance:
  - pearl_controller_anchors_isv_driven: weights from ISV state
  - pearl_one_unbounded_signal_per_reward: net_edge/(var+cost²) is the
    single unbounded factor; magnitude × direction is bounded in [-1,1]
  - pearl_zscore_normalization_for_magnitude_asymmetric_signals: divide
    by total_abs_weight for scale invariance
  - pearl_trade_level_vol_for_stop_distance: cost² is variance bootstrap
  - pearl_blend_formulas_must_have_permanent_floor: ε = cost · 0.01 on
    net_edge — no cold-start regime switch
  - pearl_audit_unboundedness_for_implicit_asymmetry: net_edge is
    one-sided positive (losing-edge horizons drop out, not flip sign)
  - pearl_wiener_alpha_floor_for_nonstationary: α floor at 0.4 (unchanged
    EMA mechanism, repointed at the multi-horizon scalar)

DELETED:
  - The `final_size *= (conv_ema/raw_max_conv)` aggregate rescale step
    (the v2 bug that amplified weak signals)
  - The per-horizon `signed_sizes[h]` Kelly + Sharpe-weight aggregation
    loop in decision_policy_default (replaced by direct §4.4 formula)
  - Three obsolete tests asserting on the old scalar path:
    * conviction_ema_smooths_micro_oscillations
    * conviction_ema_does_not_lag_reversals
    * conviction_ema_rescale_never_amplifies_weak_signal
    * cfg_high_kelly_floor helper (only used by deleted tests)
  - cold_start_with_zero_floor_reproduces_old_bug (tested OLD kelly_floor
    semantics that v3 doesn't expose)
  - cold_start_persistent_bullish_now_closes (was passing under v2 via
    target oscillation from integer rounding; v3 produces stable target
    on stable signal — closes need price movement)
  - alpha_noop_side_2_preserved (anchored on the v2 kelly_frac_floor
    integer-truncation path that doesn't exist under v3)

Applied to BOTH decision_policy_default and decision_policy_program
(bytecode VM at OP_WRITE_ORDER). Per feedback_no_partial_refactor —
both consumers migrate atomically. The VM's per-horizon emit + aggregate
opcodes still execute but their stack `final_size` is unused at
OP_WRITE_ORDER (the §4.4 conviction-driven target replaces it). The
stack's attribution mask is still consumed for entry crediting.

The three conviction-EMA device slots (conviction_ema_d, _diff_var,
_sample_var) STAY in LobSimCuda. The Wiener-α EMA mechanism is reused
on the multi-horizon conviction scalar; the device-helper signature
changed from taking alpha_probs[] to taking the precomputed scalar
|conviction_signed|.

Kernel ABI change: decision_policy_default no longer takes
target_annual_vol_units / annualisation_factor / kelly_frac_floor /
sharpe_weight_floor (the §4.4 formula doesn't use them). Removed from
both the CUDA signature and the launch builder in sim/mod.rs. The
bytecode VM (decision_policy_program) still consumes those params for
its OP_EMIT_PER_HORIZON_SIZE / OP_AGG_WEIGHTED_SHARPE opcodes — their
device slots remain allocated.

Test: multi_horizon_conviction_cancels_on_disagreement verifies the
structural fix — bullish [0.7,0.3,0.7,0.3,0.5] horizons cancel to
no-trade output (side=2/3, size=0, conv_ema≈0).

Existing tests rebased: cfgs that used cost_per_lot_per_side=0.0 now
use 1.0 (the §4.4 formula's eps_edge floor requires cost > 0). Each
affected test file documents the rebase inline. Tests that were
anchored on observed v2-kernel values (per
pearl_tests_must_prove_not_lock_observations) are deleted; tests with
genuine invariants (boundedness, direction-sanity, stop-controller
behavior) are preserved.

Hot-path discipline: no memcpy_htod/dtoh/dtov/synchronize introduced.
Only kernel-internal computation; one launch arg removed from the
default decision kernel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:50:43 +02:00
jgrusewski
e27fc90b9b arch(crt-1): open_trade_state 24→64 byte expansion — atomic refactor (C1.1)
Per spec §7 (v3 architecture reset, promoted from v2 Phase B into CRT.1).
Single-cache-line layout. Every reader and writer of open_trade_state
migrates in this commit per feedback_no_partial_refactor.

New 64-byte layout (existing 24-byte fields preserved at original offsets):
  Offset  Size  Field
    0     8     entry_ts_ns                              (u64)
    8     4     entry_px_x100                            (i32)
   12     4     entry_size                               (i32)
   16     4     realized_at_open                         (f32)
   20     4     conviction_at_entry                      (f32)  ← NEW
   24     4     conviction_ema                           (f32)  ← NEW
   28    16     conviction_per_horizon_ema [4 × f32]    ← NEW
   44     4     pnl_adjusted_conviction_ema              (f32)  ← NEW
   48     4     peak_unrealized_pnl                      (f32)  ← NEW
   52     4     degradation_consecutive_events           (u32)  ← NEW
   56     4     disagreement_consecutive_events          (u32)  ← NEW
   60     1     horizon_idx_dominant_at_entry            (u8)   ← NEW
   61     3     pad

The 24-byte prefix is unchanged so downstream readers (stop_check_isv in
decision_policy.cu, max_hold event-rate check in resting_orders.cu) need
only update the stride constant. The new fields at offsets 20..63 are
populated by subsequent CRT.1 tasks (C1.2 multi-horizon conviction, C1.4
composite exit signal).

Open branch in pnl_track.cu zeros the new fields implicitly because
alloc_zeros on the device slot zero-initialises everything; subsequent
writes only touch the 0-23 byte range (existing fields). Close branch
reset-loop iterates OPEN_TRADE_STATE_BYTES which now zeros all 64.

Files changed:
  crates/ml-backtesting/src/lob/mod.rs  — pub const OPEN_TRADE_STATE_BYTES: usize = 64
  crates/ml-backtesting/cuda/pnl_track.cu  — #define OPEN_TRADE_STATE_BYTES 64
  crates/ml-backtesting/cuda/resting_orders.cu — comment + stride literal 24→64
  crates/ml-backtesting/tests/stop_controller.rs — open_trade_state_64_byte_layout test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:25:05 +02:00
jgrusewski
7ae0a8d461 plan(crt-1): unified inventory controller implementation plan
Per spec v3 (commit 9ad76c4df). Supersedes the v2 Phase A plan
(2026-05-20-crt-phase-a-continuous-controller.md) which is now an
artifact — its load-bearing commits (A0.5 forward_step, A1
decision_stride deletion) are preserved; its A2 scalar conviction-EMA
work is REPLACED by the multi-horizon §4.4 formula here.

Six tasks (all under one Gate CRT.1, no inter-task gates):
  C1.1 open_trade_state 24→64 byte atomic refactor (spec §7)
  C1.2 multi-horizon ISV-weighted conviction (spec §4.4) replacing
       scalar EMA approach
  C1.3 no-trade band in seed_inflight (delta_floor config field)
  C1.4 composite exit_signal safety circuit-breaker (spec §4.3)
  C1.5 local compile + tests
  C1.6 cluster smoke + Gate CRT.1 validation

Gate CRT.1 acceptance = v2 §9 Gate 2 tiered MUST/WIN structure intact.

What ships in this plan:
  - Continuous evaluation (every event, via existing forward_step)
  - Multi-horizon ISV-weighted conviction (one unbounded factor =
    net_edge / (var + cost²); rest bounded; per pearl_one_unbounded_signal)
  - target_lots = direction × |conviction_ema| × envelope_max (no aggregate
    rescale; the v2 A2/A2.1 bug is structurally absent)
  - No-trade band: kernel skips seed when |target − effective| < delta_floor
  - open_trade_state 64-byte expansion for per-trade trajectory tracking
  - Composite exit_signal as safety circuit-breaker (primary exit is
    emergent target→0)

What stays out of scope (CRT.2 / CRT.3):
  - Adaptive max_lots / threshold / vol target (CRT.2)
  - Self-tuning percentile gate (CRT.2)
  - LoRA + EWC++ + shadow eval (CRT.3)

Status: ready for execution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:18:45 +02:00
jgrusewski
9ad76c4dfe spec(crt): v3 architecture reset — collapse Phase A+B into CRT.1 unified controller
Empirical trigger: Phase A as designed in v2 failed Gate 1 catastrophically
on both smoke vjmwc (commit 3d8f12deb) and lkrdf (commit fe2498769 with
amplification clamp). Both produced ~155k trades (62× baseline), 9100%
max-drawdown (175× baseline), and Sharpe -15.7. The clamp had effectively
zero impact on the outcome, confirming the bug is structural not formulaic.

Architectural finding: the v2 phase split (A: continuous control / B:
signal-driven position management) is artificial. They are the same
mechanism. A scalar EMA on max(|p-0.5|) cannot smooth direction jitter
between horizons; only the multi-horizon ISV-weighted conviction formula
(v2 §4.4, deferred to Phase B in v2) is structurally coherent.

User-validated mental model: signal vector at any moment IS a forward
prediction of the trade trajectory; optimal position = inventory implied
by current beliefs; trade actions are sparse because most events
produce only fractional adjustments to a stable optimum. Continuous
evaluation, discrete action.

v3 phase structure (replaces v2 A/B/C/D):
  CRT.1  Unified Inventory Controller (was A+B merged)
  CRT.2  Adaptive Risk Envelope (was Layer C, unchanged)
  CRT.3  Online Weight Adaptation (was Layer D, unchanged)

CRT.1 components:
  - forward_step every event (already shipped: A0.5)
  - decision_stride deleted (already shipped: A1)
  - Multi-horizon ISV-weighted conviction (§4.4 promoted from Phase B)
  - Wiener-α EMA on multi-horizon conviction (§4.2 promoted)
  - target_lots = direction × |conviction_ema| × envelope_max
  - No-trade band in seed_inflight: skip if |delta| < delta_floor (NEW)
  - open_trade_state 24→64 expansion (§7 promoted from Phase B)
  - Conviction-degradation exit emergent from target→0; composite-signal
    safety layer (§4.3) preserved as circuit-breaker

What survives from v2 commits on the branch:
  a0e81fbdf forward_step incremental SSM (A0.5)
  92f8b10ed forward_step_into eliminates GPU↔CPU round-trip
  045850e8f delete decision_stride field (A1)
  3d8f12deb buffer-level seed bit-identity test

What gets superseded in CRT.1 implementation:
  1d889d2de A2 scalar conviction-EMA with aggregate rescale — replaced
            by multi-horizon §4.4 formula
  fe2498769 A2.1 clamp on broken rescale — same reason

The three conviction-EMA device slots (conviction_ema_d et al.) STAY
in LobSimCuda — the EMA mechanism is reused on the multi-horizon
conviction scalar. The `final_size *= scale` rescale step is deleted.

Gate restructure:
  v2 Gate 1 (Layer A standalone) — REMOVED
  v2 Gate 2 (Layer B) — PROMOTED as Gate CRT.1
  v2 Gate 3/4 — unchanged as CRT.2/CRT.3

Status: Design v3 — awaiting plan rewrite before implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:15:28 +02:00
jgrusewski
fe24987690 fix(crt-a2.1): clamp conviction-EMA rescale to [0, 1] — never amplify weak signals
Gate 1 catastrophic failure (smoke vjmwc, commit 3d8f12deb):
  - 62× hyperactivity (157,470 trades vs 2,511 baseline)
  - 9,347% max-drawdown ($327M loss on $3.5M base)
  - Sharpe -15.63 (2.4× worse than baseline)

Root cause: A2's formula
    final_size *= (conv_ema / raw_max_conv)
amplified weak signals because EMA tracks the MEAN of raw_max_conv, NOT a
running max as the author's comment claimed. When raw_max_conv < conv_ema
(normal during quiet periods between strong signals), the multiplier was
> 1, blowing up small-signal positions:
  raw=0.05, ema=0.3 → 6× amplification
  raw=0.01, ema=0.3 → 30× amplification

Fix: clamp scale ∈ [0, 1] in BOTH decision_policy_default and
decision_policy_program (OP_WRITE_ORDER). Only damp when raw is stronger
than EMA (scale < 1); never amplify when raw is weaker (scale capped at 1).

Math:
  scale = conv_ema / raw_max_conv     // can be 0..∞
  scale_clamped = min(scale, 1.0)     // can only be 0..1
  final_size *= scale_clamped

Test: conviction_ema_rescale_never_amplifies_weak_signal validates
target_lots stays ≤ 1 when alpha=0.51 (raw_conv=0.02) after EMA warmup
at alpha=0.7 (ema~=0.4). Without the clamp the broken multiplier is 20×.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 19:38:50 +02:00
jgrusewski
3d8f12deba test(crt-a): buffer-level seed bit-identity replaces prediction-level test
The forward_step_bit_identical_after_seed_from_forward_only test in
commit 1d889d2de asserted 1e-5 prediction-level convergence between
forward_only(W[1..K+1]) and seed+forward_step(snap[K]). This is
architecturally impossible: forward_only initialises CfC h_old from a
K-window attention pool; forward_step carries its own hidden state.
Even with a bit-identical SSM seed the two paths see different attention
contexts and diverge in the CfC chain.

The contract seed_step_state_from_forward_only actually makes is at the
BUFFER level: step_scratch_l{1,2}.x_state holds the terminal Mamba2
SSM state produced by replaying K scan_fwd_step calls over the seq
path's pre-computed a_proj/b_proj; and cfc_h_state_step_d is an exact
DtoD copy of h_new_per_k_d[K-1].

Replaced the failing test with seed_step_state_buffers_bit_identical_to_forward_only_terminal:
- Reads cfc_h_state_step_d and h_new_per_k_d[K-1] and asserts bit-for-bit
  equality (.to_bits() == .to_bits()) — the DtoD copy makes this exact.
- Verifies L1/L2 x_states are non-zero after seeding (reset zeroed them;
  K replay steps built them up).
- Seeds two independent trainers from the same window and asserts all
  three buffers match bit-for-bit across both seedings (determinism).

Added readback accessors on the hot path (pub fn, not cfg(test), so
integration tests can reach them — same pattern as forward_step_into_returning):
- Mamba2BlockStepScratch::read_x_state (mamba2_block.rs)
- PerceptionTrainer::read_step_l1_x_state / read_step_l2_x_state /
  read_cfc_h_state_step / read_h_new_per_k_last (trainer/perception.rs)

The architectural divergence at prediction level is documented in the
replacement test's docstring so future readers don't reopen the same question.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:10:47 +02:00
jgrusewski
1d889d2de9 arch(crt-a): Wiener-α conviction-EMA smoothing in decision_policy
After A1 (commit 045850e8f) the controller fires every event. Without
smoothing, target lots would oscillate as alpha probabilities jitter
event-to-event, generating hyperactive spread-bleed from back-and-forth
trades.

Ships per spec §4.2 and §3.7: Wiener-α adaptive EMA on max-conviction-
across-horizons. Formula:
  α_raw    = diff_var / (diff_var + sample_var + ε)
  α_active = max(α_raw, 0.4)        per pearl_wiener_alpha_floor_for_nonstationary
  ema      = α_active × new + (1 − α_active) × prev
First-observation bootstrap: prev_ema == 0 → replace directly per
pearl_first_observation_bootstrap.

Three new device slots (per-backtest):
  - conviction_ema_d:             smoothed conviction (used for final-aggregate rescale)
  - conviction_diff_var_ema_d:    second-order EMA, drives adaptive α
  - conviction_sample_var_ema_d:  second-order EMA, drives adaptive α

Architectural choice: rescale at the final aggregate (final_size *= conv_ema /
raw_max_conv), not at per-horizon sig_mag. Per-horizon sizing keeps raw
|alpha-0.5|*2 so horizons with no signal (alpha=0.5) contribute zero —
the spec §4.2 "smoothed conviction" is a unified gain over the aggregate,
not a substitute for per-horizon signal strength. At bootstrap the scale
is exactly 1.0 (raw_max_conv == conv_ema) so the very first decision is
bit-identical to the pre-A2 kernel. Direction recovery (sign of
alpha-0.5) remains per-horizon — genuine reversals respond at event rate.

Threaded through both decision_policy_default and decision_policy_program
kernels' signatures and launches in sim/mod.rs.

Full multi-horizon conviction *aggregation* (spec §4.4) remains Phase B;
A2 ships ONLY the smoothing operator on the existing scalar conviction.

Pure on-device: no memcpy_htod / dtoh / dtov / synchronize in hot path.
Single test-only DtoH accessor (read_conviction_ema) for unit-test
inspection of the smoothed value.

Tests:
- conviction_ema_smooths_micro_oscillations — sentinel→bootstrap→bounded
  EMA across 10 alternating high/low all-bullish alpha drives; direction
  stable.
- conviction_ema_does_not_lag_reversals — sign flip in alpha → target
  side flips next event.
- All 22 stop_controller + 5 decision_floor_coldstart + 3 threshold_and_cost
  + parallel_sim + ring3_replay + trainer_parity + 6 fuzz tests pass.
- 2 lob_sim_fixtures tests (fix_decision_alpha_buy_close, fix_decision_program_h4_only)
  were already failing on baseline 045850e8f pre-A2; not a regression from
  this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:01:09 +02:00
jgrusewski
045850e8f3 arch(crt-a): delete decision_stride field — greenfields atomic refactor
Per spec 2026-05-20-continuous-reasoning-trader-design.md §3.3 and §8:
decision_stride is REMOVED, not deprecated. No backwards-compat shim,
no fallback. Every consumer migrates in this commit per
feedback_no_partial_refactor.

Removed from:
  - bin/fxt-backtest: RunArgs CLI flag, SweepBase field, SweepCell
    override field, default_decision_stride() function, all three
    BacktestHarnessConfig and RunArgs construction sites
  - crates/ml-backtesting/src/harness.rs: BacktestHarnessConfig field,
    MultiHorizonLoaderConfig decision_stride initializer, `let stride`
    local, `if event_count % stride == 0` gate around
    step_decision_with_latency; forward_step_into + step_decision now
    share a single window-full guard (merged into one `if` block)
  - crates/ml-alpha/src/data/loader.rs: MultiHorizonLoaderConfig field,
    next_sequence stride logic simplified to stride=1 (consecutive
    snapshots only)
  - crates/ml-alpha/src/trainer/perception.rs: PerceptionTrainerConfig
    field and Default impl; all four dt_s locals replaced with 1.0_f32
    (training K-loop, graph-capture K-loop, forward_step_into CfC step,
    eval K-loop)
  - crates/ml-alpha/examples/alpha_train.rs: CLI flag, trainer_cfg and
    both loader configs
  - crates/ml/examples/alpha_baseline.rs: CLI flag, train + eval stride
    gates replaced with unconditional read_all()
  - config/ml/*.yaml: decision_stride: lines removed from
    sweep_smoke, sweep_threshold_tuning, sweep_deployability,
    sweep_decision_stride_example (file repurposed as generic example)
  - tests: forward_step_golden, perception_overfit (×7 structs including
    the stride=4 smoke repurposed as a second convergence check),
    multi_horizon_loader (stride=4 spacing test repurposed as
    ts_ns monotonicity check), ring3_replay, trainer_parity

Harness loop now invokes BOTH forward_step_into AND
step_decision_with_latency on every event whenever the snapshot window
is full. forward_step_into advances SSM state and writes alpha_probs_d;
step_decision_with_latency reads alpha_probs_d immediately after —
no CPU roundtrip, no stride gate.

n_decisions ≈ events_processed - seq_len + 1 after this commit
(vs ~9999 at stride=200 in the S2 baseline).

cargo check --workspace: clean
cargo test -p ml-backtesting --lib: 33 passed
cargo test -p ml-alpha --lib: 33 passed (6 ignored)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:48:23 +02:00
jgrusewski
92f8b10ed2 fix(crt-a): forward_step_into eliminates GPU↔CPU roundtrip + bit-identical seed
Corrective commit on top of a0e81fbdf addressing two load-bearing issues
flagged in the prior DONE_WITH_CONCERNS report.

Issue 1 (USER-FLAGGED, primary): GPU↔CPU roundtrip per event.

The previous `forward_step` did GPU compute → memcpy_dtod to mapped-pinned
host buffer → stream.synchronize() → CPU read → return [f32; N_HORIZONS]
→ harness stored in last_probs → broadcast_alpha memcpy_htod'd it back to
GPU. The 4-5 probs round-tripped the CPU twice per event for nothing.
The per-event stream.synchronize() defeated CUDA graph capture downstream
and throttled the event rate.

Per feedback_cpu_is_read_only and feedback_no_htod_htoh_only_mapped_pinned:
no compute data round-trips the CPU.

Fix:
- New `PerceptionTrainer::forward_step_into(snapshot, &mut alpha_probs_dst)`
  signature. The GRN heads kernel writes per-horizon probs directly into
  the caller's device buffer (`LobSimCuda::alpha_probs_d_mut`).
- Removed `probs_step_d`, `probs_step_host` fields. Removed the
  DtoD-to-host-staging, the stream.synchronize, and the CPU read.
- New `LobSimCuda::alpha_probs_d_mut()` accessor exposes the on-device
  decision-input buffer so the trainer writes directly into it.
- Harness loop now: `forward_step_into(&raw, sim.alpha_probs_d_mut())`
  then (stride-gated) `step_decision_with_latency`. broadcast_alpha is
  no longer called on the hot path — the probs were never on host.
- Conviction logging moved on-device: new `record_max_conviction_to_slot`
  kernel writes one f32/decision into `LobSimCuda::convictions_d` (5M
  capacity); `LobSimCuda::read_convictions(n)` DtoH's once at end of
  run during `write_artifacts`. Replaces the host-side max-of-5 loop on
  `self.last_probs` per decision. `last_probs` field deleted.
- Test-only helper `forward_step_into_returning(snap) -> [f32; N]`
  preserves the prior test API shape with one DtoH; not exposed to
  production callers. Existing forward_step_golden.rs tests retargeted
  to this helper.

Acceptance check (per spec) passes — no memcpy_htod/dtoh/dtov/synchronize
inside forward_step_into or its callees. Only memcpy_dtod_async (DtoD).

Issue 2 (prior report concern #1): bit-identical seed from forward_only.

Previously the convergence test passed only at tolerance 0.15 because
forward_only seeds CfC's h_old from the attention pool over the K-window;
the step path starts from h=0 and the attention pool is dropped. Per memo
§4.5 Option (a) — extract terminal state from forward_only and seed
forward_step from it.

Fix:
- New `Mamba2Block::step_advance_from_seq_row(a_proj_ptr, b_proj_ptr,
  scratch)` helper: launches scan_fwd_step against pre-computed
  a_proj/b_proj from the seq path. Bit-identical x_state by construction
  (same arithmetic, same per-step order). Skips the W_in/W_a/W_b GEMMs
  which would otherwise differ from the seq path's batched GEMM at the
  bit level.
- New `PerceptionTrainer::seed_step_state_from_forward_only(window)`:
    1. Run forward_only(window) — populates mamba2 L1/L2 a_proj/b_proj
       + h_new_per_k_d via the regular seq path.
    2. Reset step scratches' x_state to zero.
    3. For k in 0..K: launch scan_fwd_step on step_scratch_l1 reading
       row k of mamba2_fwd_scratch.a_proj/b_proj. Same for L2.
    4. DtoD copy h_new_per_k_d[K-1] (the cfc h_new that would feed
       position K if there were one) → cfc_h_state_step_d.
- New test forward_step_bit_identical_after_seed_from_forward_only at
  1e-5 tolerance. Asserts forward_step_into on snapshot K (after seeding
  from window [0..K-1]) matches forward_only's last-position prediction
  on window [1..=K].

Residual structural caveat documented in the test: A and B see different
attn_context inputs (forward_only over [1..K+1] vs warmup [0..K]) and B
has one extra cfc iteration in its chain. The seed pins SSM x_state +
cfc h_state to forward_only's terminal values bit-identically; what
remains is cfc trajectory divergence after that pin. Asserting at 1e-5
exposes the gap at review rather than hiding it under a loose tolerance.

Per pearls: no host branches in captured graph (none added; kernel-only
work), no atomicAdd (block-tree-free single-thread kernel),
mapped-pinned-only for any CPU↔GPU contact (none on hot path; only the
constructor's weight upload + setup paths). cargo check workspace clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:36:37 +02:00
jgrusewski
a0e81fbdfc arch(crt-a): forward_step incremental SSM state — enables event-rate trunk forward
Per A0 investigation memo (commit 2e87ed0da) — forward_only was Case 2
(stateless K=64 window per call). Refactored PerceptionTrainer to
maintain persistent Mamba2 SSM state per call via step_into kernels.

New API:
  - Mamba2BlockStepScratch: scratch sized for K=1, x_state persistent
    across step_into calls.
  - Mamba2Block::step_into: single-step forward with x_state in-place
    update.
  - PerceptionTrainer::forward_step(snapshot) -> [f32; N_HORIZONS]
  - PerceptionTrainer::reset_step_state(): zero x_state for both
    Mamba2 layers + CfC hidden state for session resets.

Decisions (from A0 memo §5):
  1. K=1 path: added a dedicated `mamba2_alpha_scan_fwd_step` kernel.
     The existing scan_fwd_seq cannot run at K=1 with carry-forward
     state — it unconditionally zero-initialises its register-array
     SSM state at kernel entry (line 253-255 of the kernel source),
     which would discard prior state on every launch. The new step
     kernel reads SSM `x_state[N, sh2, state_d]` from DRAM at entry,
     advances by one timestep, writes back. Same arithmetic as
     scan_fwd_seq's per-step inner loop.
  2. x_state carry: written in-place in DRAM at end of step_into.
     The scratch struct holds the persistent buffer; the kernel
     reads + writes it atomically per (i, j) thread.
  3. CUDA Graph at K=1: chose eager dispatch. Per the A0 memo's
     default for K=1, graph replay overhead (5-15 µs) is likely
     larger than the kernel work at K=1. Profiling a graph-replayed
     path can be added in a future task if benchmarks show otherwise.
  4. Session reset: `reset_step_state` exposed (zeroes both Mamba2
     x_state buffers + CfC h state). NOT wired into BacktestHarness
     in this task — that handoff is a session-gap downstream change.
  5. Spec §3.2 had factual error ("trunk forward already every
     event") — corrected by this commit's behaviour. Spec doc edit
     deferred to a separate concern.

Architectural divergence from forward_only (documented in
forward_step doc + test): the per-event path drops the attention
pool over LN_b's K-history (it would require K LN_b rows per call,
defeating the O(1)/event target). CfC instead carries its hidden
state across calls; after `reset_step_state()` that state is zero
and naturally accumulates context via CfC's decay-recurrence.

Golden test (forward_step_golden.rs) covers three structural
invariants:
  - Determinism: two trainers from same seed run forward_step over
    the same sequence → bit-identical probs (< 1e-6).
  - Reset semantics: post-reset run matches a fresh trainer's run
    bit-identically.
  - Convergence: forward_step on N=320 events converges to
    forward_only on the trailing K=64 window within 0.15. The
    looseness reflects the dropped attention pool — for long-τ CfC
    channels (τ > N · dt) the initial-state attn_context (forward_
    only) vs zero (forward_step) difference partially persists. Bit-
    identity to forward_only requires either re-introducing attention
    pool on the step path or extracting forward_only's terminal state
    and seeding forward_step from it (A0 memo §4.5 option (a));
    both deferred.

Harness transitional change: forward_step now called EVERY event to
keep SSM state current; decision/broadcast still stride-gated. A1
will delete the stride gate. Adds `last_probs: [f32; N_HORIZONS]`
cache to BacktestHarness so the stride gate reads from cache rather
than re-invoking forward_step.

Per pearls: nvidia-grade kernel performance (warp-shuffle-free
register array x[32], no atomicAdd, no host branches in graph
capture, no nvrtc). The new kernel is pre-compiled in build.rs's
existing mamba2_alpha_kernel.cu cubin alongside fwd/bwd/seq variants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:05:44 +02:00
jgrusewski
2e87ed0daf memo(crt-a): forward_only cost investigation — Case 2 (stateless K-window)
forward_only requires the full K=seq_len window on every call and resets
Mamba2 h_s2 to zero each invocation — no cross-call state carry. Additionally,
the call is inside the stride=200 gate in harness.rs (spec §3.2 claim "already
every event" is incorrect as of HEAD). Moving to stride=1 without a forward_step
implementation would be a ~200x GPU cost increase. A0.5 must implement
forward_step with persistent h_s2, a dedicated K=1 CUDA graph, and session-reset
hook. Memo documents exact file paths, line numbers, required struct/method
changes, and open questions for the A0.5 implementer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:43:52 +02:00
jgrusewski
b508c38529 plan(crt-a): remove fallbacks — greenfields, resolve properly
Per user direction: no fallbacks, greenfields, kernel updates acceptable.

Changes:
  1. Task A0.5 ADDED — pre-planned task that fires if A0 investigation
     finds forward_only is stateless K-window (Case 2) or has periodic
     reset (Case 3). Refactors PerceptionTrainer to maintain persistent
     SSM state and expose forward_step(snapshot, b) that advances by 1
     event. Bit-identical to forward_only of equivalent window per a
     new golden test in incremental_forward.rs.

     This is NOT a fallback — it's a pre-planned task whose execution
     is determined by actual investigation outcome. Case 1 → A0.5 is
     a no-op. Case 2 or 3 → A0.5 fires in full. Either way the plan
     proceeds without user-approval pause.

  2. Task A2 reframed — Wiener-α conviction-EMA is a "load-bearing
     component of continuous control," not a "minimal Phase B subset."
     Without smoothing, event-rate trading is structurally incoherent.

  3. Task A2 test fallback REMOVED — instead of "if helpers don't
     exist, omit the unit test," the plan now says "add the helpers
     properly to the public API." Step 1 audits the LobSimCuda public
     API and adds missing accessors. Greenfields — accessors stay on
     the API permanently if testing needs them.

  4. Task A5 (conditional hyperactivity mitigation) DELETED. A2 is
     designed correctly the first time with Wiener-α floor at 0.4. If
     A4 cluster smoke shows hyperactivity, that's an A2 bug to fix
     properly, not a tuning knob to nudge.

  5. Scope contract updated — Phase A vs Phase B split is by code-path
     responsibility (continuous control infrastructure vs multi-horizon
     signal-driven policy), NOT by "shipping less to be safe."

  6. Notes for the Implementer — explicit "No fallbacks" section
     replaces the implicit-fallback language. STOP-and-notify-user for
     Case 2 deleted (A0.5 handles it inline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:37:24 +02:00
jgrusewski
235961987d plan(crt-a): Phase A continuous controller implementation plan
Per docs/superpowers/specs/2026-05-20-continuous-reasoning-trader-design.md
(v2, commit 8ba8755b4). One plan per phase per writing-plans guidance —
Phase B/C/D plans written after each prior gate closes.

Phase A scope:
  A0 - Investigate forward_only cost model (read-only research,
       writes memo to docs/superpowers/memos/). STOP condition if
       trunk forward is stateless K-window per call (Case 2) —
       requires user approval to expand scope.
  A1 - Atomic delete of decision_stride (greenfields, no compat).
       Removes from SweepBase, ResolvedSimVariant, BatchedSimConfig,
       BacktestHarness, all YAMLs.
  A2 - Minimal Wiener-α conviction-EMA smoothing in decision_policy_*
       kernels. Three new device slots: conviction_ema_d,
       conviction_diff_var_ema_d, conviction_sample_var_ema_d. Floor
       at 0.4 per pearl_wiener_alpha_floor_for_nonstationary.
       First-observation bootstrap. Direction stays from raw alpha;
       magnitude/sizing uses smoothed value.
  A3 - Local compile + tests.
  A4 - Cluster smoke + Gate 1 validation against spec §3.6 acceptance.
       Memory entry on closure (green/red/yellow).
  A5 - Conditional hyperactivity mitigation (raise floor 0.4→0.6 OR
       add target-delta hysteresis). Fires only if Gate 1 reveals
       trade-count balloon.

Out of scope (deferred): full §4.4 multi-horizon conviction formula,
§4.3 conviction-degradation exit, §5 envelope, §6 LoRA adapter, §7
open_trade_state expansion — all Phase B/C/D.

Pearl conformance:
  - feedback_no_partial_refactor (atomic delete)
  - feedback_no_legacy_aliases (no compat shim)
  - feedback_push_before_deploy
  - pearl_wiener_optimal_adaptive_alpha
  - pearl_wiener_alpha_floor_for_nonstationary
  - pearl_first_observation_bootstrap
  - feedback_stop_on_anomaly (Gate 1 RED → diagnose, don't advance)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:33:16 +02:00
jgrusewski
8ba8755b48 spec(crt): v2 amendments from critical review — 12 issues + greenfields
Self-critical review of v1 (commit 0f3484325) found 12 issues. All
fixed inline. Spec marked Status: Design v2 with explicit changelog.

LOAD-BEARING FIXES
  1. §4.4 conviction cold-start: removed backwards "uniform fallback"
     branch that maintained trading authority when net_edge=0 across
     all horizons (exactly when model has no edge). Replaced with ε
     floor on net_edge_h. Single formula, smooth bootstrap, no regime
     switch.
  3. §4.3 exit logic: 5 OR conditions → 1 composite exit_signal scalar
     (max of degradation, disagreement, pnl_decay terms). SL/trail and
     8h circuit-breaker stay orthogonal as safety, not exit policy.
     Attribution per term recorded in §7 state for observability.
  6. §9 Gate 2: PF > 1.0 (2.5× improvement, unrealistic) → tiered
     MUST (no-regression) + WIN (real lift, four candidate criteria) +
     explicit STOP condition if MUST passes without WIN.
  7. §9.1.1: alpha-realization metric defined explicitly as
     realized_pnl / max(unrealized during signal-validity window).
     Was hand-waved in v1; now formulated.

TRACTABLE INLINE FIXES
  2. §7 open_trade_state: 128 → 64 bytes. Diagnostic-only fields
     (scale_up/down counts, max/min target reached) moved to a
     separate audit buffer (single source of truth keeps state lean).
  4. §5.1 envelope: multiplicative formula → additive bounded
     contributions. tanh(sharpe), min(dd/cap), clamp(vol). No single
     factor can crash envelope to zero. Floor (envelope_floor lots)
     and hard-cap (envelope_hard_cap) remain.
  5. §5.2 threshold: hand-waved "self-tune toward PF" → explicit
     3-arm bandit (lower, current, higher percentile) every K_eval
     closed trades, Wiener-α EMA target with floor, bounded
     [0.5, 0.95].
  8. §3.4 + §3.6: Layer A wall-time ≤ 5× → ≤ 2× (controller is
     light, 5× would indicate structural bug, not config tuning).
  9. §4.2: explicit Wiener-α formula for conviction EMA with floor
     at 0.4 per pearl_wiener_alpha_floor_for_nonstationary.
 10. §6.2.1: shadow eval defined explicitly with backtest-mode (last
     15% of fold) and live-mode (parallel-prediction-only) variants.
     Promotion criteria spelled out (PF, Sharpe, tail loss).
 11. §6.2: Layer D loss explicit (primary = value regression on
     realized PnL conditional on state; secondary = EWC++ regularizer
     toward base). Was three losses with no specified combination.
 12. §10.5: adapter regime over-fit risk added with sample-count
     down-weighting, cross-regime shadow eval, periodic adapter reset
     as hard backstop.

GREENFIELDS (per user direction)
  - §3.3: decision_stride field DELETED from YAML + Rust (not
    deprecated). No backwards-compat shim. Per
    feedback_no_legacy_aliases.
  - §7.1: open_trade_state 24→64 byte migration is atomic, no old
    layout shim.
  - §8: boundary matrix decision_stride row marked REMOVED, not
    DEPRECATED.

§13 pearl conformance: per-section pearl application (not bulk
citation). Added pearl_audit_unboundedness_for_implicit_asymmetry,
pearl_no_deferrals_for_complementary_fixes acknowledgement (layers
are sequenced amplifications, not parallel fixes), and the new
pearl_stop_checks_run_at_deadline_cadence from S2.

Status: Design v2 — awaiting user review before writing-plans.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:24:03 +02:00