Commit Graph

2693 Commits

Author SHA1 Message Date
jgrusewski
f7e5d76e82 fix: diversity penalty max_entropy — log2(3) for 3-bucket input, not log2(9)
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>
2026-03-27 10:55:11 +01:00
jgrusewski
84ccf7b27c fix: stale comments — 9-bit mask, CVaR examples, module doc, entropy max
- 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>
2026-03-27 10:31:11 +01:00
jgrusewski
25ee24aff4 fix: wire bars_per_day + trading_days_per_year to backtest evaluator
- 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>
2026-03-27 10:28:25 +01:00
jgrusewski
5ac1c9502a fix: spread_cost formula alignment + trailing stop unification + branch sizes from config
- 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>
2026-03-27 10:19:09 +01:00
jgrusewski
70d2986280 fix: dynamic margin scaling + trading_days_per_year config
- 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>
2026-03-27 10:04:32 +01:00
jgrusewski
e5ec2fc834 feat: configurable contract spec — eliminate hardcoded ES constants
Add tick_size, contract_multiplier, margin_pct to DQNHyperparameters.
Wire through ExperienceCollectorConfig and GpuBacktestConfig to both
CUDA kernels as runtime parameters.

Before: margin = close * 50.0f * 0.06f (hardcoded ES)
After:  margin = close * contract_multiplier * margin_pct (configurable)

Before: spread_cost = 0.25 * 50.0 * frac (hardcoded ES)
After:  spread_cost = tick_size * contract_multiplier * frac (configurable)

Defaults match ES: tick=0.25, mult=50, margin=6%.
For NQ: tick=0.25, mult=20. For 6E: tick=0.00005, mult=125000.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 09:41:53 +01:00
jgrusewski
4e37a16b77 fix: spread_cost wired to training kernel — eliminates last train/eval gap
- 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>
2026-03-27 09:21:04 +01:00
jgrusewski
ea57ed3d86 fix: hold enforcement moved before execute_trade — eliminates undo pattern
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>
2026-03-27 09:07:48 +01:00
jgrusewski
809ee287d8 fix: trailing stop order-of-operations + b2_size guards
- 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>
2026-03-27 08:40:23 +01:00
jgrusewski
d49c0e8630 fix: 5 final scan findings — actions_history, trailing stop, hold time
- 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>
2026-03-27 08:34:33 +01:00
jgrusewski
d256acb638 refactor: training kernel fully unified with trade_physics.cuh
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>
2026-03-27 08:16:51 +01:00
jgrusewski
876ed67f33 fix: unify portfolio simulation model — notional cash for both kernels
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>
2026-03-27 08:00:25 +01:00
jgrusewski
be65d8e5fc fix: hive findings — capital floor, dueling centering, metrics, CVaR objective
From 5-agent deep analysis:

Kernel fixes:
- Capital floor penalty: -1.0 → -10.0 (match reward range for C51)
- Add pre-trade capital floor check to training kernel (match backtest)
- Add tx cost to forced liquidation in both kernels
- Fix expected_q dueling centering: per-atom mean (was scalar mean)
- Fix Calmar: use exact full-window mean (was sampled for >4096 bars)
- Fix CVaR off-by-one: include VaR observation in Expected Shortfall
- Trailing stop: add vol_scale/trend_scale params to shared function
- Update reward v5 → v6 docstrings (12 occurrences)
- Fix annualization comment (sqrt(98280) not sqrt(252))

Rust fixes:
- CVaR penalty: additive → multiplicative discount on composite
  (prevents CVaR from dominating objective for undertrained models)
- Fix dd_threshold default mismatch (0.01 → 0.02 matching Default)
- Fix action entropy max_entropy (log2(3) → log2(9) for 9 actions)
- Fix Sortino denominator in financials.rs (N_negative → N_total)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 07:56:50 +01:00
jgrusewski
235503efe1 fix: shared trade physics, IQN arg swap, trailing stop, win_rate
- Upgrade compute_tx_cost: add spread_scale_override + Almgren-Chriss
  impact_scale = 1+sqrt(delta/max_pos) for both train and eval paths
- Wire training kernel to shared functions: replace 6 inline duplicates
  (decode, position map, order_type, tx_cost, capital floor) with
  trade_physics.cuh calls
- Fix IQN sample_taus_kernel: args 2-3 were swapped (seed/total)
- Add trailing stop to shared header + backtest kernel
- Fix win_rate test data: 6 instances used percentages (55.0) not
  ratios (0.55)
