Commit Graph

1059 Commits

Author SHA1 Message Date
jgrusewski
88f0b3ec23 refactor(ml): extract DQN module into ml-dqn crate (task 6)
Move 53 DQN source files + gpu_replay_buffer from ml into standalone
ml-dqn crate. The ml crate's dqn module is now a thin re-export layer
(`pub use ml_dqn::*`) plus two bridge files (trainable_adapter.rs,
stress_testing.rs) that depend on ml-internal types.

Key changes:
- ml-dqn: 45 modules, 334 tests, compiles standalone
- ml: depends on ml-dqn, re-exports via dqn/mod.rs
- DQN.config: pub(crate) → pub for cross-crate access
- DQNAgent checkpoint methods moved into ml-dqn
- gpu_replay_buffer moved from cuda_pipeline/ to ml-dqn
- 8 dead-code files removed (never declared in mod.rs)

Net: -30,648 lines from ml crate. Workspace: 0 errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:16:08 +01:00
jgrusewski
d2f3b6c799 refactor(ml): move metrics/ + performance.rs to ml-core (5g)
Move Sharpe ratio metrics and SIMD performance module to ml-core.
These only depend on MLError which is already in ml-core.

Add approx = "0.5" to ml-core dev-dependencies for Sharpe tests.
Skip observability/ (needs prometheus dep — stays in ml).

Test counts: 271 ml-core + 2487 ml = 2758 total, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:41 +01:00
jgrusewski
4843b4a2a6 refactor(ml): move shared infrastructure to ml-core + eliminate FactoredActionExt (5e)
Move 6 shared modules (~2.3K lines) from ml to ml-core:
- trading_action.rs (TradingAction enum)
- action_space.rs (action masking, re-exports)
- xavier_init.rs (Xavier/Glorot weight initialization)
- mixed_precision.rs (AMP utilities, FP16/BF16)
- order_router.rs (deterministic order routing)
- portfolio_tracker.rs (portfolio state tracking)

With TradingAction now in ml-core alongside FactoredAction, the
FactoredActionExt extension trait is eliminated entirely —
from_trading_action()/to_trading_action() become inherent methods
on FactoredAction. This removes the need for `use FactoredActionExt`
imports in reward.rs, gae.rs, and trajectories.rs.

Test counts: 250 ml-core + 2508 ml = 2758 total, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:41 +01:00
jgrusewski
f951fddc86 refactor(ml): move memory_optimization/ + batch_size_resolver to ml-core (5d)
- Move memory_optimization/ (4.6K lines, 7 files) from ml to ml-core
- Move batch_size_resolver.rs to ml-core (now both deps in same crate)
- Add tempfile to ml-core dev-dependencies (qat tests)
- Re-export both modules from ml facade
- Keep checkpoint/ in ml (model_implementations.rs has model-specific deps)

185 ml-core tests + 2573 ml tests = 2758 total, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:41 +01:00
jgrusewski
70546ad1c4 refactor(ml): move safety/ to ml-core + rename Real-prefixed types (5c)
- Move safety/ module (6.5K lines) from ml to ml-core (self-contained)
- Keep security/ in ml (depends on ensemble::EnsembleDecision)
- Move From<ProductionTrainingError> for MLSafetyError to ml (cross-crate)
- Rename Real-prefixed inference types to proper names:
  RealInferenceError → InferenceError
  RealInferenceConfig → InferenceConfig
  RealPredictionResult → InferencePrediction
  RealNeuralNetwork → NeuralNetwork
  RealMLInferenceEngine → MLInferenceEngine
- Re-export safety from ml facade for backward compatibility

129 ml-core tests + 2629 ml tests = 2758 total, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:41 +01:00
jgrusewski
9fc9522821 refactor(ml): remove all legacy naming and deprecated wrappers
- Rename FactoredActionLegacy → FactoredActionExt trait
- Rename from_legacy() → from_trading_action(), to_legacy_action() → to_trading_action()
- Delete 5 deprecated free functions from ml-core (create_hft_*, create_ultra_low_latency_*)
- Update ml_tests.rs to call associated methods directly
- Remove #[allow(deprecated)] now unnecessary

