cleanup: remove ALL 32 enable_* feature flags — all features unconditional

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) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-31 10:56:20 +02:00
parent ec88b6aaa4
commit 0c9f368d24
17 changed files with 576 additions and 459 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -208,43 +208,13 @@ pub fn wave_d_features() -> Vec<Feature> {
/// 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<Feature> {
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]

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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,

View File

@@ -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)

View File

@@ -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
};

View File

@@ -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<f64> = 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,

View File

@@ -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;

View File

@@ -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<Arc<crate::dqn::stress_testing::DQNStressTester>> = 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;

View File

@@ -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);

View File

@@ -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<bool>,
pub shrink_perturb_interval: Option<usize>,
pub shrink_perturb_alpha: Option<f64>,
pub shrink_perturb_sigma: Option<f64>,
pub adversarial_dd_threshold: Option<f64>,
pub adversarial_epochs_trigger: Option<usize>,
pub enable_anti_lr: Option<bool>,
pub anti_lr_good_mult: Option<f64>,
pub anti_lr_bad_mult: Option<f64>,
pub anti_lr_sharpe_threshold: Option<f64>,
pub feature_mask_fraction: Option<f64>,
pub feature_noise_scale: Option<f64>,
pub enable_vol_normalization: Option<bool>,
pub pruning_epoch: Option<usize>,
pub pruning_fraction: Option<f64>,
pub enable_causal_intervention: Option<bool>,
pub causal_weight: Option<f64>,
pub causal_intervention_interval: Option<usize>,
pub enable_adversarial_self_play: Option<bool>,
pub adversarial_warmup_epochs: Option<usize>,
pub adversarial_checkpoint_interval: Option<usize>,
pub enable_gradient_vaccine: Option<bool>,
pub bottleneck_dim: Option<usize>,
pub stochastic_depth_prob: Option<f64>,
pub asymmetric_dd_weight: Option<f64>,
pub time_reversal_mod: Option<usize>,
pub regret_blend: Option<f64>,
pub enable_mirror_universe: Option<bool>,
pub position_entropy_weight: Option<f64>,
pub trade_clustering_penalty: Option<f64>,
pub ensemble_disagreement_penalty: Option<f64>,
@@ -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<bool>,
pub kelly_fractional: Option<f64>,
pub kelly_max_fraction: Option<f64>,
pub enable_action_masking: Option<bool>,
pub max_position_absolute: Option<f64>,
/// Alias for `max_position_absolute` — used when TOML key is `max_position`.
pub max_position: Option<f64>,
pub enable_circuit_breaker: Option<bool>,
/// Minimum Q-value clamp in GPU kernel (default -200.0)
pub q_clip_min: Option<f64>,
/// 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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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)?;

View File

@@ -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<chrono::Utc>, 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<OHLCVBar>` 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<Vec<OHLCVBar>, 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<usize>,
```
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