Commit Graph

3046 Commits

Author SHA1 Message Date
jgrusewski
df398b51d0 feat: IQN backward flows gradient to shared trunk (dual gradient source)
The IQN backward kernel now computes dL/d(h_s2) = dL/d(combined) ⊙ embed
and outputs it to d_h_s2_buf [B, hidden_dim]. Previously this was
explicitly NOT computed (comment: "trunk trained by C51").

Now the shared trunk receives BOTH gradient signals:
  C51: dense cross-entropy gradient (can be noisy/steep)
  IQN: bounded Huber quantile gradient (always stable)

The IQN gradient stabilizes trunk training when C51's gradient is steep.
With both signals, the trunk learns from C51's distributional knowledge
AND IQN's risk-aware quantile knowledge simultaneously.

New: d_h_s2_buf allocated in GpuIqnHead, zeroed before backward,
accumulated via atomicAdd across all quantiles per sample.

Accessors added: GpuDqnTrainer::bw_d_h_s2_buf(), shared_h2()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:13:01 +01:00
jgrusewski
54b3382f8c config: lower default LR 1e-4→3e-5 for stability with 72-dim state
With the larger state space (72 dims: market + portfolio + multi-TF)
and complex reward structure, the old 1e-4 default produces steep
loss surfaces and doubling grad_norm per epoch. 3e-5 gives the
optimizer a gentler starting point. Hyperopt still searches [1e-5, 3e-4].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:06:12 +01:00
jgrusewski
23fb59db2b feat: Layer 3 — IQN primary with fixed τ (QR-DQN, CUDA Graph compatible)
Layer 3a: Fixed τ midpoints replace random sampling in IQN.
τ_i = (2i-1)/(2N) for i=1..N, pre-computed in constructor.
Eliminates sample_taus_kernel launch → fully deterministic →
CUDA Graph compatible. IQN becomes equivalent to QR-DQN.

Layer 3b: IQN per_sample_loss replaces C51 td_errors for PER.
GPU DtoD copy — zero CPU. IQN's Huber quantile loss is bounded
by construction (max = kappa²/2 per quantile). This eliminates
the PER feedback explosion that caused C51 loss to reach 242K.

No conditional flag — IQN is always primary when active.
Three-layer defense complete:
  L1: Per-sample CE clamp at 50 (breaks PER loop)
  L2: Label smoothing ε=0.01 on Bellman target (prevents log(0))
  L3: IQN Huber loss for PER priorities (bounded by construction)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:52:20 +01:00
jgrusewski
a51345a141 fix: Layer 1+2 — C51 loss clamp + label smoothing (prevents 242K explosion)
Layer 1: Per-sample CE clamped to MAX_PER_SAMPLE_CE=50 before IS weight.
Breaks PER feedback loop: high loss → high priority → high IS weight → repeat.

Layer 2: Bellman target smoothed with ε=0.01 uniform mix after projection.
Prevents any atom from having zero probability → no log(near-zero) in CE.

Root cause: C51 cross-entropy is unbounded when target and predicted
distributions are maximally misaligned. PER amplifies pathological samples.
These two layers cap the maximum possible CE and prevent the extreme
misalignment from occurring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:45:11 +01:00
jgrusewski
909650f2a1 plan: three-layer stable distributional loss architecture
Layer 1: Per-sample C51 loss clamp (MAX_CE=50) breaks PER feedback loop
Layer 2: Label smoothing (ε=0.01) on Bellman target prevents log(0)
Layer 3: IQN primary with fixed τ (QR-DQN style, CUDA Graph compatible)

5 tasks, each independently testable and committable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:42:08 +01:00
jgrusewski
f5cb953082 fix: gradient clip 10.0→1.0 — prevents grad accumulation with complex reward
grad_norm was growing 5x per epoch (523→2673→13K→67K→NaN). The old
clip at 10.0 allowed gradients to accumulate. With clip=1.0, the
Adam optimizer receives bounded updates.

Trial 2 (TPE-guided params) trains cleanly for 4 epochs:
  train_loss: 4.5→3.3 (decreasing!)
  Q-value: 12-19 (stable)
  grad_norm: 162-567 (bounded)

