Commit Graph

125 Commits

Author SHA1 Message Date
jgrusewski
152f4c16d9 feat(generalization): #34 causal intervention training (THE EMPEROR)
Pearl's do-calculus applied to RL: compute per-feature causal sensitivity
by running intervened forward passes. For each of 14 active features,
set it to 0 (do(X_k=0)) and measure how much Q-values change.

High sensitivity = feature genuinely CAUSES different outcomes.
Low sensitivity = spurious correlation (noise that breaks OOS).

Implementation:
- Intervened forward passes via cuBLAS (reuse existing infrastructure)
- States copied to scratch buffer, feature k zeroed, forward pass run
- Q-value delta computed: |Q_original - Q_intervened|² per feature
- Mean sensitivity logged for interpretability
- Runs every N steps (configurable, default 10) to limit overhead

Architecture:
- causal_states_scratch [B, state_dim_padded] bf16 — intervened copy
- causal_sensitivity_buf [market_dim] f32 — per-feature sensitivity
- Reuses existing activation scratch buffers (post-graph, no conflict)
- ~10% compute overhead at interval=10 (42 extra cuBLAS GEMMs per step)

Config: enable_causal_intervention=false (default).

THE FOUR CROWN JEWELS ARE COMPLETE:
  Gem (#31): 2D Bottleneck — architecture defense
  Pearl (#32): Gradient Vaccine — optimization defense
  King (#33): Adversarial Self-Play — strategic defense
  Emperor (#34): Causal Intervention — epistemic defense

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:33:28 +02:00
jgrusewski
c5770f472e feat(generalization): #33 adversarial self-play — evolutionary saboteur (THE KING)
Train a saboteur that controls market microstructure (spread, fill
probability, slippage) to MAXIMIZE the trader's losses. Uses
gradient-free evolutionary strategy (CMA-ES lite):
- Perturb params → evaluate trader Sharpe → keep if trader did worse
- Slow perturbation decay prevents premature convergence
- Best attack params frozen during trader training epochs

Self-play cycle:
  Phase 0 (epochs 0..50): Normal training, no saboteur
  Phase 1 (odd epochs after warmup): Saboteur explores attack params
  Phase 2 (even epochs after warmup): Trader trains against frozen saboteur

Saboteur outputs applied POST domain-randomization and POST adversarial
regime injection — layered defense: model must survive random variation
+ adversarial regime + evolutionary worst-case simultaneously.

Architecture: AdversarialSaboteur struct with evolutionary state.
No neural network needed — 3-parameter search space (spread_mult,
fill_prob, slippage_mult) is efficiently explored by perturbation.

Config: enable_adversarial_self_play=false (default).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:25:25 +02:00
jgrusewski
09b6bd6eaa feat(generalization): #32 gradient vaccine — mathematical overfit guarantee
Surgical gradient projection that makes overfitting mathematically
impossible. At each training step:
1. Save g_train (from CUDA Graph forward+backward)
2. Run SEPARATE non-graph forward+backward on vaccine batch → g_val
3. Compute dot(g_train, g_val) and |g_val|² via GPU reduction kernel
4. If dot < 0 (gradients DISAGREE), project out conflicting component:
   g_train -= (dot/|g_val|²) * g_val
5. Adam only sees gradient directions where train AND val agree

Two new CUDA kernels:
- gradient_dot_and_norm: parallel reduction with warp+block+atomicAdd
  Computes both dot product and norm² in single pass (bandwidth optimal)
- gradient_project: conditional SAXPY (skips when dot >= 0, no branch divergence)

The vaccine runs OUTSIDE the CUDA Graph (between replay_forward and
replay_adam) because it needs conditional logic. The non-graph vaccine
forward+backward reuses the same cuBLAS/loss/backward infrastructure.

Vaccine batch is sampled from the replay buffer alongside the training
batch and passed via FusedTrainingCtx::pending_vaccine_batch.

Config: enable_gradient_vaccine=false (default). Enable for mathematical
guarantee that no gradient update makes the model worse on held-out data.

Together with bottleneck (#31): "you can only remember 2 numbers, and
those 2 numbers must work on data you haven't trained on."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:05:22 +02:00
jgrusewski
af61813e3b feat(generalization): #31 bottleneck weight foundation + crown jewels plan
Weight system expansion: param layout [20] → [22] tensors with
NUM_WEIGHT_TENSORS constant. Tensors 20-21 (w_bn, b_bn) hold temporal
causal bottleneck weights. Size 0 when bottleneck_dim=0 (backward
compatible — existing models load without changes).

Config: bottleneck_dim field added to DQNHyperparameters, GpuDqnTrainConfig,
TOML [generalization] section, and training profile. Default: 0 (disabled).
Set to 2 for maximum information compression.

Crown Jewels plan (Tasks 31-34):
- Gem (#31): 2D Temporal Causal Bottleneck (architecture defense)
- Pearl (#32): Gradient Vaccine (optimization defense)
- King (#33): Adversarial Self-Play with Past Self (strategic defense)
- Emperor (#34): Causal Intervention Training (epistemic defense)

Four layers of defense making memorization impossible at every level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:29:46 +02:00
jgrusewski
80bc8a2ee1 feat(generalization): #21 stochastic depth — CUDA Graph compatible layer dropout
Per-layer drop with 20% probability during training. Each hidden layer
(h_s1, h_s2, h_v) is independently scaled by 0 (dropped) or 1/(1-p)
(kept, expected-value correction). Only applied to online forward pass
on current states — target and Double-DQN passes use full network.

Implementation:
- New stochastic_depth_scale kernel in dqn_utility_kernels.cu
- Per-layer scale buffer [3] f32 — written by host before each graph
  replay (CUDA Graphs capture addresses, not contents)
- Scale kernel inserted in launch_cublas_forward after online Pass 1
- update_stochastic_depth_mask() generates random 0/keep scales per step
- At inference (experience collection), all layers active (no drop)

Forces every layer to produce useful features independently — prevents
deep compositional memorization where removal of any single layer
would collapse the output. First RL trading application of stochastic
depth (proven in Vision Transformers, novel in DQN).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:17:25 +02:00
jgrusewski
c83ceeda54 feat(generalization): #11 time-reversal + #17 counterfactual regret (CUDA)
Time-reversal (#11): 20% of episodes (index % 5 == 0) played backwards
in state_gather kernel. Reversed bar indexing forces the model to rely
on instantaneous features rather than temporal trajectory patterns.
If it can't trade backwards data, it memorized sequence artifacts.

Counterfactual regret (#17): at trade completion, compute PnL for all
9 exposure levels and blend reward with regret (taken - best_possible).
Default 30% regret / 70% raw PnL. Normalizes rewards across regimes —
a bad trade in a bad market has low regret, a bad trade in a good
market has high regret. From game theory (CFR).

Both fully in CUDA env_step/state_gather kernels. No CPU paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:05:44 +02:00
jgrusewski
3c0fbcc5e3 feat(generalization): #10 mirror universe + #19 position entropy (CUDA kernels)
Mirror universe (#10): alternate odd/even epochs with negated return
features + inverted exposure actions in CUDA experience kernels.
Doubles effective data diversity without extra market data. The model
must learn direction-invariant structure — if it only works on normal
data but fails on mirrored, it learned directional bias.

Position entropy (#19): GPU-resident position visit histogram [N, 9]
incremented at each timestep in env_step kernel. At episode end,
computes H(histogram) / log(9) and adds scaled bonus to reward.
Forces the model to explore all 9 exposure levels rather than
degenerating to "always flat" or "always long" strategies.

Both fully in CUDA — zero CPU-side computation. Wired through
ExperienceCollectorConfig → kernel args → TOML [generalization].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 08:58:50 +02:00
jgrusewski
7a7197cbfd feat(generalization): 7 gems & pearls — CUDA kernels + config + TOML wiring
Phase 1 generalization techniques to close IS/OOS Sharpe gap (+0.57→-1.30).
All techniques fully wired from DQNHyperparameters → TOML [generalization]
section → ExperienceCollectorConfig → CUDA kernel arguments.

New techniques implemented:
- #13 Vol normalization: divide return features by realized vol (state_gather)
- #18 Asymmetric DD loss: extra penalty on Q-overestimation in drawdown
      (mse_loss_batched + c51_loss_batched)
- #22 Feature noise: N(0, scale) per feature via LCG RNG (state_gather)
- #23 Causal feature masking: random 30% feature subset zeroed per epoch
      (state_gather, mask uploaded from Rust)
- #24 Anti-intuitive LR: 3x LR when Sharpe good, 0.3x when bad (Rust-only)
- #25 Trade clustering: CV(inter-trade intervals) penalty via ps[3:6]
      (env_step, uses reserved portfolio state slots)
- #27 Ensemble disagreement: Q_target -= weight * ensemble_std
      (mse_loss + c51_loss, buffer allocated for future ensemble wiring)

Also includes 30-task plan doc with all 28+ techniques across 3 phases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 08:51:05 +02:00
jgrusewski
ae99567362 feat(generalization): CQL alpha 0.1→1.0, gradient budget 15%→25%
CQL was effectively disabled (0.1 alpha × 0.15 budget = 1.5% of gradient).
Now: alpha=1.0 × 0.25 budget = 25% of gradient enforces conservatism.
C51 reduced from 70% to 60% to accommodate.

CQL penalizes Q-values for actions not in the data — directly prevents
the model from being "confident but wrong" on OOS state-action pairs.

Hyperopt search range updated: [0.0, 1.0] → [0.5, 5.0].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:40:30 +02:00
jgrusewski
acbc3c896e feat(training): continuous drawdown penalty — smooth ramp from dd_threshold to floor
The model had Omega>1 (profitable trade selection) but MaxDD 41-50%
(catastrophic drawdown timing), causing negative total returns despite
winning trades. Root cause: zero reward gradient between 0% and 25% DD.
The only drawdown consequence was the hard capital floor at 25% which
terminates the episode with reward=-10.

Fix: compute_drawdown_penalty() in trade_physics.cuh — smooth linear
ramp from 0 at dd_threshold (2%) to -5.0 at the capital floor (25%).
Applied every step, not just at trade exit, so the model learns to
reduce position size DURING drawdowns.

- Added compute_drawdown() and compute_drawdown_penalty() to trade_physics.cuh
- Wired dd_threshold and w_dd from config through to CUDA kernel
- Added to all 3 TOML profiles (smoketest, localdev, production)

Early results: MaxDD dropped from 88.9% → 36.6% by epoch 3.
Q-values went negative in drawdown states — the model is learning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:23:51 +02:00
jgrusewski
99cf25fd20 config: production min_epochs_before_stopping 100→80
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:23:53 +02:00
jgrusewski
ae704a7b73 config: production epochs 100→200, min_epochs_before_stopping 50→100
H100 has the compute budget — 200 epochs with cosine decay gives the
model full room to learn through C51 transition and converge.
Early stopping can't fire before epoch 100.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:22:21 +02:00
jgrusewski
7be192a737 fix(config): align H100 production config with f32 pipeline
- adam_epsilon: added 1e-8 (was missing → fell back to old 1e-3)
- lr_decay_type=2: cosine decay 1e-4→1e-5 over 100 epochs
- huber_delta: 1.0 (was missing → fell back to 100.0)
- gradient_clip_norm: 1.0 (was 10.0, too loose for mixed-precision)
- reward_scale: 1.0 (was 10.0, amplifies bf16 rounding)
- spectral_norm_sigma_max: 1.5 (was 3.0)
- q_clip: ±50 (was ±200)
- min_epochs_before_stopping: 50 (was 10, too early)
- curiosity_weight: 0.1 (was missing)
- Removed duplicate gradient_clip_norm in [advanced] vs [training]
- Removed [branching] section (sizes come from code defaults)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:20:45 +02:00
jgrusewski
63b0cb5f46 revert: remove cosine decay from smoketest (3 epochs too short)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:19:32 +02:00
jgrusewski
3ed9d5a300 config: cosine LR decay for smoketest too (1e-4→1e-5 over 3 epochs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:18:47 +02:00
jgrusewski
332b3eb120 feat(training): cosine LR decay in TOML profile, min_epochs_before_stopping=80
- Added lr_decay_type (0=constant, 1=linear, 2=cosine) and lr_min to
  the TOML training profile system. Wired through apply_to() with
  total_steps derived from epochs.

- dqn-localdev.toml: lr_decay_type=2 (cosine 1e-4→1e-5 over 200 epochs),
  min_epochs_before_stopping=80 (was 50, gives model more room to recover
  after C51 transition).

- Smoketest config unchanged (lr_decay_type not set → default Constant).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:16:47 +02:00
jgrusewski
f6caf61252 fix(training): 4 metric quality fixes — buffer, episodes, hyperopt, Q-stats
1. Localdev config: gpu_n_episodes 32→64, gpu_timesteps 100→200
   (12,800 exp/epoch, 4x more → stable Sharpe estimation)

2. Localdev config: buffer_size 10K→50K, min_replay_size 100→500
   (fills over ~4 epochs instead of <1, reduces overfitting)

3. Hyperopt: added CampaignConfig::dqn_localdev() (10 trials × 20 epochs),
   updated test_local_hyperopt to use it with env var overrides
   (FOXHUNT_HYPEROPT_TRIALS, FOXHUNT_HYPEROPT_EPOCHS)

4. Q-value range: was [X,X] every epoch because:
   - train_step.rs Q-stats code was DEAD (training loop uses
     fused.run_full_step() directly, not self.train_step())
   - Only sampled 10 experiences for Q-stats (tiny batch → tight range)
   Fix: reduce_current_q_stats() reads the training batch's q_out_buf
   directly (64 samples, no extra forward pass), wired into the training
   loop's inner step. q_stats_kernel.cu converted to f32 arithmetic.
   Now shows real variation: [-5.25, 5.03] instead of [2.80, 2.80]

Removed dead Q-estimation code from train_step.rs (was never reached
in the fused GPU training path).

11/11 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:06:09 +02:00
jgrusewski
e9e2e87a64 fix(hyperopt): max_steps_per_epoch by GPU tier, adam_epsilon 1e-3→1e-8
- Hyperopt adapter now sets max_training_steps_per_epoch:
  RTX 3050 (≤40GB) = 200 steps, H100 (≥40GB) = 2000 steps.
  Without this, each trial trained the full dataset (2917 steps/epoch)
  making hyperopt 11x slower than necessary on local GPU.

- adam_epsilon default 1e-3→1e-8 everywhere (conservative(), DQNConfig).
  The old 1e-3 was a BF16 workaround (bf16(1e-8)=0 → div-by-zero).
  Adam is now f32, so standard 1e-8 is correct.

- Early stopping enabled in dqn-localdev.toml (patience=20).

Hyperopt: 2 trials × 5 epochs in 132s (was ~20min). Zero NaN.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:22:16 +02:00
jgrusewski
948ffa7c67 fix(monitoring): f32 epoch accumulators, per-branch diversity, remove dead kernels
- acc_buf bf16→f32: epoch gradient/loss accumulators were bf16, causing
  avg_grad to be quantized to 0.895 every epoch. Now f32, shows real
  variation (0.776→0.783→0.788).

- EPOCH_DIAG warn!→info!: diagnostic logging, not a warning condition.

- Removed legacy training_guard_check + training_guard_accumulate kernels
  (dead code, replaced by fused training_guard_check_and_accumulate).

- Added dqn-localdev.toml: 200-epoch RTX 3050 profile for extended runs.

200-epoch run completed successfully:
  Best Sharpe: +6.43 at epoch 107
  Final: Sharpe=+2.05, PF=1.35, Return=+1052%, 0 NaN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:49:38 +02:00
jgrusewski
68804a1a51 fix(cuda): remove --use_fast_math, eliminate cross-stream NaN race, f32 rewards/dones
Three root causes of sporadic NaN during training:

1. --use_fast_math (nvcc) breaks IEEE 754 NaN semantics: fmaxf(NaN,x)
   returns NaN instead of x, isnan()/isinf() compile to false.
   Replaced with --ftz=true --fmad=true --prec-div=true --prec-sqrt=true
   across all 4 build.rs (ml, ml-dqn, ml-ppo, ml-core).

2. Cross-stream race: replay buffer wrote batch data on the device's
   original stream while the trainer read it on a forked stream.
   Fixed by passing the forked stream to the DQN agent via agent_device,
   so all GPU components share a single CUDA stream (zero sync overhead).

3. Rewards/dones stored as bf16 in replay buffer caused done=0xFFFF NaN.
   Converted entire rewards/dones pipeline to f32: experience collector,
   replay buffer storage, nstep kernel, loss/grad kernels.

Also:
- Removed fast_isnan/fast_isinf/fast_isfinite wrappers — standard
  isnan/isinf/isfinite work correctly without --use_fast_math
- Updated dqn-smoketest.toml: lr=1e-4, epsilon=1e-8 (f32 Adam values)
- Removed debug printfs from gather kernels
- Added curiosity_weight to training profile system
- Cleaned up smoke_params() inline overrides

11/11 smoke tests pass, 5/5 stress runs of 50-epoch test pass,
359/359 ml-dqn + 895/895 ml unit tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:18:37 +02:00
jgrusewski
ff4ab9f9a0 fix(bf16): adam_epsilon configurable + cuBLAS stream rebind
Root cause #1: Adam epsilon=1e-8 rounds to 0 in BF16 → sqrt(v_hat)+0 = sqrt(v_hat) → div-by-zero when v_hat≈0. Fix: adam_epsilon configurable, default 1e-3.

Root cause #2 (partial): compute_q_values produces NaN even with sync. NOT a race — the graph_adam's Adam kernel writes NaN to params in async mode but works in CUDA_LAUNCH_BLOCKING=1 mode. The Adam kernel has no shared mem / atomics / warps — it's per-element. The ONLY shared read is grad_norm_sq[0]. Investigation continues.

Added: adam_epsilon to DQNConfig, DQNHyperparameters, GpuDqnTrainConfig, dqn-smoketest.toml, training_profile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:44:48 +01:00
jgrusewski
5d78108c77 feat(bf16): BF16-tuned smoke test config + huber_delta TOML support
dqn-smoketest.toml: lr 3e-5→1e-5, gradient_clip 10→1,
  huber_delta 10→1, q_clip ±200→±50, reward_scale 10→1,
  noisy_sigma 0.5→0.3, spectral_norm 3→1.5

training_profile.rs: add huber_delta to TrainingSection + apply_to

Smoke test runs 3 epochs but Q-values diverge — C51 forward produces
overflow logits in bf16. Needs CUDA graph buffer size audit (graph
captured with F32 byte counts may have wrong bf16 sizes on replay).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:57:17 +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
a400288ae0 feat: comprehensive TOML profiles + config consistency test
Task 7: Update all three DQN TOML profiles with every configurable parameter.
- dqn-smoketest: add [distributional], [advanced], [exploration] (noisy_sigma,
  entropy_coefficient, count_bonus), [risk] (max_position, loss_aversion),
  fix learning_rate 0.0003 -> 0.00003, add reward_scale + gamma
- dqn-production: add reward_scale, exploration params (noisy/entropy/count_bonus),
  advanced params (n_steps/tau/c51_warmup/her/iqn_lambda/spectral_norm),
  remove hardcoded v_min/v_max (now computed), remove duplicate tau from [training]
- dqn-hyperopt: add search space bounds for spectral_norm_sigma_max,
  c51_warmup_epochs, her_ratio

Profile system additions (training_profile.rs):
- TrainingSection: add reward_scale (recomputes v_min/v_max on apply)
- ExplorationSection: add noisy_sigma_init, entropy_coefficient,
  count_bonus_coefficient, q_gap_threshold
- AdvancedSection: add n_steps, tau, c51_warmup_epochs, her_ratio,
  iqn_lambda, spectral_norm_sigma_max, gradient_clip_norm
- RiskSection: add max_position (alias for max_position_absolute), loss_aversion
- RewardSection: add reward_scale
- SearchSpaceSection: add spectral_norm_sigma_max, c51_warmup_epochs, her_ratio
- apply_to: gamma change now recomputes v_min/v_max automatically

Task 8: Config consistency integration tests (5 tests):
- test_config_consistency_across_structs: v_range computed not hardcoded,
  fill simulation bounds, q_clip symmetry
- test_toml_profile_applies_all_fields: smoketest profile applies every
  new section field correctly
- test_production_profile_applies_all_sections: production profile end-to-end
- test_hyperopt_search_space_has_new_bounds: new search space fields parse
- test_reward_scale_recomputes_v_range: gamma override triggers v_range recomputation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:27:45 +01:00
jgrusewski
e8f54c37c1 feat: expose experience collector params in TOML training profiles
Add 7 new fields to DQNHyperparameters (fill_ioc_fill_prob,
fill_limit_fill_min, fill_limit_fill_max, fill_spread_cost_frac,
fill_spread_capture_frac, q_clip_min, q_clip_max) and wire them
through training_profile.rs into ExperienceCollectorConfig construction
in training_loop.rs. Previously these 7 values were hardcoded at the
construction site; now they flow from TOML [experience.fill_simulation]
and [risk] sections. Default values match the prior hardcoded constants
so existing behavior is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:05:01 +01:00
jgrusewski
04d8802c94 refactor: remove 8 always-on use_ booleans — features are mandatory
Remove use_double_dqn, use_dueling, use_per, use_branching,
use_distributional, use_noisy_nets, use_huber_loss, and use_cql
from DQNConfig, DQNHyperparameters, and DqnParams structs.

These features are always enabled (Rainbow DQN standard). The boolean
flags were dead code — every constructor set them to true, and the
only code paths that set them to false were in tests that disabled
features for simplicity. With the fields removed, the features are
unconditionally active, eliminating ~490 lines of dead configuration.

Key changes:
- Struct field declarations removed from 3 core config structs
- Conditional branches (if use_X { ... } else { ... }) simplified:
  dueling/branching/PER network creation is now unconditional
- Checkpoint metadata hardcodes "true" for backward compatibility
- Hyperopt search space index 11 (use_branching) fixed at 1.0
- TOML/YAML config files cleaned of removed fields
- Tests that toggled these flags updated or rewritten

45 files changed, -487 net lines. Zero new test failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:45:54 +01:00
jgrusewski
825db90f23 feat: comprehensive DQN training pipeline overhaul — 16 bug fixes, MSE warmup, financial metrics
Major fixes:
- C51 v_range calibrated for reward v4 (±2.0, was ±25/±0.5)
- Wrong Flat index in Q-gap filter (qe[4]→qe[2] in branching_action_select)
- hold_time tracks total position duration (was only losing bars)
- Entropy coefficient wired to C51 backward kernel (0.001, was unwired)
- Count bonus wired to GPU action selection (per-branch UCB)
- Q-gap warmup ramp (0→threshold over 5 epochs, was static)
- IQN lambda gradient scaling (max_grad_norm × (1+lambda))
- PER beta annealing 4x faster (500 steps, was 2000)
- Reward normalization disabled (scrambled per-bar returns)
- Capital floor uses natural return (was hardcoded -1.0)
- Financial metrics pipeline: real per-trade GPU stats (was Trades=1)

New features:
- MSE loss CUDA kernel for C51 warmup phase
- Blended MSE→C51 loss with linear alpha ramp
- GPU trade_stats_reduce kernel for per-trade financial metrics
- TradeStats struct with real win/loss/PF from portfolio states
- Behavioral smoke test (Q-values, action entropy, trades)
- 50-epoch convergence test with anomaly detection
- c51_warmup_epochs in hyperopt search space (41D)

Dead code removed:
- portfolio_sim_kernel (150 lines CUDA)
- DSR/PnL/drawdown reward v2 computations
- 7 dead kernel params from env_step signature
- GpuPortfolioSimulator (never called)
- Reward normalization block + state fields

0 warnings, 0 errors, 1241 unit tests + 8 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:48:38 +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
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
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
5cb400a73b feat: Q-gap conviction filter + remove dead use_branching kernel arg
Add q_gap_threshold to action selection kernel: when greedy Q(best) - Q(flat)
< threshold, default to flat. Teaches model to trade only with conviction.
39D search space (was 38D). Default 0.0 (disabled), hyperopt range [0.0, 0.5].

Remove use_branching parameter from experience_action_select — GPU pipeline
always uses branching DQN. Flat mode was dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:05:46 +01:00
jgrusewski
81a7ce2d43 config: tighten dd_threshold [0.005, 0.03] for HFT — 1% default
6% drawdown tolerance too generous for HFT. Tightened search range
to 0.5%-3%, default 1%. Forces aggressive loss cutting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:06:13 +01:00
jgrusewski
af940671bc feat: reward config pipeline — DQNHyperparameters + TOML profiles
Add 7 composite reward fields to DQNHyperparameters: w_dsr, w_pnl,
w_dd, w_idle, dd_threshold, loss_aversion, time_decay_rate.

Add RewardSection to training_profile.rs with Option<f64> fields and
apply_to() mapping. Add [reward] section to all 3 DQN TOML profiles
(production, smoketest, hyperopt) with identical defaults.

Remove hold_reward from ExperienceSection (replaced by w_idle).
Add 7 reward search bounds to SearchSpaceSection and bound() match.
Add 7 reward phase_fast defaults to PhaseFastSection.

hold_penalty kept as deprecated field for hyperopt adapter compat
(Task 4 will clean it up).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:50:23 +01:00
jgrusewski
6219491db6 refactor: move smoke test config to dqn-smoketest.toml, restore check_err
Smoke test loads all hyperparams from TOML profile instead of hardcoding.
TOML: hidden_dim=64, batch=64, lr=0.0003 (stable on RTX 3050 + H100).

Restored check_err() drain in device.rs — required to clear stale CUDA
errors from primary context reuse between tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:07:49 +01:00
jgrusewski
43998a330a feat: two-phase hyperopt + backtest evaluator VRAM leak fix
Two-phase hyperopt splits 31D PSO search into sequential phases:
- Phase 1 (--phase fast, default): fix architecture to small network
  (hidden_dim=128, num_atoms=11), search learning dynamics (~15D).
- Phase 2 (--phase full): fix dynamics from Phase 1 JSON, search
  architecture (~5D). Halves dimensionality per phase → better convergence.
- Phase 1 output includes best_continuous_vector for Phase 2 consumption.

GpuBacktestEvaluator Drop impl: sync forked stream, destroy CUDA graph
and cuBLAS handles before CudaSlice buffers drop. Fixes 261MB/trial
VRAM leak on H100 hyperopt.

ml-core clippy fixes: hex literal, remove dead check_err drain,
unnecessary safety comment, unused OnceLock import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:32:37 +01:00
jgrusewski
e97c30b50a perf: cap hyperopt hidden_dim=256, num_atoms=51 — 4x faster trials
Production defaults are sufficient for hyperopt exploration. Larger networks
can be tested in a separate phase with the best hyperparams found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:00:26 +01:00
jgrusewski
3c8e177932 feat: HyperoptProfile with TOML search space bounds
Add SearchSpaceSection, PsoSection, HyperoptProfile structs to
training_profile.rs. All 31 PSO search bounds now configurable in
config/training/dqn-hyperopt.toml — no code changes needed to
adjust search ranges.

HyperoptProfile::bound("field", default) returns the TOML value
or falls back to the hardcoded default. Adapter wiring is next step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:33:55 +01:00
jgrusewski
6e40e86c09 perf: constrain hyperopt search space + increase H100 experience episodes
Hyperopt:
- num_atoms: 51-201 → 11-51 (GPU profile caps to hardware limit)
- hidden_dim_base: 256-1024 → 128-512 (1024+ is wasteful for 2-layer net)
- Prevents wildly oversized networks (num_atoms=200 caused 25s/epoch)

H100 experience:
- gpu_n_episodes: 256 → 2048 (8x larger cuBLAS batch saturates 132 SMs)
- gpu_timesteps_per_episode: 500 → 100 (fewer steps, more parallel episodes)
- Total experiences: 204K/epoch (was 128K) with better GPU utilization
- Expected: experience 357ms → ~100ms (SM utilization 5% → 40%+)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 01:40:16 +01:00
jgrusewski
a75a98bd0d feat: TOML training profile system — config-driven hyperparameters
Training Profile Loader:
- 3-tier resolution: $FOXHUNT_TRAINING_PROFILE > filesystem > embedded defaults
- DqnTrainingProfile with 10 sections, all Option<T> for sparse profiles
- apply_to() applies only Some fields, preserving struct defaults
- 11 unit tests, all passing

TOML Profiles (config/training/):
- dqn-production.toml: full Rainbow DQN (40+ params)
- dqn-smoketest.toml: CI fast path (sparse, 8 overrides)
- dqn-hyperopt.toml: PSO search space ranges + fixed flags
- ppo-production.toml, ppo-smoketest.toml
- supervised-production.toml, supervised-smoketest.toml
- walk-forward.toml: window sizes

CLI Integration:
- train_baseline_rl: --training-profile (default: dqn-production)
- train_baseline_supervised: --training-profile (default: supervised-production)
- Merge priority: CLI args > TOML profile > GPU profile > struct defaults

Smoke Tests:
- smoke_params() now loads dqn-smoketest.toml instead of hardcoding
- Production features set as manual overrides (testing flags, not config)

Infrastructure:
- K8s job-template.yaml: TRAINING_PROFILE env var + --training-profile arg
- Delete old config/ml/training.toml (replaced, zero callers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:35:41 +01:00
jgrusewski
e4b7d2ffb0 feat: GPU TOML profile system — remove ALL hardcoded VRAM if/else chains
Created config/gpu/{default,rtx3050,h100,a100}.toml with all GPU-specific
parameters: batch_size, num_atoms, buffer_size, hidden_dim_base,
replay_buffer_vram_fraction, gpu_n_episodes, gpu_timesteps_per_episode,
cuda_stack_bytes.

GpuProfile::load() auto-detects GPU by device name, falls back to
embedded defaults (include_str!). Override via FOXHUNT_GPU_PROFILE env.

Removed dead code:
- detect_vram_mb(), vram_scaled_hidden_dims(), vram_scaled_base_dim(),
  resolve_hidden_dim_base() + 18 tests for these functions

All callers updated: train_baseline_rl, DQNTrainer constructor,
PPO trainer, smoke tests, pipeline tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:39:40 +01:00
jgrusewski
8a68bdf21d feat: GPU TOML profile system — replace hardcoded VRAM if/else chains
Replace scattered VRAM-based if/else chains with a declarative TOML profile
system. GPU profiles (rtx3050, a100, h100, default) are selected by device
name and embedded at compile time via include_str! for zero-filesystem
fallback in CI/containers, with filesystem and env var overrides.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:21:55 +01:00
jgrusewski
d95e205d4b refactor(ml): delete mixed_precision module — BF16 unconditional on CUDA
Eliminate the entire mixed_precision runtime indirection layer:
- Delete crates/ml-core/src/mixed_precision.rs (training_dtype, ensure_training_dtype, align_dim_for_tensor_cores)
- Inline ~100 call sites across 130 files to constants:
  training_dtype(&device) → candle_core::DType::BF16
  ensure_training_dtype(x) → x.to_dtype(candle_core::DType::BF16)
  align_dim_for_tensor_cores(x, &device) → (x + 7) & !7
- Remove re-exports from ml-dqn, ml-supervised, ml lib.rs
- Clean config/toml/json/shell references

No CPU/Metal training path exists — BF16 is the only dtype.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 16:11:48 +01:00
jgrusewski
8a87f302c0 feat(ml): re-enable OFI features from MBP-10 order book data
Wire 8 OFI features (OFI L1/L5, depth imbalance, VPIN, Kyle's lambda,
bid/ask slopes, trade imbalance) through the DQN training pipeline:

- Add mbp10_data_dir config field to DQNHyperparameters
- Dynamic state_dim: 43 (no OFI) or 51 (with OFI) based on config
- Compute OFI per bar during data loading, store on trainer
- Pass OFI features through regime_features slot in TradingState
- Configurable MBP-10 path with recursive .dbn/.dbn.zst discovery
- Add zstd auto-detection to DbnParser::parse_mbp10_file()
- Add --mbp10-data-dir CLI flag to train_baseline_rl
- Fix hardcoded [f64; 51] → FeatureVector51 ([f64; 40]) across
  examples, walk_forward, GPU memory profile, and test fixtures
- Fix stale state_dim=51 in dqn_config_2025() and DQN tests

2747 tests pass, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:00:07 +01:00
jgrusewski
5829d6378e feat(data): schema-configurable Databento download with MinIO upload
- download_baseline now reads schema from universe TOML config
  (was hardcoded to ohlcv-1m, now supports mbp-10 and other schemas)
- Add --rclone-dest flag for direct upload to MinIO after each download
- Add config/universe-es-mbp10.toml: ES.FUT MBP-10, same date range
- Add infra/k8s/training/download-mbp10-job.yaml: K8s Job to download
  ES.FUT MBP-10 data in-cluster and upload to MinIO

Usage:
  kubectl apply -f infra/k8s/training/download-mbp10-job.yaml

Prereqs: databento-credentials secret, download_baseline binary in MinIO

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:19:46 +01:00
jgrusewski
57e22c01a8 refactor: update K8s, CI, Docker, Prometheus, scripts, and FXT CLI for api rename
- K8s: rename api-gateway → api manifests, delete web-gateway, update network policies
- CI: rename compile/deploy jobs, delete web-gateway jobs
- Docker: rename service in compose files
- Prometheus: update scrape targets and alert rules
- Scripts: update binary references in build/test/cert scripts
- FXT CLI: rename api_gateway_url → api_url (with serde alias for compat)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:46:36 +01:00
jgrusewski
d501d53c9d chore(infra): remove dead Cockpit TF, CI-CD dashboards, stale Grafana configs
- Delete infra/modules/cockpit/ and infra/live/production/cockpit/
  (Scaleway Cockpit replaced by self-hosted Grafana+Prometheus+Loki+Tempo)
- Delete CI-CD dashboards (foxhunt-ci-pipelines, gitlab-services) and
  grafana-dashboards-cicd ConfigMap from K8s
- Delete config/grafana/ — unreferenced old dashboards (15 files)
- Delete config/monitoring/grafana/ — unreferenced DQN staging dashboard
- Delete crates/ml/grafana/ — unreferenced ML performance dashboard
- Delete services/broker_gateway_service/grafana/ — unreferenced
- Delete .claude/agents/devops/ci-cd/ — GitHub Actions agent (we use GitLab CI)
- Fix grafana-values.yaml: dashboard folder GitLab → Foxhunt,
  hardcoded adminPassword → K8s secret (grafana-admin),
  gitlab-overview → node-exporter (correct name for gnetId 1860)
- Remove CI-CD group from import.sh ConfigMap groups and API fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:13:40 +01:00
jgrusewski
afd85b2f8f chore: clean up examples, update ML binaries and risk tests
- Delete 14 unused example files (-3,543 lines): config, adaptive-strategy,
  data, storage, trading_engine, api_gateway, backtesting, trading_service, chaos
- Update ML training/eval binaries: improved CLI args, completion tracking,
  CUDA test cleanup, hyperopt enhancements
- Fix KAN network and TFT module adjustments
- Update risk test assertions for consistency
- Fix backtesting repositories and promotion manager
- Update .serena project config and Cargo dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 01:33:18 +01:00
jgrusewski
c5db5aa39e perf(ci): compile once with PVC sccache, package with Kaniko
Split the build pipeline: one compile-services job builds all 8 service
binaries with PVC-backed sccache, saves as artifacts. Then 9 Kaniko jobs
just package pre-built binaries into slim runtime images (~30s each).

Before: 9 parallel Kaniko jobs each doing full cargo build --release
  (~20min each, no sccache, 9x duplicated dep compilation)
After:  1 compile job with sccache (~5min cached) + 9 package jobs (~30s)

- Add compile stage between test and build
- Add Dockerfile.runtime (minimal debian + pre-built binary)
- Add Dockerfile.web-gateway-runtime (Node dashboard + pre-built binary)
- Keep Dockerfile.training via Kaniko (needs CUDA dev image for H100)
- Remove all SCCACHE_BUCKET build-args from service builds
- Use dir:// context for Kaniko (only sends build-out/ dir, not full repo)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 00:50:25 +01:00
jgrusewski
ac688cac24 fix(ci): skip Rust builds on infra-only changes, remove stale files
Add `rules:` with `changes:` filters to check, test, and .kaniko-base
jobs so the Rust build pipeline only triggers when source code changes
(crates/, bin/, services/, Cargo.*). Prevents H100 spin-up on
infra-only pushes.

Remove 31 stale/orphan files (-11,885 lines):
- monitoring/ directory (duplicated by config/grafana + config/prometheus)
- 10 broken/placeholder migration files (.broken, .skip)
- 10 unreferenced config files (haproxy, nginx-lb, mutants, etc.)

Add 3 historical design docs from previous restructure work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:45:44 +01:00