Commit Graph

78 Commits

Author SHA1 Message Date
jgrusewski
aa19b42255 refactor(ml): move UnifiedTrainable to ml-core + delete 6K dead deployment code
- Move UnifiedTrainable trait, TrainingMetrics, CheckpointMetadata, and
  checkpoint helpers from ml to ml-core (zero new dependencies — ml-core
  already had candle-core + serde_json)
- Wrap Mamba2SSM in Mamba2TrainableAdapter to satisfy orphan rule (trait
  in ml-core, type in ml-supervised — all other 9 models already used
  wrapper pattern)
- Make Mamba2SSM::validate() pub for cross-crate adapter access
- Delete 5 permanently disabled deployment modules (cfg(any()) — never
  compiled): registry, hot_swap, validation, monitoring, endpoints
  (-5,842 lines)
- Delete 2 undeclared dead files in training/: dqn_trainer.rs,
  transformer_trainer.rs (-138 lines)
- Fix pre-existing compute_loss test shape mismatch in mamba adapter

UnifiedTrainable in ml-core unblocks future trainers/ extraction (17.8K
lines) since model-specific trainers can now depend on ml-core for the
trait without pulling in the full ml monolith.

14 files changed, +128 -6,497 (net -6,369 lines)
Tests: 274 ml-core + 948 ml = 1,222 passed, 0 failed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00
jgrusewski
7382ffd1e2 feat(infra): QuestDB metrics sink + monitoring network policies
Add QuestDB ILP sink for training metrics, update Prometheus scrape
configs, and fix network policies for monitoring stack connectivity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 02:42:53 +01:00
jgrusewski
3f4c39e035 feat(ml): wire OFI data dirs into hyperopt DQN adapter
Add --mbp10-data-dir and --trades-data-dir CLI args to
hyperopt_baseline_rl binary so hyperopt trials can use real
order book and trade data for VPIN/Kyle's Lambda features.

- DQNTrainer: add mbp10_data_dir/trades_data_dir fields + with_ofi_data_dirs() builder
- DQNHyperparameters: pipe through from trainer instead of hardcoded None
- download-trades-job: fix nodeSelector to ci-compile-cpu (platform pool full)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:36:42 +01:00
jgrusewski
c6c550a2be feat(ml): real trade data pipeline for VPIN/Kyle's Lambda + offline RL
Wire Databento Schema::Trades into OFI feature extraction so VPIN and
Kyle's Lambda use real buy/sell classification instead of tick-rule proxy.

Trade data pipeline:
- trades_loader.rs: DbnTrade struct, load_trades_sync(), binary-search
  get_trades_for_bar() for O(log n) time-aligned trade windowing
- ofi_calculator.rs: feed_trade() accumulates real buy/sell pressure
  into VPIN, Kyle's Lambda, and trade imbalance calculators
- data_loading.rs: loads trades from --trades-data-dir, feeds per-bar
  trades to OFI calculator before calculate()
- download-trades-job.yaml: K8s job for ES.FUT trades from Databento
- job-template.yaml: sync trades data from MinIO + --trades-data-dir arg

Offline RL (CQL/IQL):
- experience_dataset.rs: bincode save/load for pre-collected datasets
- iql.rs: Implicit Q-Learning (Kostrikov 2021) — expectile value network,
  advantage-weighted action extraction
- CLI: --offline, --dataset-path, --collect-dataset flags

Cleanup:
- Remove FeatureVector51/MarketFeatureVector type aliases → FeatureVector
- Fix stale dimension comments across 18 files (54→43/51)
- Fix feature_dim default (54→43)

2758 tests pass, 0 compile errors, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:14:46 +01:00
jgrusewski
1ad79493dc feat(infra): training data cache architecture — MinIO → PVC sync
- Expand training-data-pvc from 10Gi → 200Gi (OHLCV + MBP-10 + headroom)
- Add data-sync-job: rclone sync from MinIO → PVC (delta-only, fast repeats)
- Add sync-training-data init container to training job template
  (auto-syncs data from MinIO before each training run)