Trial 1 (random initial params) still NaN's — this is expected and
handled by the hyperopt penalty (1M objective). The optimizer learns
to avoid unstable parameter regions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:40:30 +01:00
jgrusewski
1936c160b5 fix: NaN guards + multi-timeframe feature clamping
Gradient explosion from unbounded multi-timeframe features (240-bar
return could be ±50%). All multi-timeframe outputs now clamped:
- Return: ±10%
- Volatility: 0-10%
- Volume ratio: 0-5x
- Momentum: [0, 1] (already bounded)

NaN guards added:
- Kelly: NaN check on continuous/discrete Kelly before blend
- target_position: final NaN→0 after all scaling
- reward: final NaN→0 before writing to replay buffer

Local test shows Q-gap=18.7 (epoch 5) — 18x better than start-of-session 0.000.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:07:21 +01:00
jgrusewski
b7ddf812cb feat: multi-head attention + Decision Transformer architecture (GPU-only)
P10: Multi-Head Feature Attention (attention_kernel.cu + gpu_attention.rs)
- 4-head self-attention over 72-dim state features
- Learned W_Q, W_K, W_V, W_O projections (21K params, ~82KB)
- Softmax + residual connection + layer normalization
- Xavier initialization, one warp per sample (32 threads)
- Zero CPU: all weights on GPU, kernel launch only

P11: Decision Transformer (decision_transformer.rs)
- Sequence model conditioned on return-to-go (Chen et al., 2021)
- Token embedding: (state + return-to-go + action) → embed_dim
- N transformer layers with causal multi-head attention + FFN
- Context window: 20 timesteps (attend to recent trading history)
- Pre-training on offline walk-forward data, fine-tuning with DQN
- Architecture defined, CUDA transformer kernels to follow
- Config: embed_dim=128, num_layers=3, num_heads=4

Both modules are GPU-only — CudaSlice allocations, kernel launches,
zero memcpy_dtoh in hot path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:01:13 +01:00
jgrusewski
d789bac2a2 feat: add expert demonstration generator for DQN imitation learning warmup
Add ExpertDemoGenerator using MA crossover strategy filtered by ADX trend
strength to produce (bar_index, exposure_action_index) pairs. Fast/slow EMA
crossover with ADX > threshold triggers Long100/Short100 signals, otherwise Flat.
Includes effective_ratio() for linear decay of expert_demo_ratio over training
epochs (controlled by expert_demo_ratio and expert_demo_decay_epochs fields
in DQNHyperparameters). Comprehensive unit tests for EMA, ADX, decay, and
edge cases included.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:54:30 +01:00
jgrusewski
b370b93cc9 feat: add curriculum learning with ADX-based difficulty scoring to walk-forward windows
Add difficulty_score field to WalkForwardWindow computed via compute_difficulty()
which calculates mean ADX(14) over training bars. Add DifficultyPhase enum (Easy/Mixed/Full)
and filter_by_difficulty() for curriculum-based window filtering — Easy phase trains
only on trending markets (ADX>30), Mixed duplicates trending windows for 2x weight,
Full uses all windows equally. The curriculum_phase field in DQNHyperparameters
(committed in previous ensemble commit) controls the active phase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:54:01 +01:00
jgrusewski
0718dabd4c feat: GPU-native multi-timeframe features — 4 windows × 4 features
16 new features computed directly in the state_gather CUDA kernel
from market data already on GPU. No CPU feature engineering.

Lookback windows: 5, 15, 60, 240 bars (≈5min, 15min, 1hr, 4hr)
Features per window:
  - Return over N bars (directional bias)
  - Volatility (high-low range / close)
  - Volume trend (current / N-bar average)
  - Momentum (position within N-bar range [0=bottom, 1=top])

State dim: 66 raw (72 aligned) without OFI, 74 raw (80 aligned) with.
The model now sees price action at 4 timescales simultaneously.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:52:36 +01:00
jgrusewski
140b9426c3 feat: add ensemble_count and ensemble_diversity_weight to DQNHyperparameters
Wire ensemble agent configuration into DQN hyperparameters:
- ensemble_count: number of agents in ensemble (default 1 = no ensemble)
- ensemble_diversity_weight: KL-divergence diversity loss weight (default 0.01)