- Static analysis: 65 kernel launches audited (1 mismatch fixed),
  90+ buffers verified safe

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:47:34 +01:00
jgrusewski
d485dde5a2 fix: eliminate NVRTC, fix evaluator SIGSEGV, 22D search space, trade physics
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>
2026-03-27 00:33:05 +01:00
jgrusewski
65cc0175ac fix: attention kernel shared memory underallocation — hardcoded [128] vs dynamic d
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>
2026-03-26 08:39:09 +01:00
jgrusewski
719d3b2367 fix: revert c51/mse to runtime NVRTC — NUM_ATOMS controls loop bounds, not just sizing
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>
2026-03-26 08:25:38 +01:00
jgrusewski
9f63193351 fix: NUM_ATOMS 51→201 in c51/mse kernels — array overflow when num_atoms=101
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>
2026-03-26 08:19:19 +01:00
jgrusewski
500d8f18cd fix: IQN_HIDDEN 256→2048 — register array overflow caused SIGSEGV in hyperopt
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>
2026-03-26 07:27:09 +01:00
jgrusewski
f9e55a45d7 fix: parameterize IQN_EMBED_DIM in all 9 IQN CUDA kernels
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>
2026-03-26 02:54:19 +01:00
jgrusewski
4ab8ca0162 fix: parameterize IQN_SHARED_H1 + IQN_HIDDEN in all 9 IQN kernels
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>
2026-03-26 01:55:52 +01:00
jgrusewski
232a1f0301 fix: parameterize IQN_SHARED_H1 + IQN_HIDDEN in all 9 IQN kernels
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>
2026-03-26 01:55:15 +01:00
jgrusewski
8e6918ab26 feat: add release-test profile — 4x faster compile for GPU smoke tests
thin LTO + 16 codegen units + opt-level 3. Tests need unwind (not abort).
Usage: cargo test --profile release-test -p ml --lib -- test_name --ignored

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:30:47 +01:00
jgrusewski
ec257febe4 fix: all 6 issues blocking H100 — train/eval mismatch, budget, dynamic thresholds
1. Backtest hold enforcement: hold_time tracked at portfolio[5], min_hold_bars
   override in backtest_env_step. Train/eval mismatch fixed.
2. Documented evaluator strategy: Layer 2 in env_step, not action masking.
3. Trial budget observer: shared Arc<AtomicUsize> counter — budget enforced.
4. CVaR threshold: 0.05/sqrt(bars_per_day) instead of hardcoded 0.003.
5. MIN_TRADES_DEGENERATE constant, dynamic test vector dimensions.
6. Integration pending — local hyperopt next.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:24:38 +01:00
jgrusewski
9b4395b0df fix: trial budget observer — shared AtomicUsize counter enforces trial limit
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>
2026-03-26 01:18:19 +01:00
jgrusewski
3b0f22e9e8 feat: hold_time tracking + hold enforcement in backtest_env_step
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>
2026-03-26 01:13:07 +01:00
jgrusewski
94cf330dad docs: final fixes plan — 6 issues blocking H100 deployment
1. Backtest hold_time tracking + enforcement (train/eval mismatch root cause)
2. Document evaluator action masking strategy (Layer 2 in env_step)
3. Fix trial budget observer (shared AtomicUsize counter)
4. Dynamic CVaR threshold (0.05 / sqrt(bars_per_day))
5. Extract MIN_TRADES_DEGENERATE constant + fix stale test vectors
6. Integration test + local hyperopt validation