- Add training-job + data-sync-job NetworkPolicies (MinIO, Tempo, Pushgateway)
- Add app.kubernetes.io/part-of: foxhunt to training pod template
- download_baseline: add --parallel N flag for concurrent Databento downloads
- download_baseline: delete local files after MinIO upload (saves scratch space)
- Bump download job scratch volume to 100Gi

Architecture: Databento → MinIO (source of truth) → PVC (local cache).
First sync pulls everything; subsequent runs only sync deltas (seconds).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 14:45:30 +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
a35a564f45 fix(ml): DQN hyperopt overhaul — DSR reward, dead features, C51/noisy/network fixes
22-task overhaul (6 phases) for DQN training quality:
- Differential Sharpe Ratio (DSR) reward replacing raw PnL
- Remove 11 dead features (3 regime + 8 OFI): state_dim 54→43, feature_dim 51→40
- C51 v_min/v_max aligned to DSR Q-value range (±25.0)
- IQN batch path fix — consistent network for train+inference
- RMSNorm in distributional-dueling network
- Noisy layer sigma_init wired through config
- Rainbow config unified with DSR/C51/noisy defaults
- All dimension constants, comments, CUDA buffers updated
- 2747 lib tests passing, 0 failures, 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:08:39 +01:00
jgrusewski
09710e590f fix(ml): audit — purge all FactoredAction::from_index from DQN paths
Critical bug: all 3 DQN action selection methods (select_action,
select_action_with_confidence, select_action_inference) used
FactoredAction::from_index() which maps indices 0-4 to exposure_idx=0
(Short100) via division by 9. This is the root cause of action
diversity collapse during both training and production inference.

Fix: ExposureLevel::from_index() + OrderRouter::route_default() in all
DQN paths. Also fixes hyperopt objective thresholds (<10 → <3 for
5-action degenerate detection), stale defaults/comments, integration
test configs.

