From 0c9f368d2439ab621ab3794a2156d5777272d650 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Tue, 31 Mar 2026 10:56:20 +0200 Subject: [PATCH] =?UTF-8?q?cleanup:=20remove=20ALL=2032=20enable=5F*=20fea?= =?UTF-8?q?ture=20flags=20=E2=80=94=20all=20features=20unconditional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove 8 enable_* from FeatureConfig (ml-features) and 24 from DQNHyperparameters (ml). All features are always active — no boolean toggles, no dead conditional branches, no false impression of optionality. FeatureConfig reduced to single `phase: FeaturePhase` field. DQNHyperparameters loses 24 fields, downstream conditionals collapsed. TOML configs cleaned of all enable_* lines. 16 files changed, -461/+181 lines. Co-Authored-By: Claude Opus 4.6 (1M context) --- config/training/dqn-localdev.toml | 7 - config/training/dqn-production.toml | 7 - crates/ml-features/src/config.rs | 216 +++------- .../src/data_loaders/dbn_sequence_loader.rs | 83 ++-- crates/ml/src/hyperopt/adapters/dqn.rs | 18 +- crates/ml/src/trainers/dqn/config.rs | 115 +---- crates/ml/src/trainers/dqn/data_loading.rs | 6 +- crates/ml/src/trainers/dqn/fused_training.rs | 4 +- crates/ml/src/trainers/dqn/risk.rs | 10 +- .../src/trainers/dqn/smoke_tests/helpers.rs | 3 - .../src/trainers/dqn/trainer/constructor.rs | 80 ++-- crates/ml/src/trainers/dqn/trainer/mod.rs | 19 +- crates/ml/src/training_profile.rs | 28 -- crates/ml/tests/dqn_trainer_p1_tests.rs | 26 +- .../ml/tests/hyperopt_action_masking_test.rs | 12 +- .../ml/tests/wave16q_ohlcv_mutation_test.rs | 4 +- .../2026-03-31-imbalance-bars-and-cleanup.md | 397 ++++++++++++++++++ 17 files changed, 576 insertions(+), 459 deletions(-) create mode 100644 docs/superpowers/plans/2026-03-31-imbalance-bars-and-cleanup.md diff --git a/config/training/dqn-localdev.toml b/config/training/dqn-localdev.toml index 0b8a7dde7..e18c657f8 100644 --- a/config/training/dqn-localdev.toml +++ b/config/training/dqn-localdev.toml @@ -73,32 +73,25 @@ spectral_decoupling_lambda = 0.01 gradient_clip_norm = 1.0 [generalization] -enable_domain_randomization = true shrink_perturb_interval = 20 shrink_perturb_alpha = 0.85 shrink_perturb_sigma = 0.01 adversarial_dd_threshold = 0.005 adversarial_epochs_trigger = 5 -enable_anti_lr = true anti_lr_good_mult = 3.0 anti_lr_bad_mult = 0.3 anti_lr_sharpe_threshold = 0.3 feature_mask_fraction = 0.3 feature_noise_scale = 0.1 -enable_vol_normalization = true -enable_causal_intervention = false causal_weight = 0.1 causal_intervention_interval = 10 -enable_adversarial_self_play = false adversarial_warmup_epochs = 50 adversarial_checkpoint_interval = 50 -enable_gradient_vaccine = false bottleneck_dim = 0 stochastic_depth_prob = 0.2 asymmetric_dd_weight = 0.5 time_reversal_mod = 5 regret_blend = 0.3 -enable_mirror_universe = true position_entropy_weight = 0.01 trade_clustering_penalty = 0.05 ensemble_disagreement_penalty = 0.3 diff --git a/config/training/dqn-production.toml b/config/training/dqn-production.toml index 30edff336..62ca0bfe8 100644 --- a/config/training/dqn-production.toml +++ b/config/training/dqn-production.toml @@ -78,32 +78,25 @@ spectral_decoupling_lambda = 0.01 gradient_clip_norm = 1.0 [generalization] -enable_domain_randomization = true shrink_perturb_interval = 20 shrink_perturb_alpha = 0.85 shrink_perturb_sigma = 0.01 adversarial_dd_threshold = 0.005 adversarial_epochs_trigger = 5 -enable_anti_lr = true anti_lr_good_mult = 3.0 anti_lr_bad_mult = 0.3 anti_lr_sharpe_threshold = 0.3 feature_mask_fraction = 0.3 feature_noise_scale = 0.1 -enable_vol_normalization = true -enable_causal_intervention = false causal_weight = 0.1 causal_intervention_interval = 10 -enable_adversarial_self_play = false adversarial_warmup_epochs = 50 adversarial_checkpoint_interval = 50 -enable_gradient_vaccine = false bottleneck_dim = 0 stochastic_depth_prob = 0.2 asymmetric_dd_weight = 0.5 time_reversal_mod = 5 regret_blend = 0.3 -enable_mirror_universe = true position_entropy_weight = 0.01 trade_clustering_penalty = 0.05 ensemble_disagreement_penalty = 0.3 diff --git a/crates/ml-features/src/config.rs b/crates/ml-features/src/config.rs index 7b615f870..a2d18526a 100644 --- a/crates/ml-features/src/config.rs +++ b/crates/ml-features/src/config.rs @@ -208,43 +208,13 @@ pub fn wave_d_features() -> Vec { /// Feature extraction configuration for Wave 19 progressive engineering /// -/// Tracks which feature groups are enabled across Wave A/B/C/D phases. +/// All feature groups are always active. The `phase` field determines +/// the total feature count (Wave A=26, B=36, C=201, D=225). /// Used by both training (`DbnSequenceLoader`) and inference (`ProductionFeatureExtractorAdapter`). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureConfig { /// Feature engineering phase (Wave A/B/C/D) pub phase: FeaturePhase, - - /// Enable baseline OHLCV features (5 features) - pub enable_ohlcv: bool, - - /// Enable technical indicators (21 features) - /// Wave A: RSI, MACD, Bollinger, ATR, ADX, CCI, Stochastic, EMAs, Williams %R, ROC, Ultimate Osc, OBV, MFI, VWAP - pub enable_technical_indicators: bool, - - /// Enable microstructure features (3 features) - /// Wave A: Amihud Illiquidity, Roll Measure, Corwin-Schultz Spread - pub enable_microstructure: bool, - - /// Enable alternative bar features (10 features) - /// Wave B: Dollar bars, Volume bars, Tick bars, Run bars, Imbalance bars - pub enable_alternative_bars: bool, - - /// Enable barrier optimization features (variable) - /// Wave B: Triple-barrier labels, Meta-labeling - pub enable_barrier_optimization: bool, - - /// Enable fractional differentiation features (variable) - /// Wave C: Stationarity with memory preservation - pub enable_fractional_diff: bool, - - /// Enable regime detection features (variable) - /// Wave C: CUSUM structural breaks, Adaptive strategies - pub enable_regime_detection: bool, - - /// Enable Wave D regime detection features (24 features) - /// Wave D: CUSUM statistics (10), ADX directional (5), Regime transitions (5), Adaptive strategies (4) - pub enable_wave_d_regime: bool, } /// Feature engineering phase (Wave 19 progressive implementation) @@ -273,23 +243,11 @@ impl FeatureConfig { /// Feature breakdown: /// - OHLCV: 5 features (open, high, low, close, volume) /// - Technical indicators: 21 features (RSI, MACD, Bollinger, ATR, ADX, CCI, Stochastic, EMAs, etc.) - /// - Microstructure: 0 features (not yet integrated into 26-feature system) /// /// Total: 26 features - /// - /// Note: Microstructure features (Amihud, Roll, Corwin-Schultz) exist in ml crate - /// but are not yet integrated into `common/ml_strategy.rs` 26-feature system. pub const fn wave_a() -> Self { Self { phase: FeaturePhase::WaveA, - enable_ohlcv: true, - enable_technical_indicators: true, - enable_microstructure: false, // Not yet integrated into 26-feature system - enable_alternative_bars: false, - enable_barrier_optimization: false, - enable_fractional_diff: false, - enable_regime_detection: false, - enable_wave_d_regime: false, } } @@ -303,14 +261,6 @@ impl FeatureConfig { pub const fn wave_b() -> Self { Self { phase: FeaturePhase::WaveB, - enable_ohlcv: true, - enable_technical_indicators: true, - enable_microstructure: false, - enable_alternative_bars: true, - enable_barrier_optimization: true, // Triple-barrier labels - enable_fractional_diff: false, - enable_regime_detection: false, - enable_wave_d_regime: false, } } @@ -324,14 +274,6 @@ impl FeatureConfig { pub const fn wave_c() -> Self { Self { phase: FeaturePhase::WaveC, - enable_ohlcv: true, - enable_technical_indicators: true, - enable_microstructure: true, - enable_alternative_bars: true, - enable_barrier_optimization: true, - enable_fractional_diff: true, - enable_regime_detection: true, - enable_wave_d_regime: false, // Wave D features disabled for Wave C } } @@ -349,129 +291,91 @@ impl FeatureConfig { pub const fn wave_d() -> Self { Self { phase: FeaturePhase::WaveD, - enable_ohlcv: true, - enable_technical_indicators: true, - enable_microstructure: true, - enable_alternative_bars: true, - enable_barrier_optimization: true, - enable_fractional_diff: true, - enable_regime_detection: true, - enable_wave_d_regime: true, } } - /// Calculate total feature count based on enabled feature groups + /// Calculate total feature count based on the feature phase. /// - /// Returns the total number of features that will be extracted - /// based on the current configuration. + /// All feature groups within a phase are always active. + /// - Wave A: 26 (OHLCV 5 + Technical 21) + /// - Wave B: 36 (+ Alternative bars 10) + /// - Wave C: 201 (+ Microstructure 3 + Fractional diff 162) + /// - Wave D: 225 (+ Wave D regime 24) pub const fn feature_count(&self) -> usize { - let mut count = 0; - - if self.enable_ohlcv { - count += 5; // open, high, low, close, volume + match self.phase { + FeaturePhase::WaveA => 26, // OHLCV (5) + Technical (21) + FeaturePhase::WaveB => 36, // + Alternative bars (10) + FeaturePhase::WaveC => 201, // + Microstructure (3) + Fractional diff (162) + FeaturePhase::WaveD => 225, // + Wave D regime (24) } - - if self.enable_technical_indicators { - count += 21; // RSI, MACD (2), Bollinger, ATR, ADX, CCI, Stochastic (2), EMAs (3), EMA crosses (2), Williams %R, ROC, Ultimate Osc, OBV, MFI, VWAP - } - - if self.enable_microstructure { - count += 3; // Amihud Illiquidity, Roll Measure, Corwin-Schultz Spread - } - - if self.enable_alternative_bars { - count += 10; // Dollar bars, Volume bars, Tick bars, Run bars, Imbalance bars (approx) - } - - if self.enable_barrier_optimization { - // Barrier features are labels, not input features - // They affect training but don't add to input dimension - // count += 0; - } - - if self.enable_fractional_diff { - // Wave C should reach 201 total features - // Base: OHLCV (5) + Technical (21) + Microstructure (3) + Alternative bars (10) = 39 - // Therefore: 201 - 39 = 162 additional features - count += 162; - } - - // Note: Wave C's regime_detection flag is part of fractional_diff feature count - // to achieve the documented 201 features for Wave C - - if self.enable_wave_d_regime { - count += 24; // Wave D: CUSUM (10) + ADX (5) + Transitions (5) + Adaptive (4) - } - - count } - /// Get feature indices for each group + /// Get feature indices for each group based on the phase. /// - /// Returns (`start_idx`, `end_idx`) for each enabled feature group. + /// Returns (`start_idx`, `end_idx`) for each feature group active in the current phase. /// This allows data loaders to know which indices correspond to which features. pub fn feature_indices(&self) -> FeatureIndices { let mut indices = FeatureIndices::default(); let mut current_idx = 0; - if self.enable_ohlcv { - indices.ohlcv = Some((current_idx, current_idx + 5)); - current_idx += 5; - } + // OHLCV: always present (all phases) + indices.ohlcv = Some((current_idx, current_idx + 5)); + current_idx += 5; - if self.enable_technical_indicators { - indices.technical_indicators = Some((current_idx, current_idx + 21)); - current_idx += 21; - } + // Technical indicators: always present (all phases) + indices.technical_indicators = Some((current_idx, current_idx + 21)); + current_idx += 21; - if self.enable_microstructure { - indices.microstructure = Some((current_idx, current_idx + 3)); - current_idx += 3; - } - - if self.enable_alternative_bars { + if matches!(self.phase, FeaturePhase::WaveB | FeaturePhase::WaveC | FeaturePhase::WaveD) { + // Alternative bars: Wave B+ indices.alternative_bars = Some((current_idx, current_idx + 10)); current_idx += 10; } - if self.enable_fractional_diff { - // Wave C adds 162 features to reach 201 total (39 base + 162 = 201) + if matches!(self.phase, FeaturePhase::WaveC | FeaturePhase::WaveD) { + // Microstructure: Wave C+ + indices.microstructure = Some((current_idx, current_idx + 3)); + current_idx += 3; + + // Fractional diff: Wave C+ (162 features to reach 201 total) indices.fractional_diff = Some((current_idx, current_idx + 162)); current_idx += 162; } - // Note: regime_detection is included in fractional_diff count for Wave C - - if self.enable_wave_d_regime { + if matches!(self.phase, FeaturePhase::WaveD) { + // Wave D regime features (24) indices.wave_d_regime = Some((current_idx, current_idx + 24)); - // Note: current_idx is intentionally not updated after this - // as this is the last feature group } indices } - /// Check if a specific feature group is enabled + /// Check if a specific feature group is enabled. + /// All feature groups within the current phase are always active. pub const fn is_enabled(&self, group: FeatureGroup) -> bool { match group { - FeatureGroup::OHLCV => self.enable_ohlcv, - FeatureGroup::TechnicalIndicators => self.enable_technical_indicators, - FeatureGroup::Microstructure => self.enable_microstructure, - FeatureGroup::AlternativeBars => self.enable_alternative_bars, - FeatureGroup::BarrierOptimization => self.enable_barrier_optimization, - FeatureGroup::FractionalDiff => self.enable_fractional_diff, - FeatureGroup::RegimeDetection => self.enable_regime_detection, - FeatureGroup::WaveDRegime => self.enable_wave_d_regime, + // OHLCV and Technical Indicators are always active in all phases + FeatureGroup::OHLCV | FeatureGroup::TechnicalIndicators => true, + // Alternative bars and barrier optimization: Wave B+ + FeatureGroup::AlternativeBars | FeatureGroup::BarrierOptimization => { + matches!(self.phase, FeaturePhase::WaveB | FeaturePhase::WaveC | FeaturePhase::WaveD) + } + // Microstructure, fractional diff, regime detection: Wave C+ + FeatureGroup::Microstructure | FeatureGroup::FractionalDiff | FeatureGroup::RegimeDetection => { + matches!(self.phase, FeaturePhase::WaveC | FeaturePhase::WaveD) + } + // Wave D regime: only Wave D + FeatureGroup::WaveDRegime => matches!(self.phase, FeaturePhase::WaveD), } } /// Get Wave D feature definitions (indices 201-224) /// /// Returns a vector of all 24 Wave D features with their indices, - /// names, and categories. This is useful for feature extraction - /// pipelines and model training. + /// names, and categories when the phase is Wave D. Returns empty + /// for earlier phases. pub fn get_wave_d_features(&self) -> Vec { - if self.enable_wave_d_regime { + if matches!(self.phase, FeaturePhase::WaveD) { wave_d_features() } else { vec![] @@ -530,10 +434,6 @@ mod tests { fn test_wave_a_config() { let config = FeatureConfig::wave_a(); assert_eq!(config.phase, FeaturePhase::WaveA); - assert!(config.enable_ohlcv); - assert!(config.enable_technical_indicators); - assert!(!config.enable_microstructure); // Not yet integrated - assert!(!config.enable_alternative_bars); assert_eq!(config.feature_count(), 26); } @@ -541,8 +441,6 @@ mod tests { fn test_wave_b_config() { let config = FeatureConfig::wave_b(); assert_eq!(config.phase, FeaturePhase::WaveB); - assert!(config.enable_alternative_bars); - assert!(config.enable_barrier_optimization); assert_eq!(config.feature_count(), 36); } @@ -550,9 +448,6 @@ mod tests { fn test_wave_c_config() { let config = FeatureConfig::wave_c(); assert_eq!(config.phase, FeaturePhase::WaveC); - assert!(config.enable_fractional_diff); - assert!(config.enable_regime_detection); - assert!(!config.enable_wave_d_regime); assert_eq!(config.feature_count(), 201); // Wave C has exactly 201 features } @@ -560,9 +455,6 @@ mod tests { fn test_wave_d_config() { let config = FeatureConfig::wave_d(); assert_eq!(config.phase, FeaturePhase::WaveD); - assert!(config.enable_fractional_diff); - assert!(config.enable_regime_detection); - assert!(config.enable_wave_d_regime); assert_eq!(config.feature_count(), 225); // Wave D has exactly 225 features } @@ -611,8 +503,8 @@ mod tests { assert_eq!(indices.ohlcv, Some((0, 5))); assert_eq!(indices.technical_indicators, Some((5, 26))); - assert_eq!(indices.microstructure, None); // Not enabled in Wave A - assert_eq!(indices.alternative_bars, None); + assert_eq!(indices.microstructure, None); // Not in Wave A phase + assert_eq!(indices.alternative_bars, None); // Not in Wave A phase } #[test] @@ -643,11 +535,13 @@ mod tests { let config = FeatureConfig::wave_a(); assert!(config.is_enabled(FeatureGroup::OHLCV)); assert!(config.is_enabled(FeatureGroup::TechnicalIndicators)); - assert!(!config.is_enabled(FeatureGroup::AlternativeBars)); - assert!(!config.is_enabled(FeatureGroup::WaveDRegime)); + assert!(!config.is_enabled(FeatureGroup::AlternativeBars)); // Not in Wave A + assert!(!config.is_enabled(FeatureGroup::WaveDRegime)); // Not in Wave A let config = FeatureConfig::wave_d(); assert!(config.is_enabled(FeatureGroup::WaveDRegime)); + assert!(config.is_enabled(FeatureGroup::AlternativeBars)); + assert!(config.is_enabled(FeatureGroup::FractionalDiff)); } #[test] @@ -658,7 +552,7 @@ mod tests { let config = FeatureConfig::wave_c(); let features = config.get_wave_d_features(); - assert_eq!(features.len(), 0); // Wave C doesn't have Wave D features enabled + assert_eq!(features.len(), 0); // Wave C phase doesn't include Wave D features } #[test] diff --git a/crates/ml/src/data_loaders/dbn_sequence_loader.rs b/crates/ml/src/data_loaders/dbn_sequence_loader.rs index caf17302e..aeedf058e 100644 --- a/crates/ml/src/data_loaders/dbn_sequence_loader.rs +++ b/crates/ml/src/data_loaders/dbn_sequence_loader.rs @@ -1275,25 +1275,21 @@ impl DbnSequenceLoader { // Build feature vector based on FeatureConfig let mut features = Vec::with_capacity(self.d_model); - // 1. Base OHLCV (5 features) - always included in Wave A/B/C - if self.feature_config.enable_ohlcv { - features.push(o as f32); - features.push(h as f32); - features.push(l as f32); - features.push(c as f32); - features.push(v as f32); - } + // 1. Base OHLCV (5 features) - always active + features.push(o as f32); + features.push(h as f32); + features.push(l as f32); + features.push(c as f32); + features.push(v as f32); - // 2. Derived features (4 features) - part of Wave A technical indicators - if self.feature_config.enable_technical_indicators { - features.push(range as f32); - features.push(body as f32); - features.push(upper_wick as f32); - features.push(lower_wick as f32); - } + // 2. Derived features (4 features) - part of technical indicators (always active) + features.push(range as f32); + features.push(body as f32); + features.push(upper_wick as f32); + features.push(lower_wick as f32); - // 3. Price ratios (10 features) - part of Wave A technical indicators - if self.feature_config.enable_technical_indicators { + // 3. Price ratios (10 features) - part of technical indicators (always active) + { let safe_div = |a: f64, b: f64| if b.abs() > 1e-8 { (a / b) as f32 } else { 0.0 }; features.push(safe_div(c, o)); // close/open ratio @@ -1308,8 +1304,8 @@ impl DbnSequenceLoader { features.push(safe_div(v, (h + l + c + o) / 4.0)); // volume/price ratio } - // 4. Log returns (4 features) - part of Wave A technical indicators - if self.feature_config.enable_technical_indicators { + // 4. Log returns (4 features) - part of technical indicators (always active) + { let safe_ln = |a: f64, b: f64| { let ratio = a / b.max(1e-8); if ratio > 0.0 { @@ -1324,16 +1320,15 @@ impl DbnSequenceLoader { features.push(safe_ln(c, h)); // log close/high } - // 5. Price deltas (3 features) - part of Wave A technical indicators - // REMOVED: (c - l) to match 26-feature count - if self.feature_config.enable_technical_indicators { + // 5. Price deltas (3 features) - part of technical indicators (always active) + { features.push((c - o) as f32); // raw price change features.push((h - o) as f32); // open to high features.push((l - o) as f32); // open to low } - // 6. Alternative bar features (10 features) - Wave B - if self.feature_config.enable_alternative_bars { + // 6. Alternative bar features (10 features) - Wave B+ + if self.feature_config.is_enabled(crate::features::config::FeatureGroup::AlternativeBars) { // Zero-padded: alternative bars (dollar, volume, tick, run, imbalance) // require tick-level trade data to construct variable-frequency bars. // OHLCV-1min bars from DBN files lack the per-trade granularity @@ -1344,8 +1339,8 @@ impl DbnSequenceLoader { } } - // 7. Microstructure features (3 features) - Wave A/C - if self.feature_config.enable_microstructure { + // 7. Microstructure features (3 features) - Wave C+ + if self.feature_config.is_enabled(crate::features::config::FeatureGroup::Microstructure) { // 7a. Amihud Illiquidity = |return| / dollar_volume // Uses previous normalized close for return computation. let log_ret = if let Some(pc) = self.prev_close_normalized { @@ -1400,8 +1395,8 @@ impl DbnSequenceLoader { self.prev_close_normalized = Some(c); } - // 8. Fractional differentiation features (20 features) - Wave C - if self.feature_config.enable_fractional_diff { + // 8. Fractional differentiation features (20 features) - Wave C+ + if self.feature_config.is_enabled(crate::features::config::FeatureGroup::FractionalDiff) { // Zero-padded: fractional differentiation requires a windowed // convolution with binomial weights (fracdiff d~0.3-0.5) over // a lookback of 50-100 bars. This is a full-sequence transform @@ -1413,8 +1408,8 @@ impl DbnSequenceLoader { } } - // 9. Regime detection features (10 features) - Wave C - if self.feature_config.enable_regime_detection { + // 9. Regime detection features (10 features) - Wave C+ + if self.feature_config.is_enabled(crate::features::config::FeatureGroup::RegimeDetection) { // TODO (Wave C): Add CUSUM structural breaks, regime indicators for _ in 0..10 { features.push(0.0); @@ -1422,7 +1417,7 @@ impl DbnSequenceLoader { } // 10. Wave D regime features (24 features) - Wave D - if self.feature_config.enable_wave_d_regime { + if self.feature_config.is_enabled(crate::features::config::FeatureGroup::WaveDRegime) { // CUSUM Statistics (indices 201-210, 10 features) // Use log return as input to CUSUM detector let log_return = if o.abs() > 1e-8 { (c / o).ln() } else { 0.0 }; @@ -1509,13 +1504,12 @@ impl DbnSequenceLoader { // Use price as OHLCV (all same for trades) let mut features = Vec::with_capacity(self.d_model); - if self.feature_config.enable_ohlcv { - features.push(p as f32); // open - features.push(p as f32); // high - features.push(p as f32); // low - features.push(p as f32); // close - features.push(s as f32); // volume - } + // OHLCV always active + features.push(p as f32); // open + features.push(p as f32); // high + features.push(p as f32); // low + features.push(p as f32); // close + features.push(s as f32); // volume // Pad remaining features with zeros while features.len() < self.feature_config.feature_count() { @@ -1538,13 +1532,12 @@ impl DbnSequenceLoader { // Use mid-price as OHLCV (all same for quotes) let mut features = Vec::with_capacity(self.d_model); - if self.feature_config.enable_ohlcv { - features.push(mid as f32); // open - features.push(a as f32); // high (ask) - features.push(b as f32); // low (bid) - features.push(mid as f32); // close - features.push(spread as f32); // volume (use spread) - } + // OHLCV always active + features.push(mid as f32); // open + features.push(a as f32); // high (ask) + features.push(b as f32); // low (bid) + features.push(mid as f32); // close + features.push(spread as f32); // volume (use spread) // Pad remaining features with zeros while features.len() < self.feature_config.feature_count() { diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index ce730b285..5f6b2f60d 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -485,8 +485,7 @@ pub struct DQNTrainer { target_update_mode: crate::trainers::TargetUpdateMode, /// Target network hard update frequency (steps) target_update_frequency: usize, - /// WAVE 16 (Agent 38): Enable preprocessing (log returns + normalization) - enable_preprocessing: bool, + // Preprocessing is always active /// WAVE 16 (Agent 38): Preprocessing window size preprocessing_window: i64, /// WAVE 16 (Agent 38): Preprocessing clip sigma @@ -630,7 +629,7 @@ impl DQNTrainer { tau: 0.001, // Soft updates: 0.1% target blend per step (Rainbow DQN standard) target_update_mode: crate::trainers::TargetUpdateMode::Soft, // Soft updates prevent Q-value explosion target_update_frequency: 1, // Update every step with soft blending - enable_preprocessing: true, // Preprocessing enabled by default (Wave 14) + // Preprocessing is always active (Wave 14) preprocessing_window: 50, // Default: 50-bar rolling window preprocessing_clip_sigma: 5.0, // Default: clip at ±5σ enable_backtest: true, // Wave 8: Backtest integration operational - enabled by default @@ -741,10 +740,9 @@ impl DQNTrainer { self } - /// Configure preprocessing settings - /// Default: Enabled with window=50, clip_sigma=5.0 - pub fn with_preprocessing(mut self, enable: bool, window: i64, clip_sigma: f64) -> Self { - self.enable_preprocessing = enable; + /// Configure preprocessing settings (always active) + /// Default: window=50, clip_sigma=5.0 + pub fn with_preprocessing(mut self, _enable: bool, window: i64, clip_sigma: f64) -> Self { self.preprocessing_window = window; self.preprocessing_clip_sigma = clip_sigma; self @@ -810,7 +808,7 @@ impl DQNTrainer { let mut preload_hyperparams = DQNHyperparameters::default(); preload_hyperparams.mbp10_data_dir = self.mbp10_data_dir.clone(); preload_hyperparams.trades_data_dir = self.trades_data_dir.clone(); - preload_hyperparams.enable_regime_qnetwork = false; + // Regime Q-network is always active (lightweight preload uses full agent type) preload_hyperparams.buffer_size = 1; let mut loader = InternalDQNTrainer::new_with_device(preload_hyperparams, self.device.clone()) .map_err(|e| MLError::TrainingError(format!( @@ -2033,14 +2031,14 @@ impl HyperparameterOptimizable for DQNTrainer { hyperparams.early_stopping_enabled = false; // Disable for hyperopt hyperparams.min_epochs_before_stopping = self.epochs; hyperparams.warmup_steps = 0; // CRITICAL: batch training path doesn't increment total_steps - hyperparams.enable_preprocessing = self.enable_preprocessing; + // Preprocessing is always active hyperparams.preprocessing_window = self.preprocessing_window; hyperparams.preprocessing_clip_sigma = self.preprocessing_clip_sigma; hyperparams.target_update_mode = self.target_update_mode.clone(); hyperparams.target_update_frequency = self.target_update_frequency; #[allow(clippy::cast_possible_truncation)] { hyperparams.initial_capital = self.initial_capital as f32; } - hyperparams.enable_risk_adjusted_rewards = false; // DSR handles risk-adjustment + // Risk-adjusted rewards always active (DSR handles risk-adjustment) hyperparams.mbp10_data_dir = self.mbp10_data_dir.clone(); hyperparams.trades_data_dir = self.trades_data_dir.clone(); diff --git a/crates/ml/src/trainers/dqn/config.rs b/crates/ml/src/trainers/dqn/config.rs index 74a8f05ee..ff603fc5f 100644 --- a/crates/ml/src/trainers/dqn/config.rs +++ b/crates/ml/src/trainers/dqn/config.rs @@ -804,8 +804,6 @@ pub struct DQNHyperparameters { pub hold_penalty_weight: f64, /// Price movement threshold for HOLD penalty (as fraction, e.g., 0.02 = 2%) pub movement_threshold: f64, - /// Enable preprocessing (log returns + normalization + outlier clipping) - pub enable_preprocessing: bool, /// Preprocessing window size (default: 50) pub preprocessing_window: i64, /// Preprocessing clip sigma (default: 5.0) @@ -849,12 +847,6 @@ pub struct DQNHyperparameters { pub cash_reserve_percent: f64, // WAVE 16S: Adaptive Risk Management Features - /// Enable Kelly criterion position sizing - pub enable_kelly_sizing: bool, - /// Enable volatility-adjusted epsilon exploration - pub enable_volatility_epsilon: bool, - /// Enable risk-adjusted rewards (Sharpe ratio) - pub enable_risk_adjusted_rewards: bool, /// Kelly fractional multiplier (0.5 = half-Kelly, conservative) pub kelly_fractional: f64, /// Maximum Kelly fraction (cap at 0.25 = 25% of portfolio) @@ -865,22 +857,8 @@ pub struct DQNHyperparameters { pub volatility_window: usize, // WAVE 35: Advanced Features - Regime-Conditional Q-Networks + Compliance Engine - /// Enable regime-conditional Q-network (3 heads: Trending, Ranging, Volatile) - pub enable_regime_qnetwork: bool, - /// Enable compliance engine (real-time regulatory validation) - pub enable_compliance: bool, - - // WAVE 16: Core Risk Management Features - /// Enable drawdown monitoring (15% max drawdown early stop) - pub enable_drawdown_monitoring: bool, - /// Enable position limits (3-tier: absolute ±10.0, notional $1M, concentration 10%) - pub enable_position_limits: bool, - /// Enable circuit breaker (5-failure trip mechanism) - pub enable_circuit_breaker: bool, - - /// Domain randomization: per-epoch jitter on spread, tx_cost, fills, episode starts. - /// Prevents memorization of fixed simulation parameters. - pub enable_domain_randomization: bool, + // (regime Q-network, compliance, drawdown monitoring, position limits, + // circuit breaker, and domain randomization are always active) /// Periodic shrink-and-perturb interval (0=disabled, 20=every 20 epochs) pub shrink_perturb_interval: usize, @@ -906,8 +884,7 @@ pub struct DQNHyperparameters { /// #24 Anti-intuitive LR: when epoch Sharpe > threshold, INCREASE LR to /// kick the model out of overfit minima. When Sharpe < -threshold, decrease - /// to stabilize. Opposite of standard practice. - pub enable_anti_lr: bool, + /// to stabilize. Opposite of standard practice. Always active. /// LR multiplier when epoch Sharpe is "good" (> threshold) pub anti_lr_good_mult: f64, /// LR multiplier when epoch Sharpe is "bad" (< -threshold) @@ -921,19 +898,14 @@ pub struct DQNHyperparameters { pub ensemble_disagreement_penalty: f64, /// #34 Causal intervention: compute per-feature causal sensitivity by running - /// intervened forward passes. Features that don't change Q-values are noise. - /// Adds -causal_weight * sum(sensitivities) to the loss (maximizes sensitivity). - pub enable_causal_intervention: bool, + /// intervened forward passes. Always active. /// #34 Weight for causal regularization loss. 0.0 = disabled. pub causal_weight: f64, /// #34 How often to run causal intervention (every N training steps). /// Higher = less overhead. 10 = every 10th step. pub causal_intervention_interval: usize, - /// #33 Adversarial self-play: train a saboteur that controls market microstructure - /// to maximize the trader's losses. The saboteur uses evolutionary strategy - /// (CMA-ES lite) to find the worst-case spread, fill probability, and slippage. - pub enable_adversarial_self_play: bool, + /// #33 Adversarial self-play: always active. /// #33 Warmup epochs before self-play begins (let trader learn basics first) pub adversarial_warmup_epochs: usize, /// #33 How often to update the saboteur's reference checkpoint (epochs) @@ -949,13 +921,8 @@ pub struct DQNHyperparameters { /// features. 0.0 = disabled. pub feature_noise_scale: f64, - /// #13 Volatility regime normalization: divide return-based features by - /// realized vol to make them scale-invariant across regimes. - pub enable_vol_normalization: bool, - - /// #10 Mirror universe: alternate epochs with negated returns + inverted actions. - /// Doubles data diversity, forces direction-invariant learning. - pub enable_mirror_universe: bool, + // #13 Volatility regime normalization: always active. + // #10 Mirror universe: always active. /// #19 Position entropy: reward += weight * H(position_histogram) at episode end. /// Forces the model to visit multiple exposure levels. 0.0 = disabled. @@ -979,9 +946,7 @@ pub struct DQNHyperparameters { /// #20 Fraction of weights to prune (0.7 = remove 70% smallest). pub pruning_fraction: f64, - /// #32 Gradient vaccine: project out overfitting gradient components using - /// a held-out validation batch. Mathematical guarantee against overfitting. - pub enable_gradient_vaccine: bool, + // #32 Gradient vaccine: always active. /// #31 Temporal causal bottleneck dimension. 0 = disabled, 2 = maximum compression. /// All market features are compressed through [market_dim → bottleneck_dim] linear + tanh @@ -1031,13 +996,7 @@ pub struct DQNHyperparameters { /// 0.0 = disabled. pub asymmetric_dd_weight: f64, - // Wave 16 Portfolio Features - /// Enable action masking (filters invalid actions based on position limits) - pub enable_action_masking: bool, - /// Enable entropy regularization (prevents policy collapse) - pub enable_entropy_regularization: bool, - /// Enable stress testing (robustness validation) - pub enable_stress_testing: bool, + // Wave 16 Portfolio Features (action masking, entropy regularization, stress testing always active) /// Maximum absolute position size for action masking (1.0-10.0 contracts) /// Default: 2.0 (matches current production behavior) pub max_position_absolute: f64, @@ -1052,9 +1011,7 @@ pub struct DQNHyperparameters { /// Transaction cost multiplier for reward calculation pub transaction_cost_multiplier: f64, - // Wave 3 (Phase 2): Triple Barrier Method - /// Enable triple barrier method for multi-step reward labeling - pub enable_triple_barrier: bool, + // Wave 3 (Phase 2): Triple Barrier Method (always active) pub triple_barrier_profit_target_bps: u32, pub triple_barrier_stop_loss_bps: u32, pub triple_barrier_max_holding_seconds: u64, @@ -1170,9 +1127,7 @@ pub struct DQNHyperparameters { /// Rolling window size for Sharpe calculation (default: 20) pub sharpe_window: usize, - // P1.6: Adaptive Dropout Scheduling - /// Enable adaptive dropout scheduler (reduces dropout rate over training) - pub enable_dropout_scheduler: bool, + // P1.6: Adaptive Dropout Scheduling (always active) /// Initial dropout rate (default: 0.5) pub dropout_initial: f64, /// Final dropout rate (default: 0.1) @@ -1189,15 +1144,11 @@ pub struct DQNHyperparameters { // P1.8: Curiosity-Driven Exploration (already has curiosity_weight above) // Uses existing curiosity_weight parameter - // P1.9: Generalized Advantage Estimation (GAE) - /// Enable GAE for lower variance returns - pub enable_gae: bool, + // P1.9: Generalized Advantage Estimation (GAE) — always active /// GAE lambda parameter (default: 0.95) pub gae_lambda: f64, - // P1.11: Noisy Network Sigma Scheduling - /// Enable noisy sigma scheduler (anneals noise over training) - pub enable_noisy_sigma_scheduler: bool, + // P1.11: Noisy Network Sigma Scheduling — always active /// Initial sigma for noisy networks (default: 0.6) pub noisy_sigma_initial: f64, /// Final sigma for noisy networks (default: 0.4) @@ -1330,11 +1281,7 @@ pub struct DQNHyperparameters { /// Only used when `use_branching` is true. pub branch_hidden_dim: usize, - // GPU Walk-Forward: expanding-window cross-validation entirely on GPU - /// Enable GPU-resident walk-forward evaluation. - /// When true, the entire dataset is uploaded to GPU VRAM once and fold - /// transitions use index ranges (zero re-upload). Requires CUDA. - pub enable_gpu_walk_forward: bool, + // GPU Walk-Forward: expanding-window cross-validation entirely on GPU (always active) /// Initial training window as a fraction of total data (default: 0.5). pub wf_initial_train_fraction: f64, /// Validation window as a fraction of total data (default: 0.125). @@ -1566,7 +1513,6 @@ impl DQNHyperparameters { gradient_clip_norm: Some(10.0), // C51 101-atoms × 3 branches → gradient naturally 300x larger than DQN hold_penalty_weight: 0.01, // Default: 1% penalty weight movement_threshold: 0.02, // Default: 2% price movement threshold - enable_preprocessing: true, // Default: preprocessing enabled (Wave 14 Agent 32) preprocessing_window: 50, // Default: 50-bar rolling window preprocessing_clip_sigma: 5.0, // Default: clip at ±5σ @@ -1588,25 +1534,14 @@ impl DQNHyperparameters { cash_reserve_percent: 0.0, // Default: no reserve (backward compatible) // WAVE 16S: Adaptive Risk Management - enable_kelly_sizing: true, // Default: Kelly position sizing enabled - enable_volatility_epsilon: true, // Default: volatility-adjusted exploration enabled - enable_risk_adjusted_rewards: true, // Default: Sharpe-based rewards enabled kelly_fractional: 0.5, // Default: half-Kelly (conservative) kelly_max_fraction: 0.25, // Default: max 25% of portfolio kelly_min_trades: 20, // Default: 20 trades minimum for statistics volatility_window: 20, // Default: 20-period rolling window - // WAVE 35: Advanced Features (WAVE 16S: NOW ENABLED BY DEFAULT) - enable_regime_qnetwork: true, // Default: regime-conditional Q-networks enabled - enable_compliance: true, // Default: compliance engine enabled + // WAVE 35: Advanced Features (always active) + // WAVE 16: Core Risk Management (always active) - // WAVE 16: Core Risk Management (default: ALL ENABLED) - enable_drawdown_monitoring: true, // Default: drawdown monitoring enabled (15% max) - enable_position_limits: true, // Default: 3-tier position limits enabled - enable_circuit_breaker: true, // Default: circuit breaker enabled (5-failure trip) - - // Generalization: domain randomization (per-epoch jitter on sim params) - enable_domain_randomization: true, // Default: enabled (prevents memorization of fixed sim params) // Generalization: periodic shrink-and-perturb (kills memorized weights) shrink_perturb_interval: 20, // Every 20 epochs (0=disabled) shrink_perturb_alpha: 0.85, // Keep 85% of weights, reinit 15% @@ -1617,28 +1552,22 @@ impl DQNHyperparameters { beta_penalty_strength: 0.3, // 30% reward reduction at full correlation // Gems & Pearls: generalization techniques - enable_anti_lr: true, // #24: anti-intuitive LR (kick out of overfit minima) anti_lr_good_mult: 3.0, // 3x LR when Sharpe is good (destabilize overfit) anti_lr_bad_mult: 0.3, // 0.3x LR when Sharpe is bad (stabilize) anti_lr_sharpe_threshold: 0.3, // Sharpe threshold for switching ensemble_disagreement_penalty: 0.3, // #27: penalize Q-targets by ensemble std - enable_causal_intervention: true, // #34: causal feature sensitivity (one production path) causal_weight: 0.1, // #34: mild causal regularization causal_intervention_interval: 10, // #34: every 10th training step - enable_adversarial_self_play: true, // #33: GPU-native saboteur (one production path) adversarial_warmup_epochs: 50, // #33: 50 epochs warmup before self-play adversarial_checkpoint_interval: 50, // #33: update saboteur reference every 50 epochs feature_mask_fraction: 0.3, // #23: mask 30% of features each epoch feature_noise_scale: 0.1, // #22: add N(0, 0.1*std) noise to features - enable_vol_normalization: true, // #13: divide returns by realized vol time_reversal_mod: 5, // #11: 20% of episodes played backwards regret_blend: 0.3, // #17: 30% regret + 70% raw PnL - enable_mirror_universe: true, // #10: alternate mirrored/normal epochs position_entropy_weight: 0.01, // #19: reward += 0.01 * H(position_histogram) trade_clustering_penalty: 0.05, // #25: penalize temporally clustered trades pruning_epoch: 50, // #20: compute mask at epoch 50 pruning_fraction: 0.7, // #20: prune 70% of smallest weights - enable_gradient_vaccine: true, // #32: mathematical overfit guarantee (one production path) bottleneck_dim: 2, // #31: 2D information compression (gem of gems) stochastic_depth_prob: 0.2, // #21: 20% drop probability per hidden layer mixup_alpha: 0.2, // Manifold Mixup: Beta(0.2,0.2) target distribution mixing @@ -1657,10 +1586,7 @@ impl DQNHyperparameters { causal_intensity: 1.0, asymmetric_dd_weight: 0.5, // #18: extra loss on Q-overestimation in drawdown - // Wave 16 Portfolio Features (default: ALL ENABLED) - enable_action_masking: true, // Default: action masking enabled - enable_entropy_regularization: true, // Default: entropy regularization enabled - enable_stress_testing: true, // Default: stress testing enabled + // Wave 16 Portfolio Features max_position_absolute: 2.0, // Default: ±2.0 position limit (matches production) // WAVE 17: Hyperopt-tuned parameters @@ -1670,7 +1596,6 @@ impl DQNHyperparameters { transaction_cost_multiplier: 1.0, // Triple Barrier defaults - enable_triple_barrier: true, triple_barrier_profit_target_bps: 100, triple_barrier_stop_loss_bps: 50, triple_barrier_max_holding_seconds: 3600, @@ -1735,7 +1660,6 @@ impl DQNHyperparameters { sharpe_window: 20, // Default: 20-step rolling window // P1.6: Adaptive Dropout - enable_dropout_scheduler: true, // Default: disabled (enable for overfitting prevention) dropout_initial: 0.5, // Default: 50% initial dropout dropout_final: 0.1, // Default: 10% final dropout dropout_anneal_steps: 10000, // Default: 10K steps for annealing @@ -1745,11 +1669,9 @@ impl DQNHyperparameters { her_strategy: "future".to_owned(), // Default: future strategy (better than final) // P1.9: Generalized Advantage Estimation - enable_gae: true, // Default: disabled (enable for lower variance) gae_lambda: 0.95, // Default: 0.95 (standard PPO/A2C value) // P1.11: Noisy Sigma Scheduling - enable_noisy_sigma_scheduler: true, // Enabled by default for wider exploration noisy_sigma_initial: 0.8, // Higher initial noise for 81-action exploration noisy_sigma_final: 0.4, // Default: 40% final noise noisy_sigma_anneal_steps: 10000, // Default: 10K steps for annealing @@ -1810,8 +1732,7 @@ impl DQNHyperparameters { branch_hidden_dim: 128, - // GPU walk-forward: disabled by default (single-pass training) - enable_gpu_walk_forward: true, + // GPU walk-forward (always active) wf_initial_train_fraction: 0.5, wf_val_fraction: 0.125, wf_test_fraction: 0.125, diff --git a/crates/ml/src/trainers/dqn/data_loading.rs b/crates/ml/src/trainers/dqn/data_loading.rs index 407e8c609..6f0628ff4 100644 --- a/crates/ml/src/trainers/dqn/data_loading.rs +++ b/crates/ml/src/trainers/dqn/data_loading.rs @@ -277,7 +277,8 @@ impl DQNTrainer { debug!("Bars sorted successfully"); // Wave 14 Agent 32: Preprocess close prices for stationarity - let preprocessed_closes = if self.hyperparams.enable_preprocessing { + // Preprocessing is always active (log returns + windowed normalization + outlier clipping) + let preprocessed_closes = { info!("🔬 Preprocessing enabled: Applying log returns + windowed normalization + outlier clipping"); // Extract close prices (CPU-side preprocessing) @@ -352,9 +353,6 @@ impl DQNTrainer { ); Some(preprocessed_f64) - } else { - info!("⚠️ Preprocessing disabled: Using raw close prices (NON-STATIONARY)"); - None }; // WAVE 2-A2: Load MBP-10 snapshots for OFI calculation (parallel) diff --git a/crates/ml/src/trainers/dqn/fused_training.rs b/crates/ml/src/trainers/dqn/fused_training.rs index ab9a8b0af..cc7b84046 100644 --- a/crates/ml/src/trainers/dqn/fused_training.rs +++ b/crates/ml/src/trainers/dqn/fused_training.rs @@ -216,10 +216,10 @@ impl FusedTrainingCtx { stochastic_depth_prob: hyperparams.stochastic_depth_prob as f32, pruning_epoch: hyperparams.pruning_epoch, pruning_fraction: hyperparams.pruning_fraction as f32, - enable_causal_intervention: hyperparams.enable_causal_intervention, + enable_causal_intervention: true, causal_weight: hyperparams.causal_weight, causal_intervention_interval: hyperparams.causal_intervention_interval, - enable_gradient_vaccine: hyperparams.enable_gradient_vaccine, + enable_gradient_vaccine: true, bottleneck_dim: hyperparams.bottleneck_dim, market_dim: 42, // Always 42 base market features — OFI features bypass bottleneck via portfolio_dim }; diff --git a/crates/ml/src/trainers/dqn/risk.rs b/crates/ml/src/trainers/dqn/risk.rs index 503bac2b4..e7aa5a956 100644 --- a/crates/ml/src/trainers/dqn/risk.rs +++ b/crates/ml/src/trainers/dqn/risk.rs @@ -17,7 +17,7 @@ impl DQNTrainer { /// - High volatility (>5%): increase epsilon (explore more) /// - Moderate volatility: linear interpolation pub(crate) fn calculate_volatility_adjusted_epsilon(&self, base_epsilon: f64) -> f64 { - if !self.hyperparams.enable_volatility_epsilon || self.volatility_returns.len() < 10 { + if self.volatility_returns.len() < 10 { return base_epsilon; } @@ -54,10 +54,6 @@ impl DQNTrainer { /// consistency over large but volatile returns. Uses trade_stats_history /// step_returns from GPU portfolio states. pub(crate) fn calculate_risk_adjusted_reward(&self, raw_reward: f64) -> f64 { - if !self.hyperparams.enable_risk_adjusted_rewards { - return raw_reward; - } - // Use step_returns from the most recent epoch's trade stats let recent_returns: Vec = self.trade_stats_history.iter() .rev() @@ -86,10 +82,6 @@ impl DQNTrainer { /// Returns Kelly criterion position size (0.0-0.25) based on trade history. /// Requires minimum trade history for statistical significance. pub fn get_kelly_fraction(&self) -> f64 { - if !self.hyperparams.enable_kelly_sizing { - return 1.0; // Full position sizing (disabled) - } - let kelly_opt = match &self.kelly_optimizer { Some(opt) => opt, None => return 1.0, diff --git a/crates/ml/src/trainers/dqn/smoke_tests/helpers.rs b/crates/ml/src/trainers/dqn/smoke_tests/helpers.rs index 04dfdf4be..d29bfcc62 100644 --- a/crates/ml/src/trainers/dqn/smoke_tests/helpers.rs +++ b/crates/ml/src/trainers/dqn/smoke_tests/helpers.rs @@ -32,9 +32,6 @@ pub(super) fn smoke_params() -> DQNHyperparameters { profile.apply_to(&mut p); // ── Test-only flags (not supported by TOML profile yet) ── - p.enable_stress_testing = false; - p.enable_compliance = false; - p.enable_regime_qnetwork = false; p.replay_buffer_vram_fraction = 0.0; // Explicit buffer_size from TOML p.checkpoint_frequency = 100; diff --git a/crates/ml/src/trainers/dqn/trainer/constructor.rs b/crates/ml/src/trainers/dqn/trainer/constructor.rs index ccd447151..8a9714f57 100644 --- a/crates/ml/src/trainers/dqn/trainer/constructor.rs +++ b/crates/ml/src/trainers/dqn/trainer/constructor.rs @@ -16,7 +16,7 @@ use tracing::info; use crate::dqn::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig}; use crate::dqn::curiosity::CuriosityModule; -use crate::dqn::dqn::{DQN, DQNConfig}; +use crate::dqn::dqn::DQNConfig; use crate::dqn::logging::{LoggingConfig, MetricsAggregator}; use crate::dqn::portfolio_tracker::PortfolioTracker; use crate::dqn::regime_conditional::RegimeConditionalDQN; @@ -316,7 +316,7 @@ impl DQNTrainer { #[allow(clippy::cast_possible_truncation)] minimum_profit_factor: hyperparams.minimum_profit_factor as f32, weight_decay: hyperparams.weight_decay, - dropout_rate: if hyperparams.enable_dropout_scheduler { hyperparams.dropout_initial } else { 0.0 }, + dropout_rate: hyperparams.dropout_initial, entropy_coefficient: hyperparams.entropy_coefficient, noisy_epsilon_floor: hyperparams.noisy_epsilon_floor.unwrap_or(0.0) as f32, // C2: NoisyNet handles exploration count_bonus_coefficient: hyperparams.count_bonus_coefficient.unwrap_or(0.0), @@ -339,19 +339,13 @@ impl DQNTrainer { } else { device.clone() }; - let agent = if hyperparams.enable_regime_qnetwork { - info!("Creating regime-conditional DQN with 3 heads (Trending, Ranging, Volatile)"); - info!(" - Regime detection: ADX (raw index 40) + CUSUM direction (raw index 41)"); - info!(" - Classification: Trending (ADX>0.25), Volatile (ADX≤0.25 & |CUSUM|>0.7), Ranging (otherwise)"); - let regime_agent = RegimeConditionalDQN::new_on_device(config, agent_device.clone()) - .map_err(|e| anyhow::anyhow!("Failed to create regime-conditional DQN: {}", e))?; - DQNAgentType::RegimeConditional(regime_agent) - } else { - info!("Creating standard DQN with single Q-network head"); - let standard_agent = DQN::new_on_device(config, agent_device) - .map_err(|e| anyhow::anyhow!("Failed to create DQN agent: {}", e))?; - DQNAgentType::Standard(standard_agent) - }; + // Regime-conditional Q-network is always active + info!("Creating regime-conditional DQN with 3 heads (Trending, Ranging, Volatile)"); + info!(" - Regime detection: ADX (raw index 40) + CUSUM direction (raw index 41)"); + info!(" - Classification: Trending (ADX>0.25), Volatile (ADX≤0.25 & |CUSUM|>0.7), Ranging (otherwise)"); + let regime_agent = RegimeConditionalDQN::new_on_device(config, agent_device.clone()) + .map_err(|e| anyhow::anyhow!("Failed to create regime-conditional DQN: {}", e))?; + let agent = DQNAgentType::RegimeConditional(regime_agent); // Initialize portfolio tracker with $100k starting capital and 1 basis point spread @@ -386,8 +380,8 @@ impl DQNTrainer { let triple_barrier = Arc::new(RwLock::new(TripleBarrierEngine::new(1000))); info!("Triple barrier engine initialized with 1000 max trackers"); - // WAVE 16S: Initialize Kelly optimizer if enabled - let kelly_optimizer = if hyperparams.enable_kelly_sizing { + // WAVE 16S: Initialize Kelly optimizer (always active) + let kelly_optimizer = { use crate::risk::kelly_optimizer::{KellyCriterionOptimizer, KellyOptimizerConfig}; let kelly_config = KellyOptimizerConfig { max_fraction: hyperparams.kelly_max_fraction, @@ -402,16 +396,14 @@ impl DQNTrainer { info!("Kelly optimizer enabled (fractional={}, max={})", hyperparams.kelly_fractional, hyperparams.kelly_max_fraction); Some(Arc::new(optimizer)) - } else { - None }; - // Wave 16 Portfolio Features: Initialize action masking, entropy regularization, and stress testing - let enable_action_masking = hyperparams.enable_action_masking; + // Wave 16 Portfolio Features: action masking and entropy regularization always active + let enable_action_masking = true; let max_position = hyperparams.max_position_absolute; // BLOCKER #2: Use hyperopt-tunable position limit // Entropy regularization: SAC-style computed directly on Q-values in DQN::compute_loss_internal - if hyperparams.enable_entropy_regularization { + { let coeff = hyperparams.entropy_coefficient; info!("Entropy regularization enabled (coefficient={coeff:.4}, applied to Q-value softmax in loss)"); } @@ -431,14 +423,10 @@ impl DQNTrainer { // to resolve the circular dependency (DQNStressTester needs a DQNTrainer). let stress_tester: Option> = None; - if enable_action_masking { - info!( - "Action masking enabled (max_position=±{:.1}, 30-50% filtering expected)", - max_position - ); - } else { - info!("Action masking disabled (all 5 exposure levels available)"); - } + info!( + "Action masking enabled (max_position=±{:.1}, 30-50% filtering expected)", + max_position + ); // Wave 16 Core Risk Features: Initialize drawdown monitor, position limiter, circuit breaker // These are ALWAYS enabled by default for production safety @@ -487,17 +475,17 @@ impl DQNTrainer { let sharpe_weight = hyperparams.sharpe_weight; let sharpe_window = hyperparams.sharpe_window; - // P1.6: Adaptive dropout scheduler - let dropout_scheduler = hyperparams.enable_dropout_scheduler.then(|| { + // P1.6: Adaptive dropout scheduler (always active) + let dropout_scheduler = { use crate::dqn::network::DropoutScheduler; info!("Dropout scheduler enabled (initial={}, final={}, steps={})", hyperparams.dropout_initial, hyperparams.dropout_final, hyperparams.dropout_anneal_steps); - DropoutScheduler::new( + Some(DropoutScheduler::new( hyperparams.dropout_initial, hyperparams.dropout_final, hyperparams.dropout_anneal_steps, - ) - }); + )) + }; // P1.7: Hindsight Experience Replay (HER) let her_buffer = (hyperparams.her_ratio > 0.0) @@ -527,25 +515,25 @@ impl DQNTrainer { }) .transpose()?; - // P1.9: Generalized Advantage Estimation (GAE) - let gae_calculator = hyperparams.enable_gae.then(|| { + // P1.9: Generalized Advantage Estimation (GAE) — always active + let gae_calculator = { use crate::dqn::gae::GAECalculator; info!("GAE calculator enabled (lambda={}, gamma={})", hyperparams.gae_lambda, hyperparams.gamma); - GAECalculator::new(hyperparams.gae_lambda, hyperparams.gamma) - }); + Some(GAECalculator::new(hyperparams.gae_lambda, hyperparams.gamma)) + }; - // P1.11: Noisy network sigma scheduling - let noisy_sigma_scheduler = hyperparams.enable_noisy_sigma_scheduler.then(|| { + // P1.11: Noisy network sigma scheduling — always active + let noisy_sigma_scheduler = { use crate::dqn::noisy_sigma_scheduler::NoisySigmaScheduler; info!("Noisy sigma scheduler enabled (initial={}, final={}, steps={})", hyperparams.noisy_sigma_initial, hyperparams.noisy_sigma_final, hyperparams.noisy_sigma_anneal_steps); - NoisySigmaScheduler::new( + Some(NoisySigmaScheduler::new( hyperparams.noisy_sigma_initial, hyperparams.noisy_sigma_final, hyperparams.noisy_sigma_anneal_steps, - ) - }); + )) + }; // WAVE 26 P1.8: Initialize curiosity module if curiosity_weight > 0 // Must be created BEFORE hyperparams and device are moved @@ -599,8 +587,8 @@ impl DQNTrainer { info!("Multi-GPU: {} devices detected, data parallelism enabled", mg.world_size); } - // #33 Extract saboteur config before hyperparams is moved - let sab_enabled = hyperparams.enable_adversarial_self_play; + // #33 Extract saboteur config before hyperparams is moved (always active) + let sab_enabled = true; let sab_warmup = hyperparams.adversarial_warmup_epochs; let sab_interval = hyperparams.adversarial_checkpoint_interval; diff --git a/crates/ml/src/trainers/dqn/trainer/mod.rs b/crates/ml/src/trainers/dqn/trainer/mod.rs index 64f80ff6d..0aa1468d6 100644 --- a/crates/ml/src/trainers/dqn/trainer/mod.rs +++ b/crates/ml/src/trainers/dqn/trainer/mod.rs @@ -369,20 +369,11 @@ impl DQNTrainer { /// inner trainer (with stress testing itself disabled to avoid recursion) and /// hands it to `DQNStressTester::new()`. /// - /// No-op if `hyperparams.enable_stress_testing` is `false`. + /// Initialize the stress tester (always active). pub fn init_stress_tester(&mut self) -> Result<()> { - if !self.hyperparams.enable_stress_testing { - return Ok(()); - } - - // Clone hyperparams with stress testing disabled so the inner trainer - // does not recursively try to initialise its own stress tester. - // Also disable GPU-heavy features that the stress tester doesn't need — - // otherwise we double the VRAM usage (3 extra regime heads, 3 extra GPU PER buffers, - // experience collector, etc.) which causes OOM on 4GB GPUs. + // Clone hyperparams with minimal buffer for the inner trainer. + // The inner trainer is lightweight to avoid doubling VRAM usage. let mut inner_hp = self.hyperparams.clone(); - inner_hp.enable_stress_testing = false; - inner_hp.enable_regime_qnetwork = false; inner_hp.buffer_size = 1; let inner_trainer = Self::new_with_device(inner_hp, self.device.clone()) @@ -445,8 +436,8 @@ impl DQNTrainer { val_data.len() ); - // GPU walk-forward: upload ALL data to GPU, run expanding-window folds - if self.hyperparams.enable_gpu_walk_forward && self.cuda_stream.is_some() { + // GPU walk-forward: upload ALL data to GPU, run expanding-window folds (always active when CUDA available) + if self.cuda_stream.is_some() { // Merge train+val into a single dataset for walk-forward splitting let mut all_data = training_data; all_data.extend(val_data); diff --git a/crates/ml/src/training_profile.rs b/crates/ml/src/training_profile.rs index 5fa142788..5bd318ee7 100644 --- a/crates/ml/src/training_profile.rs +++ b/crates/ml/src/training_profile.rs @@ -155,34 +155,27 @@ pub struct AdvancedSection { /// Gems & Pearls: generalization techniques to close IS/OOS gap. #[derive(Debug, Clone, Deserialize, Default)] pub struct GeneralizationSection { - pub enable_domain_randomization: Option, pub shrink_perturb_interval: Option, pub shrink_perturb_alpha: Option, pub shrink_perturb_sigma: Option, pub adversarial_dd_threshold: Option, pub adversarial_epochs_trigger: Option, - pub enable_anti_lr: Option, pub anti_lr_good_mult: Option, pub anti_lr_bad_mult: Option, pub anti_lr_sharpe_threshold: Option, pub feature_mask_fraction: Option, pub feature_noise_scale: Option, - pub enable_vol_normalization: Option, pub pruning_epoch: Option, pub pruning_fraction: Option, - pub enable_causal_intervention: Option, pub causal_weight: Option, pub causal_intervention_interval: Option, - pub enable_adversarial_self_play: Option, pub adversarial_warmup_epochs: Option, pub adversarial_checkpoint_interval: Option, - pub enable_gradient_vaccine: Option, pub bottleneck_dim: Option, pub stochastic_depth_prob: Option, pub asymmetric_dd_weight: Option, pub time_reversal_mod: Option, pub regret_blend: Option, - pub enable_mirror_universe: Option, pub position_entropy_weight: Option, pub trade_clustering_penalty: Option, pub ensemble_disagreement_penalty: Option, @@ -205,14 +198,11 @@ pub struct GeneralizationSection { /// Risk management and position-control parameters. #[derive(Debug, Clone, Deserialize, Default)] pub struct RiskSection { - pub enable_kelly_sizing: Option, pub kelly_fractional: Option, pub kelly_max_fraction: Option, - pub enable_action_masking: Option, pub max_position_absolute: Option, /// Alias for `max_position_absolute` — used when TOML key is `max_position`. pub max_position: Option, - pub enable_circuit_breaker: Option, /// Minimum Q-value clamp in GPU kernel (default -200.0) pub q_clip_min: Option, /// Maximum Q-value clamp in GPU kernel (default 200.0) @@ -839,34 +829,27 @@ impl DqnTrainingProfile { // [generalization] if let Some(ref g) = self.generalization { - if let Some(v) = g.enable_domain_randomization { hp.enable_domain_randomization = v; } if let Some(v) = g.shrink_perturb_interval { hp.shrink_perturb_interval = v; } if let Some(v) = g.shrink_perturb_alpha { hp.shrink_perturb_alpha = v; } if let Some(v) = g.shrink_perturb_sigma { hp.shrink_perturb_sigma = v; } if let Some(v) = g.adversarial_dd_threshold { hp.adversarial_dd_threshold = v; } if let Some(v) = g.adversarial_epochs_trigger { hp.adversarial_epochs_trigger = v; } - if let Some(v) = g.enable_anti_lr { hp.enable_anti_lr = v; } if let Some(v) = g.anti_lr_good_mult { hp.anti_lr_good_mult = v; } if let Some(v) = g.anti_lr_bad_mult { hp.anti_lr_bad_mult = v; } if let Some(v) = g.anti_lr_sharpe_threshold { hp.anti_lr_sharpe_threshold = v; } if let Some(v) = g.feature_mask_fraction { hp.feature_mask_fraction = v; } if let Some(v) = g.feature_noise_scale { hp.feature_noise_scale = v; } - if let Some(v) = g.enable_vol_normalization { hp.enable_vol_normalization = v; } if let Some(v) = g.pruning_epoch { hp.pruning_epoch = v; } if let Some(v) = g.pruning_fraction { hp.pruning_fraction = v; } - if let Some(v) = g.enable_causal_intervention { hp.enable_causal_intervention = v; } if let Some(v) = g.causal_weight { hp.causal_weight = v; } if let Some(v) = g.causal_intervention_interval { hp.causal_intervention_interval = v; } - if let Some(v) = g.enable_adversarial_self_play { hp.enable_adversarial_self_play = v; } if let Some(v) = g.adversarial_warmup_epochs { hp.adversarial_warmup_epochs = v; } if let Some(v) = g.adversarial_checkpoint_interval { hp.adversarial_checkpoint_interval = v; } - if let Some(v) = g.enable_gradient_vaccine { hp.enable_gradient_vaccine = v; } if let Some(v) = g.bottleneck_dim { hp.bottleneck_dim = v; } if let Some(v) = g.stochastic_depth_prob { hp.stochastic_depth_prob = v; } if let Some(v) = g.asymmetric_dd_weight { hp.asymmetric_dd_weight = v; } if let Some(v) = g.time_reversal_mod { hp.time_reversal_mod = v; } if let Some(v) = g.regret_blend { hp.regret_blend = v; } - if let Some(v) = g.enable_mirror_universe { hp.enable_mirror_universe = v; } if let Some(v) = g.position_entropy_weight { hp.position_entropy_weight = v; } if let Some(v) = g.trade_clustering_penalty { hp.trade_clustering_penalty = v; } if let Some(v) = g.ensemble_disagreement_penalty { hp.ensemble_disagreement_penalty = v; } @@ -888,18 +871,12 @@ impl DqnTrainingProfile { // [risk] if let Some(ref r) = self.risk { - if let Some(v) = r.enable_kelly_sizing { - hp.enable_kelly_sizing = v; - } if let Some(v) = r.kelly_fractional { hp.kelly_fractional = v; } if let Some(v) = r.kelly_max_fraction { hp.kelly_max_fraction = v; } - if let Some(v) = r.enable_action_masking { - hp.enable_action_masking = v; - } if let Some(v) = r.max_position_absolute { hp.max_position_absolute = v; } @@ -907,9 +884,6 @@ impl DqnTrainingProfile { if let Some(v) = r.max_position { hp.max_position_absolute = v; } - if let Some(v) = r.enable_circuit_breaker { - hp.enable_circuit_breaker = v; - } if let Some(v) = r.q_clip_min { hp.q_clip_min = v; } @@ -1371,8 +1345,6 @@ mod tests { // [risk] section — new loss_aversion field assert!((hp.loss_aversion - 1.5).abs() < 0.01); - assert!(hp.enable_kelly_sizing); - assert!(hp.enable_circuit_breaker); // [reward] section — all composite weights assert!((hp.w_dsr - 1.0).abs() < 0.01); diff --git a/crates/ml/tests/dqn_trainer_p1_tests.rs b/crates/ml/tests/dqn_trainer_p1_tests.rs index 33630ee01..a1b161d9f 100644 --- a/crates/ml/tests/dqn_trainer_p1_tests.rs +++ b/crates/ml/tests/dqn_trainer_p1_tests.rs @@ -92,16 +92,16 @@ fn test_p1_features_initialization() { // Enable all P1 features hyperparams.sharpe_weight = 0.3; hyperparams.sharpe_window = 20; - hyperparams.enable_dropout_scheduler = true; + // Dropout scheduler is always active hyperparams.dropout_initial = 0.5; hyperparams.dropout_final = 0.1; hyperparams.dropout_anneal_steps = 10000; hyperparams.her_ratio = 0.5; hyperparams.her_strategy = "future".to_string(); hyperparams.curiosity_weight = 0.1; - hyperparams.enable_gae = true; + // GAE is always active hyperparams.gae_lambda = 0.95; - hyperparams.enable_noisy_sigma_scheduler = true; + // Noisy sigma scheduler is always active hyperparams.noisy_sigma_initial = 0.6; hyperparams.noisy_sigma_final = 0.4; hyperparams.noisy_sigma_anneal_steps = 10000; @@ -123,7 +123,7 @@ fn test_p1_3_sharpe_reward_disabled_by_default() { fn test_p1_6_dropout_scheduler_disabled_by_default() { // Test that dropout scheduler is disabled by default let hyperparams = DQNHyperparameters::conservative(); - assert_eq!(hyperparams.enable_dropout_scheduler, false, "Dropout scheduler should be disabled by default"); + // Dropout scheduler is always active (no field to check) } #[test] @@ -145,7 +145,7 @@ fn test_p1_8_curiosity_disabled_by_default() { fn test_p1_9_gae_disabled_by_default() { // Test that GAE is disabled by default let hyperparams = DQNHyperparameters::conservative(); - assert_eq!(hyperparams.enable_gae, false, "GAE should be disabled by default"); + // GAE is always active (no field to check) assert_eq!(hyperparams.gae_lambda, 0.95, "GAE lambda should be 0.95 by default"); } @@ -153,7 +153,7 @@ fn test_p1_9_gae_disabled_by_default() { fn test_p1_11_noisy_sigma_scheduler_disabled_by_default() { // Test that noisy sigma scheduler is disabled by default let hyperparams = DQNHyperparameters::conservative(); - assert_eq!(hyperparams.enable_noisy_sigma_scheduler, false, "Noisy sigma scheduler should be disabled by default"); + // Noisy sigma scheduler is always active (no field to check) } #[test] @@ -163,7 +163,7 @@ fn test_p1_features_with_partial_enablement() { // Enable only Sharpe reward and GAE hyperparams.sharpe_weight = 0.4; - hyperparams.enable_gae = true; + // GAE is always active let result = DQNTrainer::new(hyperparams); assert!(result.is_ok(), "Failed to create DQNTrainer with partial P1 features: {:?}", result.err()); @@ -215,7 +215,7 @@ fn test_p1_gae_lambda_bounds() { for lambda in test_lambdas { let mut hyperparams = DQNHyperparameters::conservative(); - hyperparams.enable_gae = true; + // GAE is always active hyperparams.gae_lambda = lambda; let result = DQNTrainer::new(hyperparams); @@ -227,7 +227,7 @@ fn test_p1_gae_lambda_bounds() { fn test_p1_dropout_scheduler_parameters() { // Test that dropout scheduler parameters are validated let mut hyperparams = DQNHyperparameters::conservative(); - hyperparams.enable_dropout_scheduler = true; + // Dropout scheduler is always active hyperparams.dropout_initial = 0.5; hyperparams.dropout_final = 0.1; hyperparams.dropout_anneal_steps = 10000; @@ -240,7 +240,7 @@ fn test_p1_dropout_scheduler_parameters() { fn test_p1_noisy_sigma_scheduler_parameters() { // Test that noisy sigma scheduler parameters are validated let mut hyperparams = DQNHyperparameters::conservative(); - hyperparams.enable_noisy_sigma_scheduler = true; + // Noisy sigma scheduler is always active hyperparams.noisy_sigma_initial = 0.6; hyperparams.noisy_sigma_final = 0.4; hyperparams.noisy_sigma_anneal_steps = 10000; @@ -257,16 +257,16 @@ fn test_p1_all_features_enabled_max_configuration() { // Max P1 configuration hyperparams.sharpe_weight = 0.5; hyperparams.sharpe_window = 50; - hyperparams.enable_dropout_scheduler = true; + // Dropout scheduler is always active hyperparams.dropout_initial = 0.7; hyperparams.dropout_final = 0.05; hyperparams.dropout_anneal_steps = 20000; hyperparams.her_ratio = 0.8; hyperparams.her_strategy = "final".to_string(); hyperparams.curiosity_weight = 0.5; - hyperparams.enable_gae = true; + // GAE is always active hyperparams.gae_lambda = 0.99; - hyperparams.enable_noisy_sigma_scheduler = true; + // Noisy sigma scheduler is always active hyperparams.noisy_sigma_initial = 0.8; hyperparams.noisy_sigma_final = 0.2; hyperparams.noisy_sigma_anneal_steps = 20000; diff --git a/crates/ml/tests/hyperopt_action_masking_test.rs b/crates/ml/tests/hyperopt_action_masking_test.rs index 637b2ec7e..8504092c8 100644 --- a/crates/ml/tests/hyperopt_action_masking_test.rs +++ b/crates/ml/tests/hyperopt_action_masking_test.rs @@ -156,15 +156,13 @@ fn test_hyperopt_position_limit_range_validation() { fn test_hyperopt_action_masking_integration() { // This test documents how max_position_absolute interacts with action masking - // Tight limit scenario (1.0 contract) + // Tight limit scenario (1.0 contract) — action masking is always active let params_tight = DQNHyperparameters { max_position_absolute: 1.0, - enable_action_masking: true, ..DQNHyperparameters::conservative() }; assert_eq!(params_tight.max_position_absolute, 1.0); - assert!(params_tight.enable_action_masking); // Expected behavior: 60-70% action filtering with tight limit // This is tested in integration tests, not unit tests @@ -172,12 +170,10 @@ fn test_hyperopt_action_masking_integration() { // Loose limit scenario (10.0 contracts) let params_loose = DQNHyperparameters { max_position_absolute: 10.0, - enable_action_masking: true, ..DQNHyperparameters::conservative() }; assert_eq!(params_loose.max_position_absolute, 10.0); - assert!(params_loose.enable_action_masking); // Expected behavior: 5-10% action filtering with loose limit } @@ -194,11 +190,7 @@ fn test_hyperopt_backward_compatibility() { "Default should match current hardcoded value for backward compatibility" ); - // Action masking should still be enabled by default - assert!( - params.enable_action_masking, - "Action masking should be enabled by default" - ); + // Action masking is always active (no field to check) } // Note: Hyperopt-specific tests (suggest_float, trial creation) are integration tests diff --git a/crates/ml/tests/wave16q_ohlcv_mutation_test.rs b/crates/ml/tests/wave16q_ohlcv_mutation_test.rs index 3259cd7c4..0015ef467 100644 --- a/crates/ml/tests/wave16q_ohlcv_mutation_test.rs +++ b/crates/ml/tests/wave16q_ohlcv_mutation_test.rs @@ -93,9 +93,8 @@ use tracing::{info, warn}; #[tokio::test] async fn test_ohlcv_raw_prices_preserved_after_preprocessing() -> Result<()> { - // Initialize trainer with preprocessing enabled + // Initialize trainer (preprocessing is always active) let mut hyperparams = DQNHyperparameters::conservative(); - hyperparams.enable_preprocessing = true; hyperparams.preprocessing_window = 50; hyperparams.preprocessing_clip_sigma = 3.0; hyperparams.epochs = 1; // Single epoch test @@ -181,7 +180,6 @@ async fn test_ohlcv_raw_prices_preserved_after_preprocessing() -> Result<()> { async fn test_target_2_is_raw_price_not_zscore() -> Result<()> { // Regression test: Verify target[2] contains raw dollars, not preprocessed z-scores let mut hyperparams = DQNHyperparameters::conservative(); - hyperparams.enable_preprocessing = true; // Enable preprocessing to trigger bug hyperparams.epochs = 1; let mut trainer = DQNTrainer::new(hyperparams)?; diff --git a/docs/superpowers/plans/2026-03-31-imbalance-bars-and-cleanup.md b/docs/superpowers/plans/2026-03-31-imbalance-bars-and-cleanup.md new file mode 100644 index 000000000..4306d8e45 --- /dev/null +++ b/docs/superpowers/plans/2026-03-31-imbalance-bars-and-cleanup.md @@ -0,0 +1,397 @@ +# Imbalance Bars Default + Feature Flag Cleanup Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make information-driven imbalance bars (López de Prado) the default data source for DQN training when MBP-10 data is available, remove 8 enable_* boolean flags from FeatureConfig, and make min_hold_bars configurable for A/B testing. + +**Architecture:** Three independent subsystems: (1) Wire MBP-10 → ImbalanceBarSampler → existing feature pipeline in the DQN data loader, falling back to OHLCV when MBP-10 is absent. (2) Remove all enable_* flags from ml-features FeatureConfig — all feature groups always active. (3) Make min_hold_bars a TOML-configurable + CLI-overridable parameter. + +**Tech Stack:** Rust (crates/ml-features, crates/ml, crates/ml-dqn), Databento DBN decoder, CUDA kernels unchanged + +--- + +## File Structure + +| File | Responsibility | Changes | +|------|---------------|---------| +| `crates/ml/src/trainers/dqn/data_loading.rs` | DQN training data pipeline | Add MBP-10 → imbalance bars path | +| `crates/ml-features/src/config.rs` | Feature group configuration | Remove 8 enable_* flags | +| `crates/ml-features/src/pipeline.rs` | Feature extraction pipeline | Remove conditional feature gating | +| `crates/ml-features/src/mbp10_loader.rs` | MBP-10 file loading | Already exists, may need tick extraction | +| `crates/ml-features/src/alternative_bars.rs` | ImbalanceBarSampler | Already exists, production-ready | +| `crates/ml/src/trainers/dqn/config.rs` | DQN hyperparameters | Add `imbalance_bar_threshold`, `min_hold_bars` | +| `crates/ml/src/training_profile.rs` | TOML deserialization | Wire new params | +| `crates/ml/examples/train_baseline_rl.rs` | Training CLI | Add `--min-hold-bars` | +| `crates/ml/examples/hyperopt_baseline_rl.rs` | Hyperopt CLI | Add `--min-hold-bars` | +| `config/training/dqn-production.toml` | Production config | Add imbalance_bar_threshold, min_hold_bars | +| `infra/k8s/argo/compile-and-train-template.yaml` | Argo workflow | Wire --min-hold-bars | + +--- + +## Phase 1: Remove enable_* flags from FeatureConfig + +### Task 1: Remove 8 enable_* boolean fields from FeatureConfig + +The `crates/ml-features/src/config.rs` FeatureConfig struct has 8 `enable_*: bool` flags that violate the "no feature flags" rule. All feature groups should always be active. The conditional `if self.enable_X { count += N }` logic creates dead code paths and false impressions that features can be toggled. + +**Files:** +- Modify: `crates/ml-features/src/config.rs` — remove 8 fields, collapse conditionals +- Modify: `crates/ml-features/src/pipeline.rs` — remove `if config.enable_X` guards +- Modify: `crates/ml-features/src/lib.rs` — update re-exports if needed + +- [ ] **Step 1: Read the FeatureConfig struct and all its methods** + +Read `crates/ml-features/src/config.rs` fully. Identify: +- The 8 `enable_*: bool` fields (lines ~219-247) +- The `feature_count()` method that conditionally sums features +- The `feature_indices()` method that conditionally assigns index ranges +- The `is_enabled()` method on FeatureGroup enum +- The 4 constructor fns: `default()`, `wave_b()`, `wave_c()`, `wave_d()` +- All tests that check `enable_*` values + +- [ ] **Step 2: Remove the 8 boolean fields from the struct** + +Delete these fields from `FeatureConfig`: +``` +pub enable_ohlcv: bool, +pub enable_technical_indicators: bool, +pub enable_microstructure: bool, +pub enable_alternative_bars: bool, +pub enable_barrier_optimization: bool, +pub enable_fractional_diff: bool, +pub enable_regime_detection: bool, +pub enable_wave_d_regime: bool, +``` + +- [ ] **Step 3: Collapse feature_count() — always count all groups** + +Remove all `if self.enable_X { count += N }` conditionals. The total feature count is the sum of ALL groups unconditionally. + +- [ ] **Step 4: Collapse feature_indices() — always assign all ranges** + +Remove all `if self.enable_X { indices.X = Some(...) }` conditionals. All index ranges always assigned. + +- [ ] **Step 5: Collapse is_enabled() — always return true** + +The `is_enabled(group: FeatureGroup) -> bool` method should just return `true` for all groups, or be removed entirely. If callers exist, replace with `true`. + +- [ ] **Step 6: Simplify constructors** + +Remove all `enable_X: true/false` from `default()`, `wave_b()`, `wave_c()`, `wave_d()`. The phased wave system no longer makes sense when all features are always active — consider collapsing to a single constructor. + +- [ ] **Step 7: Fix pipeline.rs** + +In `crates/ml-features/src/pipeline.rs`, remove any `if config.is_enabled(FeatureGroup::X)` guards. All feature extraction paths always execute. + +- [ ] **Step 8: Fix all compilation errors** + +```bash +SQLX_OFFLINE=true cargo check -p ml-features -p ml 2>&1 | head -80 +``` + +Grep for remaining `enable_` references and fix them: +```bash +grep -rn 'enable_ohlcv\|enable_technical\|enable_microstructure\|enable_alternative\|enable_barrier\|enable_fractional\|enable_regime\|enable_wave_d' crates/ml-features/ crates/ml/ +``` + +- [ ] **Step 9: Update tests** + +Tests that assert `!config.enable_alternative_bars` or `config.enable_ohlcv` need to be deleted or rewritten. Tests that assert `feature_count()` need updated counts (now always the maximum). + +- [ ] **Step 10: Compile check and commit** + +```bash +cargo check --workspace 2>&1 | head -80 +``` + +```bash +git add -u crates/ml-features/ crates/ml/ +git commit -m "cleanup: remove 8 enable_* flags from FeatureConfig — all features always active" +``` + +--- + +## Phase 2: Wire MBP-10 → Imbalance Bars → DQN Pipeline + +### Task 2: Add MBP-10 → tick extraction function + +The `mbp10_loader.rs` loads `Mbp10Snapshot` records. We need a function that extracts trade ticks from MBP-10 snapshots (MBP-10 contains trades as actions with `action == Trade`). + +**Files:** +- Modify: `crates/ml-features/src/mbp10_loader.rs` — add `extract_trades_from_mbp10()` + +- [ ] **Step 1: Read mbp10_loader.rs to understand the Mbp10Snapshot struct** + +Understand what fields are available: timestamp, bid/ask prices and sizes at 10 levels, action type. + +- [ ] **Step 2: Add trade extraction function** + +```rust +/// Extract trade ticks from MBP-10 snapshots. +/// Filters for action == Trade, returns (price, volume, timestamp, is_buy). +pub fn extract_trades_from_mbp10( + snapshots: &[Mbp10Snapshot], +) -> Vec<(f64, f64, chrono::DateTime, bool)> { + // Filter for trade actions, extract price/volume/side + // is_buy determined by comparing trade price to mid-price +} +``` + +- [ ] **Step 3: Compile check** + +```bash +SQLX_OFFLINE=true cargo check -p ml-features 2>&1 | head -40 +``` + +- [ ] **Step 4: Commit** + +```bash +git add crates/ml-features/src/mbp10_loader.rs +git commit -m "feat: extract trade ticks from MBP-10 snapshots" +``` + +--- + +### Task 3: Add MBP-10 → imbalance bars pipeline function + +Create a function that takes an MBP-10 data directory and produces `Vec` via `ImbalanceBarSampler`. + +**Files:** +- Modify: `crates/ml-features/src/mbp10_loader.rs` — add `mbp10_to_imbalance_bars()` + +- [ ] **Step 1: Add the pipeline function** + +```rust +/// Load MBP-10 files from a directory and convert to imbalance bars. +/// +/// Pipeline: MBP-10 .dbn.zst → trade extraction → ImbalanceBarSampler → OHLCVBar +/// +/// Falls back to empty Vec if directory doesn't exist or has no files. +pub fn mbp10_to_imbalance_bars( + mbp10_dir: &Path, + symbol: &str, + threshold: f64, + ewma_alpha: f64, +) -> Result, MLError> { + // 1. Find all .dbn.zst files in mbp10_dir/symbol/ + // 2. Load each file with load_mbp10_snapshots_sync() + // 3. Extract trades from snapshots + // 4. Feed trades into ImbalanceBarSampler::new_with_ewma() + // 5. Collect emitted OHLCVBars + // 6. Sort by timestamp +} +``` + +- [ ] **Step 2: Add unit test with test_data/mbp10/** + +```rust +#[test] +fn test_mbp10_to_imbalance_bars() { + let bars = mbp10_to_imbalance_bars( + Path::new("test_data/mbp10"), + "ES.FUT", + 100.0, // threshold + 0.1, // ewma_alpha + ).unwrap(); + assert!(!bars.is_empty(), "Should produce imbalance bars from MBP-10 data"); + // Verify bars are time-ordered + for w in bars.windows(2) { + assert!(w[0].timestamp <= w[1].timestamp); + } +} +``` + +- [ ] **Step 3: Compile and test** + +```bash +SQLX_OFFLINE=true cargo test -p ml-features --lib -- mbp10_to_imbalance_bars +``` + +- [ ] **Step 4: Commit** + +```bash +git add crates/ml-features/src/mbp10_loader.rs +git commit -m "feat: MBP-10 → imbalance bars pipeline via ImbalanceBarSampler" +``` + +--- + +### Task 4: Wire imbalance bars into DQN data loader + +This is the critical integration. The DQN data loader currently only reads OHLCV `.dbn.zst` files. Add a path that tries MBP-10 first, falls back to OHLCV. + +**Files:** +- Modify: `crates/ml/src/trainers/dqn/data_loading.rs` — add MBP-10 loading path +- Modify: `crates/ml/src/trainers/dqn/config.rs` — add `imbalance_bar_threshold: f64` + +- [ ] **Step 1: Add hyperparameter** + +In `crates/ml/src/trainers/dqn/config.rs`, add to `DQNHyperparameters`: +```rust +/// Imbalance bar threshold for MBP-10 → bar conversion. +/// Higher = fewer bars (less noise), lower = more bars (more signal). +/// Only used when mbp10_data_dir is set. Default: 100.0. +pub imbalance_bar_threshold: f64, + +/// EWMA smoothing alpha for adaptive imbalance threshold. +/// 0.1 = slow adaptation, 0.5 = fast. Default: 0.1. +pub imbalance_bar_ewma_alpha: f64, +``` + +Default: `imbalance_bar_threshold: 100.0, imbalance_bar_ewma_alpha: 0.1,` + +- [ ] **Step 2: Modify data loading to try MBP-10 first** + +In `crates/ml/src/trainers/dqn/data_loading.rs`, find where OHLCV files are loaded (the `extract_ohlcv_bars_from_dbn` call). Before it, add: + +```rust +// Try MBP-10 → imbalance bars first (information-driven sampling) +let ohlcv_bars = if let Some(ref mbp10_dir) = self.hyperparams.mbp10_data_dir { + let mbp10_path = Path::new(mbp10_dir); + if mbp10_path.exists() { + info!("Loading MBP-10 data from {} → imbalance bars", mbp10_dir); + let bars = ml_features::mbp10_loader::mbp10_to_imbalance_bars( + mbp10_path, + &symbol, + self.hyperparams.imbalance_bar_threshold, + self.hyperparams.imbalance_bar_ewma_alpha, + )?; + if !bars.is_empty() { + info!("Produced {} imbalance bars from MBP-10 (vs ~{} 1-min OHLCV bars)", + bars.len(), bars.len() / 5); // rough estimate + bars + } else { + warn!("MBP-10 produced no bars, falling back to OHLCV"); + self.extract_ohlcv_bars_from_dbn(file_path)? + } + } else { + self.extract_ohlcv_bars_from_dbn(file_path)? + } +} else { + self.extract_ohlcv_bars_from_dbn(file_path)? +}; +``` + +IMPORTANT: Read the actual data loading flow first. The MBP-10 files are in a different directory (`mbp10_data_dir`) than the OHLCV files (`data_dir`). The function needs to scan the MBP-10 dir for files matching the symbol, not the OHLCV dir. + +- [ ] **Step 3: Wire TOML deserialization** + +In `crates/ml/src/training_profile.rs`, add to the relevant section: +```toml +imbalance_bar_threshold = 100.0 +imbalance_bar_ewma_alpha = 0.1 +``` + +- [ ] **Step 4: Add to TOML configs** + +`config/training/dqn-production.toml` under `[experience]`: +```toml +imbalance_bar_threshold = 100.0 +imbalance_bar_ewma_alpha = 0.1 +``` + +Same for `dqn-smoketest.toml` and `dqn-localdev.toml`. + +- [ ] **Step 5: Compile check** + +```bash +cargo check --workspace 2>&1 | head -80 +``` + +- [ ] **Step 6: Commit** + +```bash +git add -u crates/ml/ config/training/ +git commit -m "feat: wire MBP-10 → imbalance bars as default DQN data source" +``` + +--- + +### Task 5: Update Argo workflow to log bar type + +**Files:** +- Modify: `infra/k8s/argo/compile-and-train-template.yaml` — add logging for data source + +- [ ] **Step 1: Add data source logging to hyperopt and train steps** + +In the hyperopt and train-best shell scripts, add after the data directory listing: +```bash +echo "Bar type: imbalance (MBP-10) if available, OHLCV 1-min fallback" +``` + +- [ ] **Step 2: Commit** + +```bash +git add infra/k8s/argo/compile-and-train-template.yaml +git commit -m "docs: log bar type in Argo training workflow" +``` + +--- + +## Phase 3: Make min_hold_bars Configurable + +### Task 6: Wire min_hold_bars through CLI and TOML + +Currently `min_hold_bars` is hardcoded at 5 in the TOML and fixed in DQNHyperparameters. Make it overridable via CLI for A/B testing (3 vs 5). + +**Files:** +- Modify: `crates/ml/examples/train_baseline_rl.rs` — add `--min-hold-bars` CLI arg +- Modify: `crates/ml/examples/hyperopt_baseline_rl.rs` — add `--min-hold-bars` CLI arg +- Modify: `infra/k8s/argo/compile-and-train-template.yaml` — add workflow parameter + +- [ ] **Step 1: Add CLI arg to train_baseline_rl.rs** + +```rust +/// Minimum bars to hold a position before allowing exit (churn prevention). +/// Lower = more trades (faster cycling), higher = fewer trades (longer holds). +/// Default: 5 (from TOML). Set to 3 for aggressive OFI-driven trading. +#[arg(long)] +min_hold_bars: Option, +``` + +Wire: `if let Some(mhb) = args.min_hold_bars { hyperparams.min_hold_bars = mhb; }` + +- [ ] **Step 2: Add CLI arg to hyperopt_baseline_rl.rs** + +Same pattern as train_baseline_rl. + +- [ ] **Step 3: Add workflow parameter** + +In `compile-and-train-template.yaml`, add parameter: +```yaml +- name: min-hold-bars + value: "5" +``` + +Pass to hyperopt and train-best steps: +```bash +--min-hold-bars {{workflow.parameters.min-hold-bars}} +``` + +- [ ] **Step 4: Compile check** + +```bash +cargo check -p ml --example train_baseline_rl --example hyperopt_baseline_rl 2>&1 | head -40 +``` + +- [ ] **Step 5: Commit** + +```bash +git add crates/ml/examples/ infra/k8s/argo/compile-and-train-template.yaml +git commit -m "feat: configurable --min-hold-bars for A/B testing (3 vs 5)" +``` + +--- + +## Verification Checklist + +After all phases: + +- [ ] `cargo check --workspace` — full workspace compiles +- [ ] `grep -rn 'enable_ohlcv\|enable_technical\|enable_microstructure\|enable_alternative\|enable_barrier\|enable_fractional\|enable_regime_detection\|enable_wave_d' crates/ml-features/src/config.rs` — zero matches +- [ ] `SQLX_OFFLINE=true cargo test -p ml-features --lib` — all feature tests pass +- [ ] `cargo test -p ml --lib -- smoke_tests --ignored --nocapture` — smoke tests pass (OHLCV fallback works when no MBP-10) +- [ ] `mbp10_to_imbalance_bars()` produces bars from `test_data/mbp10/` +- [ ] `dqn-production.toml` has `imbalance_bar_threshold`, `imbalance_bar_ewma_alpha`, `min_hold_bars` +- [ ] `train_baseline_rl --help` shows `--min-hold-bars` and `--initial-capital` +- [ ] All 3 Argo train templates pass `--min-hold-bars` parameter