- cudarc features: removed "nvrtc" (no longer used, all kernels precompiled)
- cudarc features: added "f16" (enables DeviceRepr + ValidAsZeroBits for half::bf16)
- CudaSlice<half::bf16> now fully functional with all cudarc operations
- Foundation for full BF16 tensor core conversion (next session)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CachedPtrs:
- 35-field struct caching all GPU buffer u64 device pointers
- Computed once at construction, replaces 110+ raw_device_ptr() per step
- Eliminates cudarc event tracking machinery from hot path
Sync removal:
- apply_iqn_trunk_gradient: removed cuStreamSynchronize (same-stream ordering)
- apply_ensemble_trunk_gradient: removed cuStreamSynchronize
- run_ensemble_step: removed cuStreamSynchronize + local EvtGuard struct
- replay_adam_and_readback: zero per-step DtoH — returns 0.0, epoch boundary
uses GPU training guard's accumulator buffer for actual metrics
Logging cleanup:
- Removed per-step tracing::info diagnostic with cuStreamSync (was every 1000 steps)
- Removed per-step tracing::debug for IQL/IQN/CQL (format overhead in debug builds)
- Single tracing::debug at end of run_full_step (zero cost in release)
EventTrackingGuard made pub(crate) for fused_training.rs access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ROOT CAUSE of fake 92% MaxDD: when capital floor fires (done=1), the
training kernel wrote done but did NOT reset the portfolio state. The
next step read the blown portfolio, hit the pre-trade floor check again,
returned done=1 with reward=-10, and the episode got STUCK in a done
loop for the rest of the epoch. Every step produced done=1 + -10 reward
from dead capital.
Fix: reset all 20 portfolio state fields + current_timesteps to fresh
capital at BOTH:
1. Pre-trade floor check (early return path, line 607)
2. Post-trade done detection (end of kernel, after outputs written)
This ensures the next step starts a clean episode with initial_capital,
flat position, and zero Kelly/realized_pnl accumulators.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The done bar's return (liquidation loss) must be counted in the current
episode's DD before resetting. Previous code reset BEFORE compounding,
which applied the liquidation return to fresh initial_capital.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The training financials computed MaxDD by compounding returns across
episode boundaries. When the capital floor circuit breaker fired (done=1)
and the episode reset with fresh capital, the equity curve kept falling.
This produced fake 92% MaxDD from concatenated episodes.
Fix: added done_flags to TradeStats (downloaded from GPU done_out buffer
at epoch end). MaxDD computation now resets equity/peak at done=1 events,
matching the backtest evaluator's per-window isolation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Research finding: NoisyNet provides undirected parametric noise, count-bonus
provides directed UCB action-level exploration. They don't conflict —
they address different failure modes (NoisyNet: policy lock-in,
count-bonus: action collapse onto Flat).
The CUDA kernel was already fully wired. Only the coefficient changed
from 0.0 to 0.05. Especially important for:
- 81 factored actions (high combinatorial space)
- Sparse trade-completion rewards (long reward-drought periods)
- Non-stationary markets (exploration must remain active)
References: Osband et al. 2019, Bellemare et al. 2016, Ostrovski et al. 2017
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Automated mismatch scan confirmed all from_continuous fixed defaults now
match Default::default(). No more silent divergence between hyperopt
params and manual/test params.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update 3 integration tests in dqn_action_collapse_fix_test.rs that
expected the old 46D search space (cql_alpha tunable, phase-specific
v_range bounds). Now expect 22D with fixed cql_alpha.
- Fix cql_alpha: from_continuous hardcoded 0.5, Default uses 0.1.
Aligned to 0.1 (same pattern as dd_threshold fix earlier).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The stale comments agent incorrectly changed calculate_diversity_penalty's
max_entropy from log2(3) to log2(9). But this function takes 3-bucket
BUY/SELL/HOLD ratios — max entropy of 3 buckets IS log2(3).
The log2(9) change was correct for the LOGGING in extract_objective
(which displays max_entropy for 9 exposure actions), but wrong for the
penalty function that operates on 3 action categories.
Reverted to log2(3) and restored test assertions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- gpu_backtest_evaluator: "5-bit mask" → "9-bit mask" (9 exposure levels)
- dqn.rs CVaR ramp: updated examples to match current formula (excess*100).min(3.0)
- dqn.rs module doc: "maximizes" → "minimizes", weights match actual code
- entropy max_entropy: log2(3) → log2(9) in both occurrences
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- bars_per_day: now propagated from DQNHyperparameters to GpuBacktestConfig
(was silently using Default 390.0 regardless of hyperparams setting)
- trading_days_per_year: added to GpuBacktestConfig, replaces hardcoded 252.0
in annualization_factor computation
- Annualization: sqrt(bars_per_day * trading_days_per_year) is now fully
configurable for any bar frequency and market calendar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- spread_cost: backtest now uses tick_size * contract_multiplier * fill_spread_cost_frac
(was tick_size * spread_ticks = 25x lower than training → train/eval mismatch)
- Trailing stop: remove regime-adaptive scaling from training kernel.
Both train and eval now use fixed 0.5% trail distance (vol_scale=1.0, trend_scale=1.0).
Model observes ADX/CUSUM via state features and adjusts behavior — trailing stop is
a safety net, not a strategy component.
- Branch sizes: refactored out of hardcoded 9/3/3 in gpu_backtest_evaluator.rs.
Now stored on GpuBacktestEvaluator struct, set from DqnBacktestConfig in ensure_cublas_ready.
launch_env_step and launch_metrics_and_download read from self.b0/b1/b2_size.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- apply_margin_cap: position scales DOWN as drawdown increases.
At 0% DD → 100% position, at 12.5% DD → 50%, at 20% DD → 20%.
Prevents multi-bar cascading losses that pushed max_dd to 38-51%.
- Add trading_days_per_year to DQNHyperparameters (default 252).
Configurable for crypto (365) or different markets.
- Fix trailing stop comment: value IS prev_equity (not approximation).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add spread_cost as 28th param to experience_env_step kernel
- Wire from DQNHyperparameters.fill_spread_cost_frac through
ExperienceCollectorConfig to the kernel launch
- Training execute_trade() and compute_tx_cost() now use spread_cost
(was hardcoded 0.0, backtest used real spread_cost)
- All compute_tx_cost calls in training use spread_cost consistently
This was the LAST remaining train/eval transaction cost mismatch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Training kernel now has same order as backtest:
decode → trailing_stop → hold_enforcement → execute_trade → mark_to_market
Previously hold ran AFTER execute_trade and undid it by resetting
position/cash from ps[]. Now hold modifies target_position BEFORE
execute_trade, so the trade sees the correct target and no undo is needed.
This eliminates the last train/eval order-of-operations mismatch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move trailing stop BEFORE execute_trade in training kernel (was after,
then undone). Now matches backtest: check → modify target → execute.
Eliminates the wasteful execute-then-undo pattern.
- Trailing stop uses current-bar unrealized (not raw_next forward-looking)
for train/eval consistency
- Add b2_size/b1_size guards to action re-encoding in both kernels
(prevents division by zero if branch sizes are ever 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- actions_history: record post-enforcement action (re-encode from final
position) instead of raw model action — fixes inflated trade counts
- Trailing stop exit cost: use compute_tx_cost() in training kernel
(was simplified inline formula) — matches backtest
- Trailing stop trade return: use portfolio-weighted formula in backtest
(position * (close - entry) / value) — matches training pattern
- Hold time: training kernel uses update_hold_time() from shared header
(was 8-line inline duplicate)
- Second comprehensive scan: zero inline duplicates, 2 acceptable
medium-severity train/eval differences remain (trailing stop uses
raw_next in training vs close in backtest — by design)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Final 3 inline duplicates replaced with shared function calls:
- execute_trade(): notional cash model (was inline cash/position update)
- check_trailing_stop(): regime-adaptive trailing stop decision
- enforce_hold(): hold enforcement decision (action aliasing kept inline)
Training kernel now uses ALL 10 shared functions from trade_physics.cuh:
decode_exposure_index, compute_target_position, decode_order_type,
execute_trade, compute_tx_cost, enforce_hold, check_trailing_stop,
check_capital_floor, apply_margin_cap, update_hold_time
Zero inline duplicates remain. Both training and backtest kernels
use identical trade physics — no more train/eval mismatches.
Smoke test: Sharpe=4.89, 443 trades, no SIGSEGV.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CRITICAL train/eval mismatch resolved: the backtest kernel used an
entry-price-reset model while training uses a notional cash model.
These produce different P&L on partial fills (L50→L100) and different
equity trajectories.
Changes:
- Add execute_trade() to trade_physics.cuh (shared notional model)
- Rewrite backtest_env_kernel.cu to use notional cash:
cash -= delta * price (not entry-price-reset)
- Mark-to-market: equity = cash + position * close (not cash + unrealized)
- Capital floor liquidation uses notional close (cash += position * price)
- entry_price kept only for trailing stop trade return computation
- Updated on new entries/reversals only (not on same-direction scaling)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major changes:
- Eliminate ALL runtime NVRTC: c51_loss + mse_loss kernels converted to
precompiled cubins with runtime num_atoms/v_min/v_max/branch params
- Fix evaluator SIGSEGV: rng_states/q_gaps sized for chunked batch (cn)
not n_windows; NULL pointer guard in action_select kernel
- Add trade_physics.cuh shared header for train/eval consistency
- Add equity circuit breaker (25% DD from peak) + margin-aware position cap
- Consolidate 46D→22D hyperopt search space, enable ensemble by default
- Fix trade counting: use exposure index not factored action
- Fix Calmar overflow: clamp to ±100 in kernel
- Softer CVaR penalty (cap 3.0 not 10.0) for undertrained models
- Fix win_rate display (ratio→percentage)
- Remove dead code (normalize_reward, calculate_completion_penalty)
- Add tracing subscriber to hyperopt test for visible metrics
- Per-chunk sync in evaluator for reliable error reporting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Forward kernel has shared_concat[128] (512 bytes) but Rust allocated d*4
bytes. With d=72: 288 < 512 → shared memory overflow. Same for backward
(concat[128] + d_proj[128] = 1024 bytes vs d*8).
Fix: use d.max(128) for shared memory sizing.
Also: revert c51/mse to runtime NVRTC (NUM_ATOMS controls loop bounds).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c51_loss and mse_loss kernels use NUM_ATOMS for loop bounds AND shared
memory. Precompiling with NUM_ATOMS=51 then running with num_atoms=101
causes wrong loop iterations. These 2 kernels MUST use runtime #define
injection to match the hyperopt config.
35 of 37 kernels remain precompiled. Only c51_loss and mse_loss use
runtime NVRTC (they're the only kernels where a #define controls logic).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same class of bug as IQN_HIDDEN: compile-time array float local_proj[51]
overflows when hyperopt picks num_atoms=101. Crashed at ~35 min when
PSO explored larger atom counts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause (found via Zen debug + Gemini 2.5 Pro):
IQN_DIST_MAX = (IQN_HIDDEN + 31) / 32 = 8 (from IQN_HIDDEN=256).
Register arrays h_dist[8], embed_dist[8], comb_dist[8] overflow when
hyperopt picks hidden_dim > 256. With hidden_dim=512: loops iterate
16 times on 8-element arrays → stack corruption → SIGSEGV.
Fix: IQN_HIDDEN=2048 → IQN_DIST_MAX=64. Runtime loops still use
actual hidden_dim param. Only register array sizing affected — no
perf impact (unused elements are never accessed).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
IQN_EMBED_DIM (cosine embedding dimension, default 64) converted from
compile-time #define to runtime kernel parameter `embed_dim`. This was
the last remaining hardcoded dimension in the precompiled IQN cubin
that could diverge from GpuIqnConfig.embed_dim at runtime, causing
SIGSEGV from out-of-bounds weight offset computation.
All 9 IQN kernels (forward_loss, backward, grad_norm, adam, forward,
trunk_forward, ema, decode_actions, sample_taus) now receive embed_dim
as the final parameter. The iqn_compute_offsets() device helper uses
the runtime embed_dim for W_EMBED→B_EMBED offset calculation.
The other 7 kernel files in the task spec were verified safe:
- c51/mse_loss_kernel.cu: NVRTC-compiled with config values injected
- attention/attention_backward: already parameterized (state_dim, num_heads)
- dt_kernels.cu: NVRTC-compiled with config values injected
- curiosity/ppo: architecturally constant values matching Rust constants
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build-time #define was 256 but runtime config can be 128 (hidden_dim_base).
Caused CUDA_ERROR_ILLEGAL_ADDRESS in IQN trunk gradient. Now passed as
kernel params matching the STATE_DIM parameterization pattern.
Also: warn→error for training failures in optimizer, eprintln for debugging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build-time #define was 256 but runtime config can be 128 (hidden_dim_base).
Caused CUDA_ERROR_ILLEGAL_ADDRESS in IQN trunk gradient. Now passed as
kernel params — matches the pattern used for STATE_DIM parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
increment_trial() was never called — budget enforcement was broken because
TrialBudgetObserver had its own internal Arc<AtomicUsize> disconnected from
the trial_counter used by evaluate_point() and cost(). With num_trials=2,
PSO ran 20 particle evaluations instead of stopping at 2.
Now TrialBudgetObserver::new() takes the caller-owned Arc<AtomicUsize> so
all evaluation paths (LHS via evaluate_point + PSO via cost()) share one
counter. ObjectiveFunction::cost() and ParallelObjectiveFunction::cost()
increment trial_counter directly (fetch_add SeqCst) and check budget inline;
observe_iter() reads the same counter to terminate PSO between iterations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backtest portfolio now tracks hold_time at index [5]. min_hold_bars
param enforces hold constraint during evaluation, matching training.
Fixes train/eval mismatch that caused NaN Sharpe → 1e6 penalty.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The kernel was updated with portfolio_states, min_hold_bars, max_position
params (Task 3 of trade lifecycle fix) but the backtest evaluator's launch
site wasn't updated. It passed 10 args to a 13-param kernel, causing
SIGSEGV from reading garbage for the missing params.
Fix: pass NULL/0/0.0 for the 3 new params (hold enforcement disabled
during evaluation for now — will be enabled in eval cleanup plan).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend build.rs to compile all 25 .cu kernel files to cubins via nvcc.
Common header prepended to 24 kernels; experience_kernels.cu is standalone.
Total cubin size ~1.1 MB embedded in binary.
- cargo:rerun-if-changed tracks all .cu and .cuh files
- CUDA_COMPUTE_CAP env var selects target arch (sm_86, sm_90)
- Graceful skip when nvcc unavailable (non-CUDA builds)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The reversal P&L block (Kelly stats, entry_price reset) ran BEFORE the
hold enforcement guard. If hold_time < min_hold_bars cancelled a reversal,
the P&L was already booked for a trade that didn't execute, corrupting
portfolio_states. This caused SIGSEGV in the hyperopt path where the
corrupted state triggered invalid memory access in subsequent kernels.
Fix: moved the reversal P&L block from line 751 to after line 862
(after hold enforcement resolves final reversing_trade value).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Trade count dropped from 77% turnover (620K/800K) to 16.5% (527/3200)
with min_hold_bars=3. The hold enforcement is working correctly in the
standard training path. SIGSEGV in hyperopt path is a separate issue
(likely buffer sizing in the hyperopt adapter's experience collector).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With v_range ±240 (vs old ±2), C51 distributional loss is naturally
~100-400 instead of ~3-6. Updated smoke test assertion accordingly.
Trade lifecycle fix validated: 467 trades (was 620K) with min_hold_bars=3.
PF=1.87, WinRate=49.7%, MaxDD=41.7% — model learns multi-bar swing trading.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>