Files: dqn.rs (3 methods), trainer.rs (validation + select_action),
hyperopt/adapters/dqn.rs (thresholds), dqn_model.rs (comments),
train_baseline_rl.rs (default), reward.rs (comment),
dqn_integration.rs + ensemble_integration.rs (num_actions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:20:58 +01:00
jgrusewski
5ace5dd24f fix(ml): DQN hyperopt overhaul — C2 triple exploration + eval 5-action compat
Critical fixes:
- hyperopt backtest: ExposureLevel::from_index() replaces FactoredAction::from_index()
  which mapped all DQN indices 0-4 to Short100 (every trial ran all-short)
- evaluate_baseline: same fix + DQN num_actions default 5, PPO hardcoded 45
- simulate_chunk_trades: is_dqn dispatch for DQN vs PPO action decoding

C2 triple exploration stacking:
- Removed count bonus UCB from Q-value computation in both batch paths
  (noisy nets are sole exploration mechanism)
- Narrowed noisy_epsilon_floor from [0.02, 0.10] to [0.0, 0.05], default 0.0
- Removed count_bonus_coefficient from search space (30D → 29D)
- Count bonus module kept for diversity metrics tracking only

Smoke tests: 6 new tests verifying 5-action space, 29D search space,
epsilon floor defaults, count bonus coefficient fixed at 0.1

2735 tests passed, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:29:02 +01:00
jgrusewski
86b950df10 fix(ml): DQN hyperopt overhaul — 7 root cause fixes (B1-B3, C1-C4)
B1: Eval mode — disable noisy layer noise, use softmax action selection
    (was greedy argmax, causing train/eval policy mismatch)
B3: Per-bar portfolio state sync in eval (was frozen within 1024-bar chunks)
C1: Extrinsic-only replay buffer — curiosity reward no longer stored
    (was corrupting Q-values to learn novelty instead of trading P&L)
C2: Single exploration mechanism — noisy nets only. Removed count bonus
    from Q-values and epsilon floor (was triple-stacking exploration)
C3: Neutral hold reward (0.0) — removed hold_penalty_weight from 31D→30D
    search space (was biasing Q-values toward excessive trading)
C4: Re-enabled early stopping with adaptive plateau_window = epochs/2

2720 tests pass, 0 failures, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:23:51 +01:00
jgrusewski
5a9fa1534b feat(ml): multi-window backtest objective + top-K ensemble training
Multi-window backtest: splits validation data into 3 non-overlapping
windows and aggregates with mean(Sharpe) - 0.5*std(Sharpe), penalizing
inconsistency and reducing overfit to a single data segment.

Top-K ensemble: hyperopt now emits top_k_params (top 5 trials) in JSON
output. train_baseline_rl gains --ensemble-top-k flag to train multiple
models per fold from different hyperopt configs, saving checkpoints as
dqn_ensemble_{k}_fold_{n}.safetensors.

Workflow template: adds ensemble-top-k parameter (default 5) and passes
--ensemble-top-k to the train-best step.

2720 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:42:03 +01:00
jgrusewski
06a875e6fc fix(metrics): push training metrics to pushgateway before pod exit
Ephemeral Argo workflow pods terminate after training completes, causing
Prometheus to lose all scraped metrics. Add push_to_gateway() to POST
final metrics to the existing pushgateway service so they persist on the
Grafana training dashboard after pod completion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:32:09 +01:00
jgrusewski
f2938b19e8 fix(ml): improve TPE exploitation with Scott bandwidth, best-trial injection
- Replace Silverman's bandwidth (h = 1.06σn^(-1/5)) with Scott's rule
  (h = 0.7σn^(-1/(d+4))) for tighter kernels in high-D parameter spaces
- Add best-trial injection: always evaluate EI at best known point plus
  5 small perturbations (±5%), preventing optimizer from forgetting peaks
- Scale n_candidates dynamically: max(256, 8*n_dims) instead of fixed 100
- Reduce gamma from 0.25 to 0.15 when trials < 50 for tighter exploitation
- Wire model_name through PSO/TPE paths for per-trial Prometheus metrics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:54:30 +01:00
jgrusewski
7d9808ecf0 fix(ml): smooth CVaR penalty, fix clip leakage, align noisy sigma, fix eval_supervised
Fixes from deep investigation audit (LOW/MEDIUM priority):

1. CVaR penalty: hard cliff (0 or 10) → smooth ramp with gradient signal
   for PSO. Formula: min(10, max(0, -cvar-0.05)*200).

2. Clip outliers leakage: data_loading.rs now computes clip bounds from
   training portion only (first 80%), then applies to full series.
   Log returns and windowed normalize are causal (no leakage).

3. Noisy sigma scheduler: hyperopt now matches conservative() defaults
   (enabled, initial=0.8, final=0.4) so hyperopt-found params
   generalize to train_best without scheduler mismatch.

4. evaluate_supervised.rs: NormStats fallback from test data (leakage)
   replaced with bail! matching evaluate_baseline.rs behavior.

5. Doc comments: stale 27D references updated to 31D (4 locations).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:14:31 +01:00
jgrusewski
f39219be9e feat(ml): expand DQN hyperopt search space 27D→31D + GPU-batched eval inference
Root cause: catastrophic OOS eval (Sharpe -280) traced to 11 issues
including hardcoded training dynamics and per-bar CPU inference.

Search space (dqn.rs):
- Add warmup_ratio [0.0, 0.15] — was hardcoded to 0
- Add lr_decay_type [Constant/Linear/Cosine] — was hardcoded Constant
- Add min_epochs_before_stopping [2, 6] — was 1000 (disabled)
- Add minimum_profit_factor [1.1, 2.0] — was hardcoded 1.5
- Widen entropy_coefficient [0.01, 0.2] for 45-action factored space

GPU-batched eval (evaluate_baseline.rs):
- 1024-bar chunked inference for both DQN and PPO
- DQN: batch_greedy_actions per chunk (was per-bar select_action)
- PPO: action_probabilities + GPU argmax per chunk
- ~1000x fewer GPU kernel launches
- Add trade_sharpe_ratio for hyperopt-comparable metric

Preprocessing (preprocessing.rs):
- Add compute_clip_bounds/clip_outliers_with_bounds for leakage-free
  clipping across train/val/test splits

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:45:13 +01:00
jgrusewski
a31512643b feat(ml): multi-GPU trial parallelism for hyperopt
Add device_pool to DQN/PPO hyperopt trainers for round-robin GPU assignment
per trial. Binary detects all CUDA devices, scales VRAM budget by GPU count.
Single-GPU: no behavior change (pool of 1).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:37:58 +01:00
jgrusewski
39848a926b fix(eval): read use_noisy_nets from hyperopt params in evaluate_baseline
NoisyLinear layers have different parameter names (sigma_weight/sigma_bias)
than standard Linear layers. Hardcoding use_noisy_nets=false caused
checkpoint loading to silently fail when the training run used noisy nets,
resulting in empty folds in the eval report.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:07:51 +01:00
jgrusewski
1ebbfea8f6 fix(eval): match all architecture params from hyperopt in evaluate_baseline
num_atoms, dueling_hidden_dim, v_min/v_max, gamma were using defaults
instead of hyperopt values — causing tensor shape mismatch on checkpoint
load (e.g. output layer 45×200=9000 vs 45×51=2295). Also fixed use_iqn
to read from use_qr_dqn key (matching trainer's field mapping).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:19:49 +01:00
jgrusewski
d3c834eeb7 fix: pass reward-shaping params from hyperopt to walk-forward DQN
Hyperopt found hold_penalty_weight=1.818, curiosity_weight=0.402, etc.
but walk-forward training used tiny defaults (0.01, 0.1), causing the
agent to learn "holding is optimal" → 0 trades across all 50 epochs.

Now passes: hold_penalty_weight, max_position_absolute, huber_delta,
entropy_coefficient, curiosity_weight, weight_decay, kelly_fractional,
kelly_max_fraction from hyperopt JSON to DQNHyperparameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 06:24:26 +01:00
jgrusewski
1d25d96f7e fix: walk-forward training passes hyperopt params, evaluator falls back to epoch checkpoints
Walk-forward DQN training hardcoded epsilon_start=1.0 with noisy_nets=true,
forcing pure random exploration for all 50 epochs (0 trades, 0 Sharpe).
Now reads epsilon, PER, dueling, distributional, noisy nets, and QR-DQN
params from the hyperopt JSON. When noisy_nets=true, defaults epsilon to
0.05 instead of 1.0.

Evaluator now falls back to highest dqn_fold{N}_epoch{E}.safetensors when
_best.safetensors doesn't exist (early stopping saves epoch checkpoints).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 04:03:21 +01:00
jgrusewski
042c38fed2 fix(dqn,ppo): verification swarm round 2 — gamma^n, checkpoint config, circuit breaker
- IQN + C51 Bellman bootstrap: gamma → gamma^n for n-step returns
- DQNConfig::from_safetensors_file(): reconstruct config from checkpoint
  metadata instead of hardcoding dims/Rainbow flags in enhanced_ml.rs
- PPO update_value_only(): add circuit breaker guard (consistency with update())
- PPO eval: tensor-core alignment on hidden dims (must match PpoTrainer)
- enhanced_ml.rs: checkpoint-driven config loading with fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:40:08 +01:00
jgrusewski
cdb32cd8bf fix(dqn,ppo): comprehensive verification audit fixes — 14 files, 7 agents
DQN correctness:
- N-step Bellman target uses gamma^n (was gamma^1), fixing ~2% Q-value bias
- Cash reserve enforcement actually reduces position (was warn-only)
- CVaR sort_last_dim(true) for IQN random tau ordering
- Marsaglia-Tsang gamma sampling uses normal(0,1) not uniform(0,1)
- DQNConfig::default state_dim 51→54 (51 market + 3 portfolio)

PPO correctness:
- set_learning_rate preserves trained weights (was recreating entire model)
- update_value_only() for critic pretraining (was training both networks)
- PolicyNetwork::entropy() single forward pass (was 2x GPU compute)
- LSTM entropy uses proper H=-sum(p*log(p)) (was -mean(log_probs))
- grad_norm metric set to None (was reporting policy loss as gradient norm)

Config parity (train/eval/hyperopt/enhanced_ml):
- PPO hyperopt state_dim 51→54, value_hidden_dims 3→5 layer
- enhanced_ml feature_count 16→54, PPO policy_hidden_dims [128,64,32]→[128,64]
- Eval: tensor core alignment, Rainbow from hyperopt params, warmup alignment
- GPU batch model_state_dim 51→54 (matches kernel output)

Infrastructure:
- QNetwork dropout training mode (AtomicBool toggle, was always disabled)
- reward_history Vec→VecDeque (O(1) front removal, was O(n))
- Plateau detection distinguishes worsening from plateau in log messages

2732 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:16:08 +01:00
jgrusewski
fc0754c63f fix(dqn): production hardening — LR scheduler, urgency costs, F32 traps, atomic IO
Wire LR scheduler to actually update the Adam optimizer (was logged but
never applied). Add update_learning_rate to DQN, RegimeConditionalDQN,
and DQNAgentType so decay_factor propagates through all agent variants.

Fix train/eval parity: CLI defaults 51→54 features, 3→45 actions;
DQN eval always uses 3-layer hidden_dims; PPO eval uses 5-layer value
network matching trainer. enhanced_ml.rs hardcoded config updated from
state_dim=16/num_actions=3 to 54/45.

Fix Candle F32/BF16 traps: replace `Tensor * 0.5` (f64 literal) with
broadcast_mul(Tensor::full(0.5_f32)) in quantile_regression.rs (2x),
dqn.rs Huber loss, and IQN gamma multiplication. Prevents panics on
Ampere+ BF16 GPUs.

Add urgency_weight() multiplier to training cost model in reward.rs
and portfolio_tracker.rs — urgency dimension (Patient/Normal/Aggressive)
now affects learned value function, matching evaluate_baseline.rs.

Fix equity tracking: additive (equity += ret) → multiplicative
(equity *= 1.0 + ret) in compute_metrics. Fix total return calc.

Fix PER beta annealing: epochs*70 → epochs*1000 (~1015 actual steps
per epoch for 130k bars / batch 128).

Fix silent target network freeze: mutex lock failure now propagates
error instead of silently skipping weight update.

Make save_checkpoint atomic (write .tmp then rename). Make NormStats
write atomic with error logging instead of silent discard.

Convert EnsembleConfig::new assert! → Result<Self, MLError> with
14 call site updates. Fix hyperopt result serialization to warn
instead of silently dropping to Value::Null.

Fix clippy MSRV mismatch: clippy.toml 1.75 → 1.85 matching Cargo.toml.

2732 tests pass, 0 clippy warnings across workspace.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:04:53 +01:00
jgrusewski
d289b2f264 fix(dqn): close checkpoint validation gaps, atomic NormStats, clippy cleanup
- DQNAgent::load_from_safetensors: validate architecture hash before
  loading weights (was bypassing validation entirely - P0 production gap)
- DQNEnsemble::save_to_directory: embed architecture metadata via
  safetensors::serialize_to_file instead of bare VarMap::save (save/load
  round-trip was guaranteed to fail)
- architecture_hash: hash hidden_dims.len() before values to prevent
  theoretical collision between different-length configs
- train_baseline_rl: NormStats write now atomic (write .tmp then rename)
  with cleanup guard on rename failure; serialization error is now loud
  (error! + return None) instead of silently discarded
- train_baseline_rl: checkpoint rename failure cleans up orphaned .tmp
- hyperopt_baseline_rl: fix clippy single_match_else (match on equality
  check -> if/else)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:31:20 +01:00
jgrusewski
2f60e0cda0 fix(eval): train/eval parity - position tracking, cost differentiation, feature alignment
- Wire position-delta cost model: costs only apply on position changes,
  proportional to delta (not flat per-bar)
- Differentiate order types: Market=15bps, LimitMaker=5bps, IoC=10bps
- Apply urgency weight: Patient=0.5x, Normal=1.0x, Aggressive=1.5x
- Append 3 portfolio features (position, unrealized PnL, drawdown) to match
  training's 54-dim state vector (was 51, causing shape mismatch on load)
- Fix default num_actions 3->45 to match training's factored action space
- Add --bars-per-year CLI flag for per-symbol annualization
  (ES/NQ=347760, 6E=345000, ZN=105840)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:43:01 +01:00
jgrusewski
3ef80b9efe fix(dqn): atomic checkpoints, CUDA sync, NormStats hard error, q_value_std warning
- Atomic checkpoint writes: write to .tmp then fs::rename() (POSIX atomic)
- Remove redundant thread::sleep(50ms) after CUDA tensor readback sync
- NormStats: bail on missing file instead of computing from test data (lookahead bias)
- q_value_std: warn when missing from training metrics instead of silent 0.0 fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:34:19 +01:00
jgrusewski
6d0182d77e feat(hyperopt): make TPE the default optimizer
Change --optimizer default from "pso" to "tpe" since TPE has better
sample efficiency in 25D spaces. Fix clippy let_underscore_must_use
on CUDA sync tensor readback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:50:02 +01:00
jgrusewski
903e690e9a feat(hyperopt): wire TPE optimizer into pipeline with --optimizer flag
Add optimize_with_tpe() function that uses Tree-Parzen Estimator for
Bayesian hyperparameter optimization. Add --optimizer=tpe CLI flag
to hyperopt_baseline_rl binary (default: pso for backward compat).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:40:03 +01:00
jgrusewski
a7b7796147 fix(ml): correct PSO auto-scaling with empirical VRAM estimates
Split model overhead into two constants: MODEL_OVERHEAD_MB (pure
model weights for batch-size capping) and TRIAL_VRAM_MB (total
per-trial VRAM for concurrent hyperopt planning). DQN trials
empirically consume ~7 GB each on L40S (model + GPU replay buffer +
experience collector + CUDA allocations + fragmentation), not the
200 MB previously estimated. This caused plan_hyperopt to compute
128 concurrent trials instead of the actual 5, inflating PSO
particles from 20→128 and total trials from 20→384 via .max()
instead of .min(), guaranteeing a 4h timeout kill.

Fix auto-scaling to: (1) match particles to GPU concurrency for
maximum hardware utilization on any node, (2) cap particles at
max_trials to never inflate the trial budget, (3) never auto-inflate
the total trial count.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 11:34:05 +01:00
jgrusewski
2642286c86 feat(hyperopt): record elapsed_seconds metric in all hyperopt binaries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:10:02 +01:00
jgrusewski
ddacfbbff1 feat(supervised): record learning rate and epoch duration metrics
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:10:00 +01:00
jgrusewski
c4693403d5 feat(ml): wire hyperopt Prometheus metrics into training binaries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:25:56 +01:00
jgrusewski
4b2f9e8133 fix(ml): GPU-aware PSO thread cap — 3→7 parallel trials on L40S
The auto-detect heuristic used cpus/2 ("smart cap"), designed for
CPU-bound workloads.  DQN/PPO trials are GPU-bound — each rayon
thread submits CUDA kernels and waits on cudaDeviceSynchronize(),
using minimal CPU.  cpus-1 is the correct cap.

On L40S-1-48G (8 vCPU): 3 threads → 7 threads (2.3× more trials).
Also bumps CI CPU limit 7500m→8000m to expose all 8 cores.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:55:54 +01:00
jgrusewski
64ca8f97ce fix(observability): dedicated OTLP runtime for sync training binaries
The batch span processor needs a tokio runtime for gRPC transport and
periodic flush. Async services already have one via #[tokio::main], but
sync training binaries (hyperopt, train, evaluate) don't.

Previous approach (making binaries async with #[tokio::main]) caused
"Cannot start a runtime from within a runtime" panics because the ML
crate's internal code creates its own tokio runtimes for block_on().

New approach: build_otel_tracer() detects runtime context via
Handle::try_current(). If absent, it creates a dedicated 1-worker
multi-thread runtime stored in a process-lifetime OnceLock. The worker
thread actively polls the OTLP batch export task.

Reverts training binaries to sync fn main() so internal runtime creation
(hyperopt adapters, DQN/PPO trainers) continues working as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 18:54:10 +01:00
jgrusewski
fcf87a5f72 fix(ml): add tokio runtime to training binaries for OTLP export
All 6 training binaries (hyperopt_baseline_rl, hyperopt_baseline_supervised,
train_baseline_rl, train_baseline_supervised, evaluate_baseline,
evaluate_supervised) used sync fn main() but the OTLP batch exporter
requires a tokio runtime (tonic/hyper-util gRPC transport). This caused
an immediate panic on CI when OTEL_EXPORTER_OTLP_ENDPOINT was set.

Fix: #[tokio::main(flavor = "current_thread")] on all 6 binaries.
Also fix pre-existing clippy warnings (shadow, let_underscore_must_use,
doc_markdown, cognitive_complexity, integer_division, unsafe_code).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 18:16:37 +01:00
jgrusewski
118ceca4d5 fix(ml): update evaluate_baseline for 45 factored actions
Both DQN and PPO eval paths used old 3-action index matching (0=Buy,
1=Sell, 2=Hold). Now uses FactoredAction.target_exposure() for
exposure-weighted returns and order-type-specific transaction costs.
PPO path had .to_int() which doesn't exist on FactoredAction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:24 +01:00
jgrusewski
3d679e824f feat(ml): GPU saturation final — DQN PER deferral, PPO trajectory batching, Liquid/TGGN sync reduction, DoubleBuffer wiring
- DQN PER: defer td_errors to_vec1() after loss.to_scalar() — piggyback on
  existing pipeline flush instead of forcing premature GPU→CPU stall
- PPO trajectories: capacity-hint Vec allocations, extend_flat_states methods,
  states_flat field on TrajectoryBatch for zero-copy GPU upload
- TGGN validate(): batch N per-sample losses on GPU → single to_scalar() sync
  (was N GPU→CPU syncs)
- Liquid backward(): batch grad-norm per-param sqr().sum_all() on GPU → single
  to_scalar() sync (was N GPU→CPU syncs per optimizer step)
- Liquid validate(): same N→1 GPU sync reduction as TGGN
- DQN trainer: restore EpochPrefetcher/DoubleBufferedLoader API (wrongly deleted)
- train_baseline_rl: wire DoubleBuffer GPU pre-upload — after CPU prefetch
  completes, immediately upload next fold to GPU via DqnGpuData::upload() so
  next fold starts with data already resident on GPU

2478 tests pass, 0 clippy warnings, 0 compile errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
a454a26a5c feat(ml): GPU saturation backlog — PPO rollout sync, VRAM-aware dims, KAN GPU splines
Three remaining GPU bottlenecks from the saturation backlog:

1. PPO rollout GPU sync stalls (3→1 per step):
   - Merged sample_action to return (action_idx, probs_vec), eliminating
     duplicate to_vec1 sync on action probabilities
   - Batched critic forward after rollout loop — single GPU→CPU sync
     replaces per-step critic.forward() calls (2048 syncs → 1)
   - Safe indexing throughout (clippy deny rules)

2. VRAM-aware default network dimensions:
   - Added detect_vram_mb() with GPU_MEMORY_MB env var override for K8s
   - Added vram_scaled_hidden_dims() with 4 tiers (CPU/<8GB/16GB/40GB+)
   - DQN: [256,256] → [2048,1024,512] on L40S/H100
   - PPO: hidden_dim_base 128 → 1024 on L40S/H100
   - Wired into train_baseline_rl.rs for non-hyperopt training runs

3. KAN B-spline GPU lookup table:
   - Pre-compute basis values on 1024-point grid at layer construction
   - GPU evaluation via gather + linear interpolation (replaces recursive
     Cox-de Boor CPU bounce: 32K recursive calls → 2 GPU gathers)
   - Fallback to CPU path when grid not pre-computed

5 files changed, +687/-43, 2476 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
6c3e518499 feat(ml): wire Prometheus eval metrics into evaluate_supervised
Emit set_eval_metrics (directional_accuracy, sharpe, profit_factor, return)
per fold for supervised model evaluation. Start metrics server on :9094.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 23:03:13 +01:00
jgrusewski
6c829d59a8 feat(ml): wire Prometheus eval metrics into evaluate_baseline
Emit set_eval_metrics (win_rate, sharpe, profit_factor, return) per fold
for DQN/PPO evaluation. Start metrics server on :9094. Feeds training
cockpit Grafana dashboard eval panels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 23:03:13 +01:00
jgrusewski
2cc68af7d6 feat(ml): add OTLP tracing to all 6 training/eval binaries
Replace tracing_subscriber::fmt() with init_observability() which adds
JSON structured logging + optional OTLP export to Tempo. When
OTEL_EXPORTER_OTLP_ENDPOINT env var is unset, falls back to fmt-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 23:03:13 +01:00
jgrusewski
e741b50932 feat(ml): wire per-fold Prometheus metrics into train_baseline_rl
Emit set_epoch, set_epoch_loss, set_validation_loss, set_iteration_seconds
after each DQN/PPO fold completes. Feeds training cockpit Grafana dashboard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 23:03:13 +01:00
jgrusewski
533249eb91 fix(ml): wire spread_cost_bps into RL training — commission + spread = total tx cost
Previously train_baseline_rl.rs only passed commission (tx_cost_bps) to
DQN/PPO trainers, ignoring bid-ask spread slippage. Now computes per-fold
average spread via spread_cost_bps() (same as evaluate_baseline) and passes
total cost (commission + spread) to both trainers.

Removes #[allow(dead_code)] — function is now used by all 4 example binaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 22:10:43 +01:00
jgrusewski
f975c9cf71 fix(ml): suppress dead_code warning on spread_cost_bps (used by other examples)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 22:02:30 +01:00
jgrusewski
a03ae324b1 feat(ml): wire fold prefetching in walk-forward loop — overlap I/O with GPU training
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:56:25 +01:00
jgrusewski
5d390037e5 feat(ml): wire train_baseline_rl PPO to PpoTrainer — enables GPU optimizations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:31:38 +01:00
jgrusewski
95dfd4ad1a feat(ml): wire train_baseline_rl DQN to DQNTrainer — enables all GPU optimizations
Replace raw DQN::new() + manual training loop in the walk-forward
training binary with DQNTrainer, which automatically activates:
- Mixed precision (BF16/F16 auto-detected from GPU)
- Dynamic batch sizing (AutoBatchSizer + HardwareBudget)
- Gradient accumulation
- Full Rainbow DQN (PER, dueling, C51, noisy nets, n-step)
- Regime-conditional Q-networks
- Portfolio tracking, Kelly sizing, entropy regularization

The walk-forward fold structure (data loading, feature extraction,
window generation, normalization) stays in the binary — only per-fold
training delegates to DQNTrainer::train_with_preloaded_data().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:27:10 +01:00
jgrusewski
ed1a9fa59d feat(ml): wire dead GPU optimizations — data preloading + PPO mixed precision forward
Quality audit found 2 dead code paths in the GPU optimization commit:

1. Data caching: preload_data() was defined on all 10 hyperopt adapters
   but never called. Now wired in both hyperopt binaries (RL + supervised)
   before the trial loop. Each model preloads training data once into
   Arc<Vec<...>>, eliminating per-trial disk I/O.

2. PPO mixed precision: config.mixed_precision was stored but never used
   in forward passes. Added forward_mixed() to PolicyNetwork and
   ValueNetwork (same BF16/FP16 pattern as DQN's NetworkLayers). Stored
   on network structs and auto-applied via forward(). Wired in
   PPO::with_device() for MLP networks.

Also fixes missing mixed_precision field in 2 test files and
trading_service PPOConfig literal.

5 files changed, +152/-20. 2418 tests pass, workspace compiles clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 18:23:03 +01:00
jgrusewski
b91fa43b32 Merge branch 'worktree-gpu-max-performance'
# Conflicts:
#	crates/ml/examples/hyperopt_baseline_rl.rs
2026-03-01 18:00:10 +01:00