8 files, -9 net lines, 2758 tests pass (81 ml-core + 2677 ml)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:41 +01:00
jgrusewski
33cc076eb3 refactor(ml): move compute primitives to ml-core (5b)
Move optimizers, gradient_accumulation, gradient_utils, cuda_compat,
tensor_ops, and gpu (device config, capabilities, memory profiling) to
ml-core. These are shared compute primitives used by all models.

Also commit module files for core types (common, config, error, model,
traits, types) that were moved from ml to ml-core in task 5a but left
staged without being committed.

Notable changes:
- resolve_batch_size() stays in ml (new batch_size_resolver module)
  because it depends on memory_optimization::auto_batch_size which
  has not yet moved to ml-core
- FactoredAction legacy bridge converted from inherent impl to
  extension trait (FactoredActionLegacy) since FactoredAction is now
  defined in ml-core, not ml
- candle-optimisers added to ml-core dependencies (needed by Adam)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:41 +01:00
jgrusewski
7d1b0f232f refactor(ml): move core types to ml-core + dedup MLError (5a)
Move all inline type definitions from ml/src/lib.rs to ml-core:
MLError, MLResult, Trade, MarketRegime, HealthStatus, Features,
MLModel trait, ModelRegistry, ParallelExecutor, LatencyOptimizer,
TrainingMetrics, ValidationMetrics, InferenceResult, ModelMetadata.

Dedup: consolidate ConfigError{reason}/ConfigurationError(msg) into
single ConfigError(String) tuple variant (was 2 variants, 174 refs).

Cleanup: convert create_hft_* free functions to associated methods
(HFTPerformanceProfile::ultra_low_latency(), ParallelExecutor::hft()).

ml facade re-exports via `pub use ml_core::*`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:41 +01:00
jgrusewski
5c18f17a01 refactor(ml): extract shared items from dqn/ to top-level modules
Move mixed_precision, xavier_init, portfolio_tracker, action_space,
order_router from dqn/ to ml/src/ root. Extract TradingAction enum
from dqn/agent.rs to standalone trading_action module. Re-export
from dqn/mod.rs for backward compatibility.

These modules are shared infrastructure used by PPO, trainers,
hyperopt, and ensemble — not DQN-specific.

Test results: 2757 passed, 0 failed, 25 ignored (unchanged).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:07 +01:00
jgrusewski
e440de4c6b feat(ml): scaffold sub-crate directory structure for ml split
Add 5 empty sub-crates (ml-core, ml-dqn, ml-ppo, ml-supervised, ml-infra)
to workspace. Modules will be moved from monolithic ml crate in subsequent
tasks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:13:07 +01:00
jgrusewski
809295dc5f fix(ml): correct GPU experience path — aligned state_dim, reward tracking, Prometheus label
Three bugs in the GPU experience collection hot path:

1. gpu_batch_to_experiences() had hardcoded state_dim=43 but the CUDA kernel
   outputs states at the ALIGNED dimension (56 with OFI, 48 without). After
   sample 0, every replay buffer entry had corrupted state vectors — the
   network was learning from garbage data.

2. GPU path never called monitor.track_reward(), so mean_reward was always
   reported as 0.0 in epoch logs despite the agent generating real rewards.

3. Action tracking was double-counted (direct array write + track_action_by_exposure),
   inflating diversity metrics by 2x. Consolidated into single bounded call.

Also adds missing app.kubernetes.io/component label to job-template.yaml
so Prometheus training-pods scrape job discovers training pods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:11:01 +01:00
jgrusewski
7295d2c79f feat(ml): add sharpe_weight to hyperopt search space (C4) — 27D → 28D
PSO can now tune sharpe_weight in [0.0, 0.5] instead of hardcoded 0.3.
This lets hyperopt discover the optimal Sharpe ratio blending weight
in the composite reward signal per-symbol.

