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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
397
docs/superpowers/plans/2026-03-31-imbalance-bars-and-cleanup.md
Normal file
397
docs/superpowers/plans/2026-03-31-imbalance-bars-and-cleanup.md
Normal 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
|
||||
Reference in New Issue
Block a user