Both fields default to backward-compatible values (single agent, no
diversity loss) and are plumbed through the conservative() constructor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:52:29 +01:00
jgrusewski
d1b183c6dc feat: add Phase Risk (P4) to four-phase hyperopt — 8D risk parameter search
Add HyperoptPhase::Risk variant that fixes dynamics + architecture from
Phase 2 best params and searches only risk parameters (8D):
kelly_fractional, dd_threshold, loss_aversion, time_decay_rate,
q_gap_threshold, w_dsr, w_pnl, w_dd.

CLI: --phase risk (alongside fast, full, reward)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:51:49 +01:00
jgrusewski
33d91cce5b spec: profitability roadmap — 11 findings from deep research
4 critical root causes fixed this session (stop-loss, dense shaping,
action aliasing, HFT objective weight). 7 remaining improvements
documented with priority order: three-phase hyperopt, behavioral
cloning warm start, curriculum learning, ensemble, multi-timeframe,
attention, Decision Transformer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:45:06 +01:00
jgrusewski
35a5b4f1dc fix: 4 critical root causes killing profitability
ROOT CAUSE 1: Stop-loss 0.3%→1%, take-profit 0.5%→2% (2:1 R:R).
Old 0.3% = 8.3 ticks on ES. Normal 1-min noise is 4-6 ticks.
99.88% of trades were stopped out by NOISE, not by bad entries.

ROOT CAUSE 2: Dense shaping 0.1x→0.01x. Over 50 bars, old dense
signal = 5.0 vs completion ±2.0 — dense dominated. Now dense = 0.5
vs completion ±2.0 — trade completion is the primary signal.

ROOT CAUSE 3: Action aliasing in 5-bar hold override. When model
chose Flat but was forced to Hold, replay stored (state, Flat, Hold's
reward) — corrupting Q-values for Flat. Now overwrites out_actions
with the ACTUAL held exposure action.

ROOT CAUSE 4: Hyperopt HFT activity weight 25%→5%. Old objective
penalized selective trading. MIN_VIABLE_TRADES 100→20.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:44:03 +01:00
jgrusewski
93a64b4e59 refactor: remove dead enable_gpu_experience_collector toggle
GPU experience collection is ALWAYS active in CUDA builds. The boolean
toggle was dead code — no CPU fallback exists. Removed from config,
hyperopt adapter, constructor, smoke tests, and training loop guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:37:52 +01:00
jgrusewski
51a0ef8159 perf: eliminate per-step IQN loss readback — deferred to epoch-end
IQN train_iqn_step_gpu() was doing a synchronous DtoH readback of the
loss scalar EVERY training step. This serializes the GPU pipeline.

Fix: return 0.0 placeholder from train step. Actual loss available via
read_loss() method — call only at epoch boundaries for logging.

Remaining readbacks (all once-per-epoch, acceptable):
- epoch_state: 32 bytes (DSR monitoring)
- q_stats: 20 bytes (Q-value diagnostics)
- gradient accum: dead code path (fused CUDA always active)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:33:16 +01:00
jgrusewski
24ed6a50b4 feat: GPU-native CVaR kernel — eliminated CPU round-trip bottleneck
The compute_cvar_scales() was doing GPU→CPU→sort→CPU→GPU for quantile
CVaR computation. With batch_size=307 × 32 quantiles × 15 actions,
this was ~150KB of DtoH + sort + HtoD per epoch — likely the source
of the 10s/epoch overhead vs 3s baseline.

New inline CUDA kernel (iqn_cvar_kernel):
- One thread per sample (256 threads/block)
- Insertion sort of alpha_count smallest quantiles in registers
- Fast path for alpha_count=1 (just find minimum)
- Zero CPU readback, zero CPU allocation

Compiled once via OnceLock, cached for all subsequent calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:30:31 +01:00
jgrusewski
6172243d8e feat: GPU-native enhanced Kelly criterion — zero CPU involvement
Full Kelly implementation in CUDA kernel, no CPU path:

Enhanced Kelly = 0.7 × continuous (μ/σ²) + 0.3 × discrete ((bp-q)/b)
× confidence scaling (1 - 1/√n_trades)
× half-Kelly (0.5)
Clamped to [0.05, 0.25], normalized to position scale.

Tracks 6 statistics in portfolio state (PORTFOLIO_STRIDE 18→20):
  ps[14] win_count, ps[15] loss_count, ps[16] sum_wins,
  ps[17] sum_losses, ps[18] sum_returns, ps[19] sum_sq_returns

Removed CPU kelly_scale parameter — was a shortcut that violated
the zero-CPU-in-hot-path principle. All Kelly computation now
happens per-step in the env_step kernel from accumulated trade
statistics. Activates after ≥20 trade completions.

Also noted: IQN CVaR compute_cvar_scales() still does a CPU readback
for sorting quantiles. Should be a GPU kernel in next iteration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:27:58 +01:00
jgrusewski
dc2fcac103 feat: GPU-native risk integration — Kelly, sqrt impact, order-type costs
4 priorities from risk module investigation:

P1: Kelly fraction sizing — new kernel parameter kelly_scale (f32).
    Computed per-epoch on CPU, uploaded as scalar. Stacks with
    CVaR × conviction: size = exposure × CVaR × conviction × kelly.

P3: Square-root market impact (Almgren & Chriss 2000) — replaced
    quadratic x² with √x. Academic standard for futures markets.
    Less penalty for moderate sizes, more realistic.

P4: Order-type tx cost differentiation — decode order_type branch
    from factored action. Market=+0bps, IoC=+2bps, LimitMaker=-5bps.
    LimitMaker rebate teaches model to prefer limit orders.

P2 (C51 VaR) deferred — IQN CVaR already provides this signal.

Position sizing chain is now:
  target = exposure × max_pos × CVaR × conviction × kelly
  cost = |delta| × price × (tx_mult × spread × √impact + order_premium)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:22:26 +01:00
jgrusewski
7e52ef4feb feat: conviction-scaled stops — Q-gap widens/tightens TP/SL dynamically
Fixed TP/SL produced bimodal reward (all trades hit exactly -0.3% or
+0.5%), making the reward distribution too deterministic. Model learned
"all actions produce the same bounded return" → Q-values converged.

Fix: stops scale with Q-gap conviction (0.5x to 2.0x of base levels):
- High conviction (Q-gap=2.0): SL=-0.6%, TP=+1.0% (let it run)
- Low conviction (Q-gap=0.5): SL=-0.15%, TP=+0.25% (quick exit)
- Time stop also scales: 50-200 bars based on conviction

This makes the reward distribution CONTINUOUS, not bimodal. High-conviction
entries with wider stops produce different returns than low-conviction entries
with tight stops → the model can learn that conviction matters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:10:44 +01:00
jgrusewski
d12f983b2f fix: SEGV in backtest evaluator — branch_0_size 5→9 in 3 remaining defaults
GpuBacktestEvaluator, GpuDqnTrainer, and hyperopt adapter had hardcoded
branch_0_size=5. Training produced 9-action indices (0-8) but backtest
only allocated 5-action buffers → SIGSEGV (exit 139) during walk-forward
evaluation after Trial 0 training.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:03:33 +01:00
jgrusewski
fec9edb812 feat: conviction sizing + Q-gap output + all 5 trader insights
5 findings from "what real traders know":

1. CONVICTION SIZING — Q-gap from action_select kernel now output to
   q_gaps_buf[N] and passed to env_step. target_position scaled by
   conviction = clamp(q_gap/2.0, 0.25, 1.0). Combined with CVaR:
   size = exposure × CVaR_scale × conviction.

2. ASYMMETRIC ENTRY/EXIT — already works: Q-gap filter only blocks
   entries (flat→trade), never exits (trade→flat). Verified.

3. TRAILING STOP — done in previous commit. Stop trails peak_equity.

4. TIME-OF-DAY — already in 42-dim feature vector (indices ~20-25).
   Stop levels adapt via ADX/CUSUM regime features. Future: explicit
   RTH/ETH multiplier.