Also updates docstring to reflect current C1-C4 search space state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:29:09 +01:00
jgrusewski
48dfe12171 fix(ml): inject STATE_DIM/MARKET_DIM/NUM_ATOMS_MAX into CUDA kernel via NVRTC
The GPU experience kernel had hardcoded STATE_DIM=54, MARKET_DIM=51,
NUM_ATOMS_MAX=51 but the host-side feature buffer uploads 40 features
per bar and networks use state_dim=56 with up to 100 atoms. Past ~702K
bars the stride mismatch caused out-of-bounds GPU reads → ILLEGAL_ADDRESS.

All dimension constants now use #ifndef guards so the NVRTC JIT compiler
receives the actual values via #define injection — zero runtime overhead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:18:23 +01:00
jgrusewski
f921bbe5d0 fix(ml): inject dynamic network dims into CUDA kernel via NVRTC
Hyperopt varies hidden_dim_base (256–1024) but the CUDA experience
kernel hardcoded SHARED_H1=256, SHARED_H2=256. When hyperopt picked
larger dims, the kernel did matvec with wrong strides → illegal memory
access → training crash on first epoch.

Fix: wrap CUDA #defines in #ifndef guards and inject actual dims from
the dueling network config at NVRTC compile time. The kernel is now
specialized per-trial with the exact weight matrix dimensions — zero
runtime overhead, no dimension mismatch possible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:33:59 +01:00
jgrusewski
f916b17e6b fix(ml): align dueling shared layers with CUDA kernel — eliminate CPU fallback
from_dqn_params() was stripping the last hidden_dim, creating only 1
shared layer (shared_0) when hidden_dims=[256,256]. The CUDA experience
kernel hardcodes 2 shared layers and gpu_weights.rs expects shared_1.*
weight keys. This caused "Missing weight: shared_1.weight" every epoch,
forcing CPU fallback for all experience collection on H100.