Root cause chain: broken trial budget → 21 evals → no hold in eval
→ NaN Sharpe → 1e6 penalty → "degenerate" result.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:06:28 +01:00
jgrusewski
0d41770958 Merge branch 'worktree-agent-acea52b4' 2026-03-26 00:48:44 +01:00
jgrusewski
cfcec6bc94 fix: pass 3 missing args to experience_action_select in backtest evaluator
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>
2026-03-26 00:47:15 +01:00
jgrusewski
6414f57e34 docs: eval cleanup spec + plan — remove dead 5-action path, fix train/eval mismatch
3 fixes: (1) Delete GpuActionSelector + branching_action_select (zero callers,
hardcoded 5 actions vs production 9), (2) Add hold enforcement to backtest
env_step kernel (same Layer 2 as training), (3) Dynamic trade insufficiency
threshold = max(window_bars / (min_hold * 20), 5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:43:58 +01:00
jgrusewski
116f04968b feat: major kernel modules load from precompiled cubins — zero runtime nvcc
Replace OnceLock<Ptx> + compile_ptx_for_device() with include_bytes! +
Ptx::from_binary() in 13 Rust files:

- gpu_action_selector.rs (epsilon_greedy)
- gpu_statistics.rs (batch_statistics)
- gpu_training_guard.rs (training_guard)
- signal_adapter.rs (signal_adapter)
- gpu_monitoring.rs (monitoring_reduce)
- gpu_curiosity_trainer.rs (curiosity_training)
- gpu_attention.rs (attention forward + backward)
- gpu_backtest_evaluator.rs (env, metrics, gather, ppo, supervised, dqn)
- gpu_experience_collector.rs (experience, nstep, her_episode)
- gpu_her.rs (her_episode, her_relabel)
- gpu_iql_trainer.rs (iql_value)
- gpu_iqn_head.rs (iqn_dual_head)
- gpu_ppo_collector.rs (ppo_experience)

Remaining runtime compilation: inline utility kernels in gpu_dqn_trainer.rs
(grad_norm, adam, ema, saxpy, zero, etc.) — small kernels that compile in ms.
883/887 tests pass (4 pre-existing failures in data_loader/hyperopt).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:42:39 +01:00
jgrusewski
bf2a7ba56d feat: build.rs precompiles all 25 CUDA kernels at cargo build time
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>
2026-03-26 00:25:56 +01:00
jgrusewski
33e5f61685 fix: move reversal P&L booking AFTER hold enforcement — prevents portfolio corruption
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>
2026-03-26 00:24:31 +01:00
jgrusewski
9f4762d763 refactor: pass state_dim/num_atoms/etc as kernel args instead of #define strings
Update all Rust kernel launch sites to pass the new runtime parameters
added in the previous commit:
- gpu_attention.rs: state_dim, num_heads for forward + backward
- gpu_monitoring.rs: num_actions, order_actions, urgency_actions
- gpu_backtest_evaluator.rs: metrics + PPO forward params
- gpu_dqn_trainer.rs: state_dim for regime_scale kernel
- gpu_iql_trainer.rs: state_dim for forward_loss + backward
- gpu_her.rs: state_dim for relabel kernel
- gpu_iqn_head.rs: state_dim for trunk_forward kernel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:22:20 +01:00
jgrusewski
bcf62eaecd fix: remove kernel printf debug, verify trade count fix works
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>
2026-03-26 00:20:49 +01:00
jgrusewski
82b595f633 refactor: convert #define constants to kernel parameters in .cu files
Replace #error guards in common_device_functions.cuh with safe defaults
(STATE_DIM=72, MARKET_DIM=42, PORTFOLIO_DIM=8). Add MAX_STATE_DIM=128
for safe stack array sizing.

Add runtime parameters to kernels that used compile-time #define:
- attention_kernel.cu: state_dim, num_heads
- attention_backward_kernel.cu: state_dim, num_heads
- backtest_forward_ppo_kernel.cu: state_dim, num_actions
- backtest_metrics_kernel.cu: num_actions, order_actions, urgency_actions
- dqn_utility_kernels.cu: state_dim
- her_relabel_kernel.cu: state_dim
- iql_value_kernel.cu: state_dim (all 3 kernel functions)
- iqn_dual_head_kernel.cu: state_dim
- monitoring_kernel.cu: num_actions, order_actions, urgency_actions

This enables single-cubin-per-kernel precompilation — no #define
injection needed at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:18:18 +01:00
jgrusewski
4729549250 fix: C51 loss threshold 50→500 for v_range ±240, fix flaky softmax diversity test
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>
2026-03-26 00:17:09 +01:00
jgrusewski
a2bf5210ec feat: build.rs precompiles epsilon_greedy_kernel.cu — proof of concept for NVRTC elimination
Add build.rs that compiles .cu kernels to cubins at cargo build time via nvcc.
Replace OnceLock<Ptx> + compile_ptx_for_device() in gpu_action_selector.rs with
include_bytes! + Ptx::from_binary() loading from the precompiled cubin.

- build.rs gracefully skips when nvcc is not available (non-CUDA builds)
- CUDA_COMPUTE_CAP env var controls target GPU arch (86=RTX 3050, 90=H100)
- common_device_functions.cuh prepended to kernel sources at build time
- Zero runtime compiler invocations for epsilon_greedy kernel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:07:16 +01:00
jgrusewski
0b743c0f80 docs: plan to eliminate NVRTC — precompile all 25 CUDA kernels at build time
7 tasks: build.rs POC → parameterize #define → update launches → build all
→ replace runtime compilation → delete NVRTC infra → integration test.

Key insight: convert #define STATE_DIM/NUM_ATOMS to kernel params so each
kernel has ONE version regardless of model config. No multi-variant compilation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 23:58:30 +01:00
jgrusewski
5f2e8930d3 feat: trade lifecycle fix — 3-layer hold enforcement (action masking + kernel override + reward scaling)
4 bugs fixed:
1. Hold guard was dead code (!exiting_trade always false) — replaced with unified guard
2. Reversals bypassed hold — new guard covers exits AND reversals
3. Single-bar trades got full reward — reward scaled by hold_time/min_hold_bars
4. No action masking — experience_action_select now forces current exposure during hold

3 layers of defense:
- Layer 1: Action masking in experience_action_select (masks Q-values + blocks random exploration)
- Layer 2: Kernel override in experience_env_step (catches epsilon exploration slipthrough)
- Layer 3: Reward scaling (smooth gradient: 10% floor to 100% at min_hold_bars)

Config: min_hold_bars added to DQNHyperparameters (default 5), TOML, hyperopt [2,20].
Trailing stop respects min_hold_bars (was hardcoded > 2.0f).
Search space: 46D (was 45D).

Also fixes flaky test_batch_hierarchical_vs_flat_softmax_diversity assertion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 23:32:15 +01:00
jgrusewski
745bd262cb feat: min_hold_bars in hyperopt search space [2, 20]
Integer parameter (rounded from continuous). Lets PSO discover
optimal hold duration for ES futures regime characteristics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:26:42 +01:00
jgrusewski
64925a21ff feat: action masking in experience_action_select — Layer 1 hold enforcement
During min_hold period, forces exposure to current position index.
Covers both greedy (Q-value) and random (epsilon) paths.
Portfolio state already GPU-resident — zero-copy parameter pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:17:36 +01:00
jgrusewski
84f8543a5f fix: hold enforcement in experience_env_step — blocks exits AND reversals during min_hold
Layer 2 (safety net): unified hold guard replaces dead code boolean.
Old guard (prev_sign!=0 && curr_sign==0 && !exiting_trade) was always
false. New guard covers both flat exits and reversals.

Layer 3 (gradient): reward scaled by hold_time/min_hold_bars (floor 10%).
Trailing stop respects min_hold_bars (was hardcoded > 2.0f).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:13:08 +01:00
jgrusewski
2b85fa2fdf feat: add min_hold_bars config (default 5) — prevents per-bar churning
Add min_hold_bars to DQNHyperparameters (usize, default 5), ExperienceSection
in training_profile, and both TOML configs (smoketest=3, production=5).
Wired through apply_to() so TOML overrides land correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:06:28 +01:00
jgrusewski
d71bf5805e docs: trade lifecycle fix plan — address review feedback
- Fixed struct name: ExperienceCollectorConfig (not GpuExperienceCollectorConfig)
- Fixed max_pos scoping: moved declaration before action_select launch
- Fixed exiting_trade ordering: preliminary variable before trailing stop
- Documented hardcoded 5-action limitation in branching_action_select
- Zero Q-gap during hold periods (conviction meaningless when forced)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 23:02:39 +01:00
jgrusewski
2197db8472 docs: trade lifecycle fix spec — address review feedback
- Layer 1 now targets experience_action_select (GPU-fused training path)
  AND branching_action_select (backtest/fallback), not just the fallback
- Action masking covers both greedy AND random exploration paths
- Exposure index uses parameterized b0_size, not hardcoded 5 or 9
- Fixed end-of-episode variable name (total_bars, not timesteps_per_episode)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:53:13 +01:00
jgrusewski
1da1b52bcd feat: centralize bars_per_day — no more hardcoded 390.0 scattered everywhere
- Added BARS_PER_DAY, BARS_PER_YEAR, ANNUALIZATION_FACTOR to common::thresholds::time
- Added bars_per_day field to DQNHyperparameters (default 390.0, configurable)
- compute_epoch_financials() now takes bars_per_day parameter from hyperparams
- coordinator_extended.rs + ab_testing.rs use centralized ANNUALIZATION_FACTOR
- When switching to tick data, change one constant in common/thresholds.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:17:20 +01:00
jgrusewski
b3110557df fix: use annualized mean return instead of compounded, cap MaxDD at 100%
Compounding per-bar returns over 10K+ bars still produces extreme numbers
(+1.8M% return, 2687% MaxDD). Replaced with:
- Return: mean_per_bar × bars_per_year (annualized, no compounding)
- MaxDD: windowed over last 10K bars AND capped at 1.0 (100%)

These are monitoring metrics — the hyperopt objective uses the backtest
evaluator which already has correct windowed metrics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:06:53 +01:00
jgrusewski
62484e9c59 fix: stop deleting feature cache on every hyperopt run
The feature cache auto-invalidates via content hash — manual rm -f
wasted ~2 minutes of MBP-10 + OFI recomputation per trial start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:03:48 +01:00
jgrusewski
f456dd1220 fix: cap training financials to 10K-bar window (Return/MaxDD were ±trillions%)
Training epoch metrics compound step_returns multiplicatively. With 2M+
bars per epoch on H100, this produces Return=+2.1e24% and MaxDD=99.5% —
meaningless numbers that corrupt risk monitoring and early stopping.

Fixed: total_return and max_drawdown now use only the last 10K bars
(~25 trading days), matching the backtest evaluator's window cap.
Sharpe/Sortino are unaffected (use arithmetic mean/std, not compounding).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:54:38 +01:00