5. CHOPPY REGIME BONUS — flat in ADX<20 + CUSUM>0.3 gets +0.02/bar.
   "Good traders know when NOT to trade."

Kernel changes:
- experience_action_select: new out_q_gaps[N] parameter
- experience_env_step: new q_gaps[N] parameter for conviction scaling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:29:52 +01:00
jgrusewski
63310f98bc feat: trailing stop + choppy regime bonus + trade management refinements
Trailing stop: once trade is profitable, stop trails peak unrealized P&L.
If peak return = +0.4%, stop tightens to +0.1% (locks in profit).
Uses peak_equity HWM which already tracks per-bar portfolio maximum.

Choppy regime bonus: when flat in low-ADX (<20) + high-CUSUM (>0.3)
conditions, reward +0.02 per bar. Teaches "good traders know when
NOT to trade." Trending markets (ADX>25) → flat gets zero (missed opp).

Position scaling note: Q-gap-based sizing deferred (requires kernel
parameter change). CVaR scaling already provides risk-based sizing.
Asymmetric entry/exit already works — Q-gap filter only prevents
entries (flat→trade), never prevents exits (trade→flat).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:24:54 +01:00
jgrusewski
ed753a6f17 feat: dynamic trade management — stop-loss, take-profit, regime-adaptive
Three critical fixes from H100 Trial 0 analysis:

1. REMOVE IDLE PENALTY — was making Flat the worst Q-value (Q=-19.4),
   forcing the model to trade on 95% of bars. Now Flat = reward 0.0.
   DSR already penalizes inaction through the Sharpe denominator.