Fix: use all hidden_dims as shared layers. Default [256,256] now creates
shared_0 + shared_1, matching the CUDA kernel architecture exactly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:58:52 +01:00
jgrusewski
c2606790e2 fix(ml): use explicit trades_data_dir when set in DQN adapter
The DQN hyperopt adapter has an explicit trades_data_dir field from CLI
args, but load_ofi_features() was only using the derived sibling path.
Now uses the explicit field when available, falling back to derivation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:09:11 +01:00
jgrusewski
ddb4300c29 fix(ml): feed trade data into OFI pipeline — VPIN/Kyle's Lambda were always zero
The OFI calculator's compute_ofi_from_file() never called feed_trade(),
leaving 3/8 features (VPIN, Kyle's Lambda, trade_imbalance) at zero in
production. Added compute_ofi_with_trades() that interleaves trades by
timestamp into the MBP-10 streaming callback. Updated DQN and PPO
hyperopt adapters to derive trades_dir as sibling of dbn_data_dir.

Smoke test validates: without trades VPIN=0/5000, with trades VPIN=5000/5000.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:06:22 +01:00
jgrusewski
f2a028752c test(ml): add 8 GPU kernel parity tests — validate CUDA forward pass
Tests require a CUDA GPU and are #[ignore]d by default. Run with:
  cargo test -p ml --test gpu_kernel_parity_test -- --ignored

Covers:
- Standard dueling forward: finite Q-values, valid action range [0,4]
- C51 distributional forward: Q-values bounded by atom support [-25,25]
- Candle vs kernel Q-value parity: argmax action consistency
- NoisyNet exploration: noise injection produces action diversity
- Weight extraction roundtrip: VarMap → CudaSlice → sync
- Distributional weight shapes: value_out [51,128], advantage_out [255,128]
- RMSNorm gamma extraction: initialized to 1.0
- Repeated kernel launches: 5 consecutive runs all produce finite output

All 8 tests pass on RTX 3050 Ti (4.49s total).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 11:32:30 +01:00
jgrusewski
d37d5a57df feat(ml): port NoisyNets (D5) and C51 distributional (D6) to GPU kernel
Port factorized Gaussian noise exploration and 51-atom categorical value
distribution to the CUDA experience collection kernel, fixing a correctness
bug where GPU Q-values were wrong when use_distributional=true (production
default) due to misinterpreted distributional weight shapes.

- D5: NoisyNet factorized noise (Box-Muller + f(x)=sign(x)*sqrt(|x|)) on
  all 6 dueling layers, online network only — target stays deterministic
- D6: C51 distributional dueling forward with per-action atom softmax,
  RMSNorm after shared/value/advantage layers, correct [51,128]/[255,128]
  weight interpretation
- RmsNormWeightSet extraction and post-epoch sync (GPU-to-GPU)
- Fix get_effective_epsilon() to report actual 2% noisy floor instead of 0.0
- Proportional diversity entropy penalty (continuous gradient vs cliff)
- 2758 tests passing, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 11:09:26 +01:00
jgrusewski
8926c6e1cf perf(ml): eliminate CPU from DQN training hot path — GPU-resident ops only
GPU experience collector was falling back to CPU because it required a
curiosity VarMap, but curiosity_weight=0.0 means the module is never
created. Fix: make curiosity optional (CuriosityWeightSet::zeros() for
GPU buffers, curiosity_scale=0.0 in kernel config). Also require
explicit binary-tag SHA in Argo training workflow (no "latest" fallback).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 09:15:52 +01:00
jgrusewski
6ab9e184f5 fix(ml): align hyperopt backtest state_dim for tensor cores
The walk-forward backtest in the DQN hyperopt adapter constructed batch
tensors with raw state_dim (51/43) but the model expects aligned
state_dim (56/48). This caused shape mismatch errors during backtest
evaluation: matmul [1024, 51] vs [56, 1024].

Fix: use align_dim_for_tensor_cores() and zero-pad each state vector
before tensor construction, matching the same pattern used in
compute_loss_internal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 04:00:56 +01:00
jgrusewski
c1287efbcf fix(ml): pad remaining state tensor paths for tensor core alignment
Additional padding in get_q_values and convert_to_state — these are
currently unused in the training pipeline but would crash if called.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 03:24:49 +01:00
jgrusewski
2a9e185db9 fix(ml): pad state tensors in ALL code paths for tensor core alignment
Validation, action selection, and Q-value monitoring paths were using
raw state dimensions (51) while the model expected aligned dims (56).
Training epoch 1 passed because the GPU pipeline pads correctly, but
validation crashed: shape mismatch [1000,51] vs [56,1024].

Fixed: validation batch, select_actions_batch CPU fallback,
estimate_avg_q_value, compute_q_gap — all now zero-pad to aligned dim.
Also fixed model size estimation log to show aligned dims.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 03:20:43 +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
03c2ad5920 perf(ml): proper tensor core alignment — pad at data pipeline, not forward pass
Move BF16 tensor core alignment from per-forward-pass allocation to
data pipeline boundaries. On H100, state_dim 43→48 and 51→56 (8-aligned)
so cuBLAS dispatches HMMA instructions instead of falling back to scalar FMA.

Architecture:
- Trainer computes aligned state_dim at source (align_dim_for_tensor_cores)
- GPU path: DqnGpuData.pad_state_tensor() pads once at upload boundary
- CPU path: train_batch() fold zero-pads Experience.state vectors
- Networks receive pre-aligned tensors — zero per-step overhead

All state_dim defaults updated to aligned values (43→48, 51→56).
Removed pad_to_aligned() from all network forward() methods.
2758 tests pass, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 02:42:19 +01:00
jgrusewski
542be32bed perf(ml): pad DQN input dims for H100 tensor core alignment
cuBLAS requires M/N/K dimensions to be multiples of 8 for BF16 tensor
core dispatch. state_dim=51 (with OFI) caused silent fallback to scalar
FMA ops, leaving tensor cores at 0.1% utilization despite BF16 enabled.

Pad state_dim to next 8-multiple (51→56, 43→48) at network construction
and zero-pad input tensors in forward pass across all DQN network types:
Sequential, DuelingQNetwork, DistributionalDuelingQNetwork, NetworkLayers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 02:10:38 +01:00
jgrusewski
8f95908e16 perf(ml): eliminate CPU from DQN training hot path — GPU-resident ops only
- Fix NaN detection: replace non-existent isnan() with ne(&self) (NaN≠NaN)
- C51 gradient strip: copy().detach() replaces to_vec2→from_vec GPU→CPU→GPU roundtrip
- GPU batch fast path: skip CPU fold + 5× from_vec when GpuBatch available
- Action validation: GPU clamp() replaces CPU loop in GPU batch path
- PER TD errors: keep as GPU Tensor when GpuPrioritized replay active
- PER indices: use GpuBatch.indices directly instead of CPU Vec→Tensor
- IS weights: cache GPU tensor from GpuBatch, reuse in both loss paths
- Logging: Q-values 10→500 steps, diagnostics 100→1000 steps

2758 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:08:00 +01:00
jgrusewski
88f6d90576 refactor(ml): DQN/PPO adapters use shared load_ofi_features_parallel
Both adapters now call load_ofi_features_parallel from mbp10_loader
instead of duplicating the rayon + streaming logic inline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:34:00 +01:00
jgrusewski
78db6f5389 fix(ml): streaming parallel OFI + fix hardcoded state_dim=54 in backtest
Three fixes:
- Streaming MBP-10 parser (parse_mbp10_streaming): computes OFI inline
  during decode — eliminates Vec<Mbp10Snapshot> allocation (41.9M clones)
- Parallel file processing: rayon par_iter across 9 MBP-10 files
  (778s sequential → ~90s expected on H100 24-core)
- Fix hardcoded state_dim=54 in walk-forward backtest tensor creation
  that caused panic "range end index 55296 out of range for slice of
  length 52224" — now uses dynamic state_dim (43 or 51 with OFI)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:28:18 +01:00
jgrusewski
4976e2e1d7 fix(ml): OFI preload uses adapter's load_ofi_features, not trainer's empty field
Root cause: preload_data() called loader.ofi_features.take() on the internal
DQN trainer, but load_training_data() only loads OHLCV bars — it never
populates ofi_features. The OFI loading is done by the hyperopt adapter's
own load_ofi_features() method.

Fixes:
- preload_data() now calls self.load_ofi_features() directly
- load_ofi_features() uses self.mbp10_data_dir when set (was hardcoded ../mbp10)
- input_dim pre-computation uses OFI-aware size (51 when enabled, 43 otherwise)
- CI 'latest' package: delete-then-upload to avoid GitLab duplicate file issue
- Warn when mbp10_data_dir is set but no OFI features loaded

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:29:22 +01:00
jgrusewski
d7061ca2ac fix(ml): preload OFI features in hyperopt adapter
The hyperopt preload_data() created a loader with default hyperparams
(mbp10_data_dir=None), so MBP-10/trades data was never loaded. Each
trial then set mbp10_data_dir → state_dim=51, but the preloaded data
had no OFI features → shape mismatch [128,43] vs [51,1024].

- Pass mbp10_data_dir/trades_data_dir to preload hyperparams
- Extract ofi_features from loader after preload
- Store as preloaded_ofi_features: Option<Arc<Vec<[f64;8]>>>
- Inject into each trial's DQNTrainer before training

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:43:47 +01:00
jgrusewski
b10cdebfb1 fix(ml): wire OFI features into GPU batch state construction
The GPU path (build_batch_states / build_state_tensor) only concatenated
market[40] + portfolio[3] = 43 dims, while state_dim was set to 51 when
OFI was enabled. This caused shape mismatch [128,43] vs [51,1024].

- Add ofi_features: Option<Tensor> field to DqnGpuData
- Add upload_ofi() method for MBP-10 order book features (8 dims/bar)
- Concatenate OFI in build_batch_states: [count,40]+[count,3]+[count,8]=[count,51]
- Concatenate OFI in build_state_tensor: [1,40]+[1,3]+[1,8]=[1,51]
- Wire trainer to call upload_ofi() after GPU data upload
- Fix CPU path to zero-pad regime_features when OFI enabled but data missing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:16:32 +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
4e74cb5612 fix(ml): align C51 v_min/v_max with DSR Q-value range (±25)
DSR rewards are ±2. With gamma=0.92, Q-values reach ≈±25.
Old defaults (-10,10) and hyperopt bounds (-2,-0.5)/(0.5,2)
wasted C51 atom resolution on unreachable ranges.

Changes:
- DQNConfig::default() v_min/v_max: -10/10 → -25/25
- DQNConfig aggressive/conservative/emergency: -2/2 → -25/25
- DistributionalConfig::default(): -2/2 → -25/25
- RainbowConfig::default(): -2/2 → -25/25
- Hyperopt bounds: (-2,-0.5)/(0.5,2) → (-30,-5)/(5,30)
- Hyperopt from_continuous clamps: match new bounds
- Test vectors updated for new ranges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:32:18 +01:00
jgrusewski
a442a48acd fix(ml): fix IQN training path — consistent network for train+inference
batch_greedy_actions, batch_softmax_actions, and
batch_hierarchical_softmax_actions all used self.forward() which routes
through the dist_dueling/dueling/standard Q-network. When use_iqn=true,
the training loss trains the IQN QuantileNetwork but inference never
consulted it — the trained IQN weights were ignored at evaluation time.

Add q_values_for_batch() helper that dispatches to the IQN network
(with CVaR or expected-Q reduction) when use_iqn=true, and wire all
three batch methods through it. select_action, select_action_with_confidence,
and select_action_inference already had correct IQN branches.

Add test_iqn_batch_greedy_actions_uses_iqn_network covering training,
batch greedy, batch softmax, and inference paths with IQN enabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:24:42 +01:00
jgrusewski
a16994b0ff feat(ml): add RMSNorm to distributional-dueling network
Add RMSNorm (root mean square normalization) after each hidden layer
in the distributional-dueling Q-network: shared backbone layers, value
stream, and advantage stream. RMSNorm stabilizes activations and
gradients without the overhead of full LayerNorm (no mean centering),
making the network less sensitive to input scale during training.

Architecture per layer: Linear -> LeakyReLU -> RMSNorm

RMSNorm weights are automatically tracked in the existing VarMap since
they are created via VarBuilder::from_varmap with the same shared map.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:19:33 +01:00
jgrusewski
1b40fecccd feat(ml): add Differential Sharpe Ratio (DSR) struct and config fields
Implement the Moody & Saffell (2001) DSR for incremental reward shaping
that directly optimizes risk-adjusted returns. This replaces the broken
double-normalization pipeline (EMA normalizer + risk-adjusted division)
that was producing random noise and preventing DQN learning.

- DifferentialSharpeRatio struct: step(), reset(), eta clamping, +/-5 bounds
- RewardConfig: use_dsr (default false), dsr_eta (default 0.01)
- RewardConfigBuilder: use_dsr() and dsr_eta() builder methods
- RewardFunction: dsr field, reset_dsr(), reset_epoch_state()
- 4 new unit tests (basic, bounded, reset, config roundtrip)
- All 40 reward tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:42:03 +01:00
jgrusewski
c318ffa30a fix(ml): 3 hyperopt objective bugs — sliding windows, tanh normalization, adaptive tau
1. Walk-forward windows: replaced 3 non-overlapping with sliding (50% overlap, ~5 windows).
   Aggregation changed from mean-0.5*std to median-0.5*IQR for outlier robustness.

2. Composite score: tanh normalization prevents Calmar ratio scale dominance
   (0.02% drawdowns → values in thousands drowning out Sharpe/Sortino).

3. Q-value overestimation: new Prometheus gauge foxhunt_training_q_overestimation_ratio,
   warning log when ratio>10 or q_mean>5, adaptive tau doubles when Q-mean growth>0.5/epoch
   (capped at 0.01), decays back when stable.

2742 tests pass, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 02:14:31 +01:00
jgrusewski
8f35c4f480 fix(ml): fix stale C2 defaults in trainer + clarify comments
- noisy_epsilon_floor fallback: 0.05 → 0.0 (C2: NoisyNet only)
- count_bonus_coefficient fallback: 0.1 → 0.0 (C2: disabled)
- use_count_bonus: true → false (C2: conflicts with NoisyNet)
- Fix 4 stale comments saying "REMOVED" → "FIXED to 0.0"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:02:31 +01:00
jgrusewski
9fc1b629c4 feat(ml): M2 PER priority staleness tracking for N-step returns
Replace stub StalenessTracking trait with real StalenessTracker:
- Per-slot timestamp tracking (last_updated_step per experience)
- get_stale_indices() returns oldest-first for refresh
- Wired into PrioritizedReplayBuffer (mark_updated on priority update)
- Epoch-boundary refresh in DQN trainer: recomputes priorities for
  stale entries (>500 steps old) via Q-value forward pass
- 7 staleness tests + 23 PER tests pass, 0 clippy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 22:55:58 +01:00
jgrusewski
b2610d24b2 fix(ml): implement 8 DQN hyperopt research findings (C1-C3, H1-H3, M1, M3)
- C1: Narrow V_min/V_max from [-15,+15] to [-2,+2] for C51 atom resolution
- C2: Remove exploration stacking (28D→26D), keep NoisyNet only
- C3: Fix entropy regularizer for 5-action space, remove 2x/3x multipliers
- H1: Narrow gamma to [0.88, 0.96] (avoid overnight gap discounting)
- H2: Raise tau lower bound to 0.005 (target net tracks in short trials)
- H3: Cap kelly_fractional at 0.75 (no full-Kelly ruin)
- M1: Enable QR-DQN when num_atoms > 100 (hyperopt-toggled)
- M3: Add Q-value gap logging (Q_best - Q_second_best) per epoch

2735 tests pass, 0 clippy warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 22:40:46 +01:00
jgrusewski
14c3ca2507 fix(ml): use backtest action distribution in objective, not training counts
The objective function was using buy/sell/hold percentages from training
metrics instead of the backtest. This caused the optimizer to receive stale
action distribution signals that diverged from actual backtest behavior.

Added buy_action_pct/sell_action_pct/hold_action_pct to BacktestMetrics,
counted during the multi-window backtest loop, and used in extract_objective
when backtest metrics are available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 22:10:59 +01:00
jgrusewski
b1f377babd fix(ml): gate patience early stopping behind early_stopping_enabled, reduce search to 28D
Patience-based early stopping (WAVE 24) was not gated by early_stopping_enabled,
causing ALL hyperopt trials to terminate early and return penalty metrics with
backtest_metrics: None — no backtest ever ran, producing FALLBACK OBJECTIVE 44.6.

Also removes eval_softmax_temp from search space (29D→28D) since backtest uses
greedy argmax, widens gamma range (0.95-0.99→0.90-0.999) for better TPE signal,
and updates all tests to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:56:53 +01:00
jgrusewski
aa7cc44c31 Merge feature/action-diversity-fix: remove diversity hard gate
Resolves early_stopping comment conflict (keep main's wording).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:26:30 +01:00
jgrusewski
05df216f05 fix(ml): remove diversity hard gate, accept greedy mono-action policies
Greedy argmax legitimately converges to 1-2 actions when the model
is confident. The hard gate (unique_actions < 3 → 8.0 penalty) was
blocking ALL trials from using real backtest metrics, forcing FALLBACK.

Changes:
- Remove hard diversity short-circuit gate (unique_actions < 3)
- Reduce soft diversity penalty from 3.0 to 0.8 max (mild TPE signal)
- Fix early_stopping_enabled: false (was self.epochs > 12)
- Update test to validate mono-action policies produce good objectives

Multi-window backtest (3 windows, mean - 0.5*std) already handles
phantom Sharpe from single-bucket flukes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:24:42 +01:00