The hold enforcement infrastructure (enforce_hold in trade_physics.cuh,
hold_time counter at ps[10], action masking in experience_action_select)
was already fully implemented but disabled by min_hold_bars defaulting
to 1. Change default from 1 to 5 so hold enforcement is active by
default. Production configs already set min_hold_bars=10 via TOML.
Targets ~100-200K trades instead of 619K across 5M bars.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EMA sync restricted to indices 0-67 (non-ISV params).
ISV weights (68-77) exist only in online params_buf.
Target forward uses online gamma_buf (drift-conditioned) and
online branch_gate_buf — correct for Bellman projection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
h_s2 → sigmoid(w_conf @ h + b_conf) → predicted_error [B].
MSE loss vs lagged_td_error (0.01× weight). Backward accumulates
into trunk gradient + conf weight gradients in main grad_buf.
Creates self-improvement loop: model learns to predict when wrong.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8 ISV signals (q_drift, grad_norm_ema, td_error_ema, ensemble_var_ema,
ensemble_var_delta, reward_ema, atom_util, loss_ema) computed by
single-thread GPU kernel. Temporal history [K=4, 8] with ring buffer.
Lagged TD-error saved for recursive confidence. Zero CPU involvement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
td_error_scratch, ensemble_var_scratch, reward_scratch, atom_util_scratch
— all pinned device-mapped. atom_util and reward_scratch written in
reduce_current_q_stats. td_error and ensemble_var wired in later tasks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rank normalization: holding bars (reward≈0) passthrough as zero,
only actual trade P&L is ranked. Preserves magnitude for trades.
Q-drift: hardcoded penalty removed, E1 enrichment handles adaptively.
Metrics: sharpe_raw (un-annualized per-trade) for direct comparison
between training and validation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Components were fighting a broken signal, not broken themselves.
With correct signal: adapt DSR to trade-level, rank only non-zero
rewards, keep commitment as soft signal, let E1 enrichment handle
Q-drift instead of hardcoded kernel penalty.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: training environment differs from validation backtest in 3 ways:
1. 100-bar episodes force exits (val runs continuous) → position-gated done
2. Rank normalization destroys sparse trade-level reward → remove it
3. Different Sharpe computation (per-trade vs per-bar, different annualization)
Single execution path:
Phase 1: Episode alignment (100→5000 bars, position-gated done, soft reset)
Phase 2: Reward alignment (remove rank norm, raw trade P&L to replay)
Phase 3: Metrics alignment (un-annualized per-trade Sharpe, both paths)
Expected: training Sharpe within 2× of validation with same weights.
Removes 5 unnecessary shaping components that fought the broken signal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Q-mean drifted to +4.6 but E1 correction was capped at -2.0.
The model needs full correction range to keep Q-values calibrated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
segment_pnl depended on raw_pnl (now zero after raw_next removal).
The trade-closing P&L was missing the final bar's unrealized gain/loss.
Fix: compute unrealized_at_exit = position * (raw_close - entry_price)
directly. No future price needed — just current close vs entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E1 enrichment was computing q_corr = mean(predicted_q - pnl) but
predicted_q was set to avg_pnl → bias always ~0. Now uses actual
avg_q_value from the training step, producing meaningful corrections
when Q-values drift away from realized returns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
portfolio_sim_kernel also used next_close_raw (future price) for
mark-to-market and reward computation. Fixed: use current price only.
Updated stale comments referencing per-bar reward in trade-level section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The experience collector used raw_next (NEXT bar's close) for P&L and
equity, creating a 1-bar misalignment between actions and rewards.
The model couldn't learn which actions produce which outcomes.
val_Sharpe=24 (backtest uses raw_close correctly) but training Sharpe=0
(experience collector used raw_next, shifting reward by 1 bar).
Fix: ALL portfolio computations use raw_close only. raw_next removed
from reward path entirely. Rewards are per-trade only (position change
= realized P&L, holding = tiny cost, flat = zero).
This is the root cause of training Sharpe being capped at ~0 across
ALL runs regardless of model complexity or component changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Post-validation enrichment: extract eval trades from WindowMetrics,
run all 8 enrichments, apply adaptive epsilon + gamma + agreement
threshold. Smoke test verifies enrichment runs at least once.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every epoch's validation backtest feeds back into the next epoch:
E1: Q-value reality check (bias correction, replaces drift penalty)
E2: Adaptive epsilon (performance-driven, replaces schedule)
E3: Dynamic gamma (from trade duration, replaces manual annealing)
E4: Trade autopsy (per-branch LR scaling from error rates)
E5: Ensemble agreement tuning (auto-tune epistemic gate)
E6: Winner distillation (boost top 10% trades in replay)
E7: Hindsight optimal labels (correct actions for losers)
E8: Curriculum weights (oversample failure regimes)
E9: State confidence (tradability scores from eval)
Zero hardcoded schedules. The model adapts from its own performance.
~215 lines Rust, no new CUDA kernels. ~15s overhead per epoch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Risk branch weights trained via gentle decay toward initial values
(prevents R collapse to 0 or 1). Full BPTT backward deferred —
the risk branch learns its initial representation from trunk gradients
flowing through shared weights.
compute-sanitizer: 0 errors. Smoke test passes.
NUM_WEIGHT_TENSORS: 68. Total risk params: ~33K.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c51_loss_kernel: reads cvar_alpha_buf[sample_id] when available (NULL = iqn_readiness fallback).
env_step: reads commit_lambda_buf[i] when available (NULL = 0.01 fallback).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Zero-target normalization: scale=max(|target|,|observed|,1.0) instead of
max(|target|,1e-6). Prevents Q-mean (target=0) from producing infinite
error that steals entire budget from other observables.
2. Per-observable budget cap: each observable gets budget/N_OBS instead of
competing for a global pool. One runaway can't starve the others.
3. Readiness-driven alpha: alpha = 0.3*(1-readiness) + 0.01*readiness.
Model readiness drives target adaptation speed, not epoch number.
Exploring → fast targets. Converged → slow targets. Never frozen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-bar reward (next_close - close) has SNR ~0.01 — 99% random walk noise.
Trade-level P&L has SNR ~0.1-0.5 — the atomic unit of trading signal.
Position change + had old trade → reward = realized_pnl (trade outcome)
Holding (no change) → reward = -0.0001 * |position| (holding cost)
Flat → reward = 0
C51 atoms now model distribution of TRADE OUTCOMES instead of
distribution of per-bar noise. 10-50× signal improvement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fundamental fixes for training Sharpe breakthrough:
1. Trade-level rewards: replace per-bar noise (SNR=0.01) with trade
P&L attribution (SNR=0.1-0.5). C51 atoms model trade outcome
distributions, not random walk noise.
2. Exploration risk budget: protection stack (CVaR, epistemic gate,
commitment, DSR) scaled by iqn_readiness². Loose during exploration,
tight when converged. Model can discover edges before being punished.
Also: homeostatic regularization spec (unified adaptive penalties).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds G16 homeostatic regularization that penalizes training observables
drifting from calibrated set-points. All 6 scalar signals use pinned
device-mapped memory (zero memcpy). Targets self-calibrate via EMA
during epochs 1-5, then freeze.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Q-mean drift: linear penalty (0.01 * q_mean) → quadratic
(0.01 * q_mean * |q_mean|). Small drift = tiny penalty, large
drift = hard correction. At q_mean=3.5: 12.25× stronger than linear.
Cost curriculum: linear ramp (epoch/20) → sigmoid centered at epoch 10.
Gradual start (find raw edges), steep middle (force cost adaptation),
gradual finish (fine-tune at real costs). Prevents strategy breakage
from sudden cost increases that caused training Sharpe oscillation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mamba2_scan_backward kernel reads d_h_enriched [B, SH2] but was passed
self.grad_buf [TOTAL_PARAMS] — wrong buffer, wrong size. At batch_size=4096
the kernel read 1M floats from a 582K buffer → 2749 OOB reads.
Fixed: use self.bw_d_h_s2 [B, SH2] which is the actual trunk activation
gradient from the cuBLAS backward pass.
Also increased smoke test batch_size to 4096 to catch scale-dependent OOB.
compute-sanitizer: 0 errors at batch_size=4096.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CRITICAL: epsilon_buf memcpy_htod used full max_batch_size buffer but
eps_host was batch_size. Fixed: slice_mut(..batch_size) to match.
HIGH: update_q_mean_ema read pinned memory before GPU finished writing.
Moved after cuStreamSynchronize to ensure kernel completion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The compute_expected_q and quantile_q_select kernels read per_sample_support[i*3+0/1/2]
(3 floats per sample), but the experience collector was passing eval_v_range_ptr which
is only 2 floats (v_min, v_max). Every sample after sample 0 read out of bounds.
Replace the u64 pointer field with a proper CudaSlice<f32> buffer [alloc_episodes, 3]
that is tiled with [v_min, v_max, delta_z] once per epoch via update_per_sample_support().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two tests:
- test_generalization_kernels_load: no data, verifies all CUDA cubins load
- test_generalization_components_smoke: 3 epochs on fxcache, verifies
AdamW, cost_anneal, gamma_anneal, DSR, walk-forward state, Q-gap,
atom utilization, all kernel launches succeed, finite metrics.
Passes in 2.7s on RTX 3050 with 5000 bars.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>