2. TRADE COMPLETION SCALING — ×1000 clamped to ±1 made ALL trades look
   the same (can't distinguish 1-tick from 10-tick return). Now ×200
   with clamp ±2, plus sqrt(hold_time) bonus for holding longer.

3. DYNAMIC TRADE MANAGEMENT — model learns ENTRY quality, exits are
   managed by the system:
   - Stop loss: -0.3% × vol_mult (CUSUM-adaptive, up to -0.75%)
   - Take profit: +0.5% × vol_mult × trend_mult (up to +2.5% in trends)
   - Time stop: 100 bars × vol_mult (up to 250 in volatile markets)
   - Min hold: 5 bars before model can exit voluntarily
   - Auto-exit forces flat and computes trade_completion_reward

   Regime-adaptive via ADX (trend strength) and CUSUM (volatility)
   features already in the GPU memory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:21:33 +01:00
jgrusewski
d405247478 fix: state_dim 45→50/53→58 for PORTFOLIO_DIM=8, num_actions 5→9 in constructor
CRITICAL: The DQN constructor had hardcoded state_dim=45/53 (old PORTFOLIO_DIM=3).
With PORTFOLIO_DIM=8, raw state_dim is 50 (no OFI) or 58 (with OFI).
The network was sized for 56 dimensions but OFI features (8 dims) were uploaded
and ignored because state_dim didn't include them.

Also fixed:
- constructor.rs: num_actions 5→9
- All GPU trainer/head defaults: state_dim 48→56
- metrics.rs: state_dim calculation
- mod.rs test: state_dim assertions
- Argo template reapplied for stale cache clearing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 10:41:29 +01:00
jgrusewski
ad8d5f67eb fix: monitoring kernel + Rust arrays 5→9, buffer 12→16
CRITICAL: gpu_monitoring.rs had [usize; 5] for action_counts but kernel
writes 9 exposure levels → memory corruption. Fixed array, buffer (12→16),
readback size, and the CUDA monitoring_reduce kernel (s_counts[5]→[9],
summary offsets updated). Also fixed remaining 45→81 references in
metrics.rs, training_loop.rs, monitoring.rs, config.rs comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 10:24:50 +01:00
jgrusewski
050407dbd5 fix: 3 critical findings — state blindness, cold-start, reward scaling
Finding 1: STATE BLINDNESS — Q-network couldn't see risk
  Portfolio features expanded from 3 (useless: pv/pv≈1, cash/pv≈1) to 8:
  position, unrealized_pnl/equity, drawdown, hold_time/100, realized_pnl/equity,
  distance_to_floor, trade_return, cash_ratio.
  PORTFOLIO_DIM 3→8 across all NVRTC injection sites.
  State dim: 48→56 (without OFI), 56→64 (with OFI).

Finding 2: Q-GAP COLD-START — model couldn't trade to learn
  Q-gap threshold ramps linearly from 0.0 to target over first 10 epochs.
  At epoch 0, all Q-values ≈ 0 → Q-gap < threshold → no trades → no signal.
  Added current_epoch field to DQNTrainer.

Finding 3: TRADE REWARD SCALING — idle penalty dominated trade signal
  trade_return scaling: ×100 → ×1000 (1 ES tick = 0.36 reward, was 0.036).
  idle penalty max: 0.05 → 0.01 (was competing with 1-tick trade reward).
  Now: trade reward (0.36) >> idle penalty (0.01). Correct incentive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 10:13:27 +01:00
jgrusewski
de39f0bb0e feat: full CVaR wiring — IQN risk sizing flows through training loop
Complete dual distributional pipeline:
1. After training: fused_ctx.compute_cvar_device_ptr() runs IQN forward
2. CVaR at α=5% computed per sample → [0.25, 1.0] position scaling
3. Device pointer set on collector via set_cvar_scales()
4. Next epoch: env_step kernel applies target_position *= cvar_scales[i]

C51 picks direction (argmax expected Q), IQN sizes risk (CVaR tail).
High tail risk → smaller position. Safe distribution → full size.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 10:01:50 +01:00
jgrusewski
170c9312e5 feat: CVaR position scaling kernel wiring — env_step accepts cvar_scales
- experience_env_step kernel: new cvar_scales parameter (NULL = no scaling)
- target_position *= cvar_scales[i] when buffer is non-NULL
- GpuExperienceCollector: cvar_scales_ptr field + set_cvar_scales() setter
- Default: NULL (0) = no scaling until IQN CVaR is wired from training loop

The GpuIqnHead.compute_cvar_scales() produces the buffer, the collector
passes it to the kernel. Full wiring through training_loop.rs is the
remaining step — needs the collector to receive the device pointer from
the fused training context after each IQN training step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:54:53 +01:00
jgrusewski
da7734b877 feat: IQN compute_cvar_scales() — CVaR position scaling from quantiles
New method on GpuIqnHead: samples τ values, runs forward-only kernel,
computes CVaR at α=5% per sample, returns [0.25, 1.0] scaling factors.
CVaR ≥ 0 (safe) → full position. CVaR < 0 (risky) → reduced position.

Next: wire into env_step kernel for position scaling during experience
collection, and into fused_training.rs to call after IQN training step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:49:48 +01:00
jgrusewski
727ed0d43b refactor: remove use_qr_dqn — IQN always enabled via iqn_lambda
use_qr_dqn was a hacky toggle that gated the IQN dual-head behind
a num_atoms threshold. IQN is now always enabled when iqn_lambda > 0
(default 0.25). C51 remains the main loss; IQN is the auxiliary head
for CVaR risk quantification.

Removed use_qr_dqn from: DQNParams, DQNHyperparameters, fused_training,
hyperopt adapter, all tests, all examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:46:41 +01:00
jgrusewski
889d9263f4 feat: monitoring + metrics arrays 5→9, factored 45→81
Updated all fixed-size arrays across monitoring.rs, financials.rs,
metrics.rs, training_loop.rs from [_;5]→[_;9] and [_;45]→[_;81].
DIRECTION_LUT expanded to 9 levels (-1.0 to +1.0 in 0.25 steps).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:41:14 +01:00
jgrusewski
51037f60b8 spec: trading realism conformance — 7 real-market requirements
RTH/ETH cost differential, market impact (done), book depth fill quality,
macro events, weekend gap risk, margin utilization feature.
All configurable via TOML. We trade against real markets — simulation must match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:35:55 +01:00
jgrusewski
ac61fbb8f8 feat: quadratic market impact — larger trades cost more
1-lot fills at bid/ask. 4-lot moves the market.
impact_scale = 1 + (|delta|/max_position)^2, ranges [1.0, 2.0].
Teaches the model that max-size positions are 2x more expensive to enter.
Makes the 9-exposure granularity meaningful — 25% positions are cheaper
to enter/exit than 100% positions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:34:11 +01:00
jgrusewski
a247c77543 feat: capital floor protection — 25% max drawdown terminates episode
PDT $25K rule protection: if equity drops below 75% of peak_equity,
the episode terminates (done=1) with a -1.0 penalty reward and forced
flat. The model learns that approaching the floor = game over.

Uses peak_equity (not initial_capital) so it adapts as account grows.
With $35K initial, floor triggers at ~$26.25K — above the $25K minimum.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:33:03 +01:00
jgrusewski
235323d34b feat: reward v3 — trade-completion signal + dense shaping
The model thinks in TRADES, not in bars. Per-bar 1-minute noise is like
flipping a coin — the real signal is the full trade return (entry→exit).

Three reward levels:
1. TRADE EXIT (sparse, strong): full trade return entry→exit, the PRIMARY
   learning signal. 10x stronger than dense shaping.
2. IN TRADE (dense, weak): 0.1x DSR + PnL shaping per bar. Just enough
   gradient for direction, doesn't overwhelm the completion signal.
3. FLAT (near-zero): doing nothing is almost free.

New portfolio state fields (PORTFOLIO_STRIDE 12→14):
- ps[12] = entry_price (price at trade entry)
- ps[13] = trade_start_pnl (cumulative PnL snapshot at trade entry)

Trade lifecycle detection: entering_trade / exiting_trade / in_trade
from position transitions (was_flat→positioned / positioned→is_flat).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:31:15 +01:00
jgrusewski
66dfcf599f feat: 9-exposure branch_sizes + num_actions defaults across workspace
- gpu_experience_collector: branch_sizes [5,3,3]→[9,3,3]
- gpu_iqn_head: branch_0_size 5→9
- All DQN configs: num_actions 5→9
- Production TOML: branch_0_size=9
- Removed use_branching from TOMLs (always enabled)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:24:55 +01:00
jgrusewski
d0c11574f4 feat: ExposureLevel 5→9 variants (Short75/Short25/Long25/Long75)
9 levels: -100%, -75%, -50%, -25%, 0%, +25%, +50%, +75%, +100%
Flat index: 2→4 (center). Factored action space: 45→81.
All match exhaustiveness errors fixed, 300 core tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:21:58 +01:00
jgrusewski
e04edc0a84 feat: 9-exposure CUDA kernels — formula-based fraction, generalized flat_idx
- common_device_functions.cuh: DQN_NUM_ACTIONS 5→9, TOTAL_ACTIONS 45→81
- experience_kernels.cu: exposure_idx_to_fraction() formula (-1+idx*0.25)
- experience_kernels.cu: action_to_exposure() same formula
- backtest_env_kernel.cu: switch→formula for 9-level mapping
- All Q-gap flat_idx: hardcoded 2 → b0_size/2 (generalized)
- c51_loss_kernel.cu: MAX_BRANCH_SIZE 5→9, BRANCH_0_SIZE 5→9

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:11:54 +01:00
jgrusewski
1277e8d10e spec: dual distributional C51+IQN — CVaR position sizing + uncertainty gating
C51 for action selection (expected Q), IQN for risk quantification (CVaR).
Disagreement between C51 and IQN expected Q = uncertainty signal.
Architecture already 90% built — IQN head exists but output unused.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:05:22 +01:00
jgrusewski
98afee0a51 plan: 9-exposure action space + dual C51/IQN distributional architecture
Phase A: expand exposure 5→9 (25% steps, 81 factored actions)
Phase B: wire existing GpuIqnHead for CVaR position scaling —
C51 picks direction, IQN sizes the risk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:03:19 +01:00
jgrusewski
93dc7f811e fix: QR-DQN threshold 100→200 — num_atoms=101 was triggering dual C51+QR-DQN
With num_atoms=101 (just above the old threshold of 100), QR-DQN activated
alongside C51, doubling distributional computation. H100 epochs went from
~2s to ~35s. Raised threshold to 200 so num_atoms=101 uses C51 only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:49:37 +01:00
jgrusewski
739dd986bb spec: 9-exposure action space design — expand from 5 to 9 levels
25% step increments: {-100%, -75%, -50%, -25%, 0%, +25%, +50%, +75%, +100%}
Total factored actions: 81 (was 45). Branching Q-values: 15 (was 11).
Implementation after reward v2 H100 validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:45:18 +01:00
jgrusewski
e8a3104159 feat: DSR warm-up, num_atoms=101, clear stale cache on H100
- DSR warm-up: skip first 50 steps when EMA has insufficient history
- Phase Fast num_atoms: 51 → 101 (H100 can afford finer resolution,
  1.19 per atom vs 2.35 — critical for distinguishing Q-values)
- Argo template: clear stale feature cache before hyperopt (ensures
  fresh computation with VPIN/trades enrichment)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:32:48 +01:00
jgrusewski
4f5daaa755 fix: Q-gap filter in all action selection kernels + C51 boundary + NaN guards
Bug hunt findings:
- epsilon_greedy_kernel.cu: all 3 kernels (select, routed, branching) lacked
  the Q-gap conviction filter. Training learned with q_gap=0.1 but inference
  paths bypassed it entirely. Now all action selection paths are consistent.
- c51_loss_kernel.cu: Bellman projection boundary fix — b_pos clamped away
  from exact NUM_ATOMS-1 to prevent phantom upper==lower atom collapse.
- experience_kernels.cu: NaN/Inf guards on DSR and normalized PnL outputs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:17:35 +01:00
jgrusewski
35d417f08e chore: remove accidental local test artifacts from ml/ dir
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 00:40:01 +01:00
jgrusewski
0b37ff77b0 fix: reward v2 + dynamic C51 support — root cause of Q-value collapse
ROOT CAUSE: 5 interlocking bugs made learning impossible:
1. DSR denominator floor 1e-12 produced values in millions → drowned all signal
2. Global [-1,+1] clamp destroyed Bellman equation signal (can't distinguish
   catastrophic loss from mild loss)
3. v_range=20 exactly equals V_max for gamma=0.95 → Bellman target pins at
   ceiling → Q-values saturate → Q-gap collapses to 0.0000
4. num_atoms=11 over 40-unit range = 4.0 per atom (C51 paper min is 51)
5. 6/7 reward components were penalties → mean_reward=-0.311 regardless of action

FIXES:
- DSR denominator floor: 1e-12 → 0.01 (prevents million-scale spikes)
- Each component individually clamped BEFORE weighting (DSR to [-1,+1],
  z-score to [-3,+3], drawdown to [0,1], time decay to [0,0.3])
- Removed global [-1,+1] clamp (no longer needed with bounded components)
- profit_take_bonus: 0.1 → 0.01 (was 100x too large, caused reward hacking)
- Removed confidence scaling (positive feedback loop destabilized learning)
- Removed regime scaling (non-stationary reward confused the model)
- Dynamic v_range from gamma: v_range = 2.5/(1-gamma)*1.2 (always covers Q range)
- num_atoms minimum: 11 → 51 (C51 paper standard)
- gamma default: 0.99 → 0.95

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 00:39:39 +01:00
jgrusewski
95d9fdb034 config: set q_gap_threshold=0.1 as default — force trade selectivity
Q-gap was 0.0 (disabled) meaning the model traded on every bar regardless
of conviction. With 0.1, the model must have Q(best) - Q(flat) > 0.1
before entering a position. Local test showed 21% fewer trades.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:45:41 +01:00
jgrusewski
de21ea514d fix: include MBP-10 + trades dirs in feature cache key
The DBN feature cache was keyed ONLY on OHLCV .dbn files. If a cache was
created without trades data (VPIN/Kyle's Lambda), subsequent runs with trades
would silently serve stale features from cache, dropping VPIN enrichment.

Now cache key hashes OHLCV + MBP-10 + trades dirs together. Adding or removing
data sources invalidates the cache correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:31:05 +01:00