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>
565 lines
19 KiB
Rust
565 lines
19 KiB
Rust
//! Feature Configuration for Progressive ML Feature Engineering
|
|
//!
|
|
//! This module defines the feature set configuration across Wave 19 phases:
|
|
//! - Wave A: 26 features (real-time inference baseline)
|
|
//! - Wave B: 36 features (adds alternative bars + volume features)
|
|
//! - Wave C: 201 features (adds fractional diff + meta-labeling)
|
|
//! - Wave D: 225 features (adds regime detection + adaptive strategies)
|
|
//!
|
|
//! ## Architecture
|
|
//!
|
|
//! `FeatureConfig` provides a single source of truth for feature extraction
|
|
//! across both training (`DbnSequenceLoader`) and inference (`ProductionFeatureExtractorAdapter`).
|
|
//! This eliminates the previous padding bug (256 features via 25x repetition).
|
|
//!
|
|
//! ## Usage
|
|
//!
|
|
//! ```rust
|
|
//! use ml::features::config::{FeatureConfig, FeaturePhase};
|
|
//!
|
|
//! // Wave A: 26 features (baseline)
|
|
//! let config = FeatureConfig::wave_a();
|
|
//! assert_eq!(config.feature_count(), 26);
|
|
//!
|
|
//! // Wave B: 36 features (alternative bars)
|
|
//! let config = FeatureConfig::wave_b();
|
|
//! assert_eq!(config.feature_count(), 36);
|
|
//!
|
|
//! // Wave C: 201 features (advanced)
|
|
//! let config = FeatureConfig::wave_c();
|
|
//! assert_eq!(config.feature_count(), 201);
|
|
//!
|
|
//! // Wave D: 225 features (regime detection)
|
|
//! let config = FeatureConfig::wave_d();
|
|
//! assert_eq!(config.feature_count(), 225);
|
|
//! ```
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Feature category classification for Wave D features
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum FeatureCategory {
|
|
/// OHLCV baseline features
|
|
OHLCV,
|
|
/// Technical indicators
|
|
TechnicalIndicators,
|
|
/// Microstructure features
|
|
Microstructure,
|
|
/// Regime detection features
|
|
RegimeDetection,
|
|
/// Adaptive strategy features
|
|
AdaptiveStrategy,
|
|
}
|
|
|
|
/// Individual feature definition with index, name, and category
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Feature {
|
|
/// Feature index (0-224 for Wave D)
|
|
pub index: usize,
|
|
/// Feature name
|
|
pub name: String,
|
|
/// Feature category
|
|
pub category: FeatureCategory,
|
|
}
|
|
|
|
impl Feature {
|
|
/// Create a new feature definition
|
|
pub const fn new(index: usize, _name: &'static str, category: FeatureCategory) -> Self {
|
|
Self {
|
|
index,
|
|
name: String::new(), // Will be set via constructor
|
|
category,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wave D feature definitions (indices 201-224)
|
|
///
|
|
/// This provides the detailed specification for all 24 Wave D features
|
|
/// that extend Wave C's 201 features to reach 225 total features.
|
|
pub fn wave_d_features() -> Vec<Feature> {
|
|
vec![
|
|
// CUSUM Statistics (indices 201-210, 10 features)
|
|
Feature {
|
|
index: 201,
|
|
name: "cusum_s_plus_normalized".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 202,
|
|
name: "cusum_s_minus_normalized".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 203,
|
|
name: "cusum_break_indicator".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 204,
|
|
name: "cusum_direction".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 205,
|
|
name: "cusum_time_since_break".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 206,
|
|
name: "cusum_frequency".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 207,
|
|
name: "cusum_positive_count".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 208,
|
|
name: "cusum_negative_count".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 209,
|
|
name: "cusum_intensity".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 210,
|
|
name: "cusum_drift_ratio".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
// ADX & Directional Indicators (indices 211-215, 5 features)
|
|
Feature {
|
|
index: 211,
|
|
name: "adx".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 212,
|
|
name: "plus_di".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 213,
|
|
name: "minus_di".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 214,
|
|
name: "dx".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 215,
|
|
name: "trend_classification".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
// Regime Transition Probabilities (indices 216-220, 5 features)
|
|
Feature {
|
|
index: 216,
|
|
name: "regime_stability".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 217,
|
|
name: "most_likely_next_regime".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 218,
|
|
name: "regime_entropy".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 219,
|
|
name: "regime_expected_duration".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
Feature {
|
|
index: 220,
|
|
name: "regime_change_probability".to_owned(),
|
|
category: FeatureCategory::RegimeDetection,
|
|
},
|
|
// Adaptive Strategy Metrics (indices 221-224, 4 features)
|
|
Feature {
|
|
index: 221,
|
|
name: "position_multiplier".to_owned(),
|
|
category: FeatureCategory::AdaptiveStrategy,
|
|
},
|
|
Feature {
|
|
index: 222,
|
|
name: "stop_loss_multiplier".to_owned(),
|
|
category: FeatureCategory::AdaptiveStrategy,
|
|
},
|
|
Feature {
|
|
index: 223,
|
|
name: "regime_conditioned_sharpe".to_owned(),
|
|
category: FeatureCategory::AdaptiveStrategy,
|
|
},
|
|
Feature {
|
|
index: 224,
|
|
name: "risk_budget_utilization".to_owned(),
|
|
category: FeatureCategory::AdaptiveStrategy,
|
|
},
|
|
]
|
|
}
|
|
|
|
/// Feature extraction configuration for Wave 19 progressive engineering
|
|
///
|
|
/// 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,
|
|
}
|
|
|
|
/// Feature engineering phase (Wave 19 progressive implementation)
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum FeaturePhase {
|
|
/// Wave A: 26 features (baseline technical indicators + microstructure)
|
|
WaveA,
|
|
/// Wave B: 36 features (adds alternative bars + barrier optimization)
|
|
WaveB,
|
|
/// Wave C: 201 features (adds fractional diff + regime detection)
|
|
WaveC,
|
|
/// Wave D: 225 features (adds Wave D regime detection + adaptive strategies)
|
|
WaveD,
|
|
}
|
|
|
|
impl Default for FeatureConfig {
|
|
/// Default configuration: Wave A (26 features)
|
|
fn default() -> Self {
|
|
Self::wave_a()
|
|
}
|
|
}
|
|
|
|
impl FeatureConfig {
|
|
/// Wave A configuration: 26 features (baseline)
|
|
///
|
|
/// Feature breakdown:
|
|
/// - OHLCV: 5 features (open, high, low, close, volume)
|
|
/// - Technical indicators: 21 features (RSI, MACD, Bollinger, ATR, ADX, CCI, Stochastic, EMAs, etc.)
|
|
///
|
|
/// Total: 26 features
|
|
pub const fn wave_a() -> Self {
|
|
Self {
|
|
phase: FeaturePhase::WaveA,
|
|
}
|
|
}
|
|
|
|
/// Wave B configuration: 36 features (alternative bars)
|
|
///
|
|
/// Feature breakdown:
|
|
/// - Wave A: 26 features (OHLCV + technical indicators)
|
|
/// - Alternative bars: 10 features (dollar, volume, tick, run, imbalance bars)
|
|
///
|
|
/// Total: 36 features
|
|
pub const fn wave_b() -> Self {
|
|
Self {
|
|
phase: FeaturePhase::WaveB,
|
|
}
|
|
}
|
|
|
|
/// Wave C configuration: 201 features (advanced)
|
|
///
|
|
/// Feature breakdown:
|
|
/// - Base: 39 features (OHLCV 5 + Technical 21 + Microstructure 3 + Alternative bars 10)
|
|
/// - Wave C additions: 162 features (fractional differentiation, regime detection, statistical features)
|
|
///
|
|
/// Total: 201 features (indices 0-200)
|
|
pub const fn wave_c() -> Self {
|
|
Self {
|
|
phase: FeaturePhase::WaveC,
|
|
}
|
|
}
|
|
|
|
/// Wave D configuration: 225 features (regime detection + adaptive strategies)
|
|
///
|
|
/// Feature breakdown:
|
|
/// - Wave C: 201 features (indices 0-200)
|
|
/// - Wave D additions: 24 features (indices 201-224)
|
|
/// - CUSUM Statistics: 10 features (indices 201-210)
|
|
/// - ADX & Directional Indicators: 5 features (indices 211-215)
|
|
/// - Regime Transition Probabilities: 5 features (indices 216-220)
|
|
/// - Adaptive Strategy Metrics: 4 features (indices 221-224)
|
|
///
|
|
/// Total: 225 features (indices 0-224)
|
|
pub const fn wave_d() -> Self {
|
|
Self {
|
|
phase: FeaturePhase::WaveD,
|
|
}
|
|
}
|
|
|
|
/// Calculate total feature count based on the feature phase.
|
|
///
|
|
/// 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 {
|
|
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)
|
|
}
|
|
}
|
|
|
|
/// Get feature indices for each group based on the phase.
|
|
///
|
|
/// 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;
|
|
|
|
// OHLCV: always present (all phases)
|
|
indices.ohlcv = Some((current_idx, current_idx + 5));
|
|
current_idx += 5;
|
|
|
|
// Technical indicators: always present (all phases)
|
|
indices.technical_indicators = Some((current_idx, current_idx + 21));
|
|
current_idx += 21;
|
|
|
|
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 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;
|
|
}
|
|
|
|
if matches!(self.phase, FeaturePhase::WaveD) {
|
|
// Wave D regime features (24)
|
|
indices.wave_d_regime = Some((current_idx, current_idx + 24));
|
|
}
|
|
|
|
indices
|
|
}
|
|
|
|
/// 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 {
|
|
// 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 when the phase is Wave D. Returns empty
|
|
/// for earlier phases.
|
|
pub fn get_wave_d_features(&self) -> Vec<Feature> {
|
|
if matches!(self.phase, FeaturePhase::WaveD) {
|
|
wave_d_features()
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Feature group classification
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum FeatureGroup {
|
|
/// OHLCV features (5)
|
|
OHLCV,
|
|
/// Technical indicators (21)
|
|
TechnicalIndicators,
|
|
/// Microstructure features (3)
|
|
Microstructure,
|
|
/// Alternative bar features (10)
|
|
AlternativeBars,
|
|
/// Barrier optimization (labels, not features)
|
|
BarrierOptimization,
|
|
/// Fractional differentiation (20)
|
|
FractionalDiff,
|
|
/// Regime detection (10)
|
|
RegimeDetection,
|
|
/// Wave D regime detection (24)
|
|
WaveDRegime,
|
|
}
|
|
|
|
/// Feature index ranges for each group
|
|
///
|
|
/// Provides (`start_idx`, `end_idx`) for each feature group to allow
|
|
/// data loaders and models to identify which indices correspond to which features.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct FeatureIndices {
|
|
/// OHLCV indices (5 features)
|
|
pub ohlcv: Option<(usize, usize)>,
|
|
/// Technical indicator indices (21 features)
|
|
pub technical_indicators: Option<(usize, usize)>,
|
|
/// Microstructure indices (3 features)
|
|
pub microstructure: Option<(usize, usize)>,
|
|
/// Alternative bar indices (10 features)
|
|
pub alternative_bars: Option<(usize, usize)>,
|
|
/// Fractional differentiation indices (20 features)
|
|
pub fractional_diff: Option<(usize, usize)>,
|
|
/// Regime detection indices (10 features)
|
|
pub regime_detection: Option<(usize, usize)>,
|
|
/// Wave D regime detection indices (24 features, indices 201-224)
|
|
pub wave_d_regime: Option<(usize, usize)>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_wave_a_config() {
|
|
let config = FeatureConfig::wave_a();
|
|
assert_eq!(config.phase, FeaturePhase::WaveA);
|
|
assert_eq!(config.feature_count(), 26);
|
|
}
|
|
|
|
#[test]
|
|
fn test_wave_b_config() {
|
|
let config = FeatureConfig::wave_b();
|
|
assert_eq!(config.phase, FeaturePhase::WaveB);
|
|
assert_eq!(config.feature_count(), 36);
|
|
}
|
|
|
|
#[test]
|
|
fn test_wave_c_config() {
|
|
let config = FeatureConfig::wave_c();
|
|
assert_eq!(config.phase, FeaturePhase::WaveC);
|
|
assert_eq!(config.feature_count(), 201); // Wave C has exactly 201 features
|
|
}
|
|
|
|
#[test]
|
|
fn test_wave_d_config() {
|
|
let config = FeatureConfig::wave_d();
|
|
assert_eq!(config.phase, FeaturePhase::WaveD);
|
|
assert_eq!(config.feature_count(), 225); // Wave D has exactly 225 features
|
|
}
|
|
|
|
#[test]
|
|
fn test_wave_d_features() {
|
|
let features = wave_d_features();
|
|
assert_eq!(features.len(), 24);
|
|
|
|
// Verify indices are correct (201-224)
|
|
assert_eq!(features[0].index, 201);
|
|
assert_eq!(features[23].index, 224);
|
|
|
|
// Verify CUSUM features (10)
|
|
let cusum_features: Vec<_> = features
|
|
.iter()
|
|
.filter(|f| f.index >= 201 && f.index <= 210)
|
|
.collect();
|
|
assert_eq!(cusum_features.len(), 10);
|
|
|
|
// Verify ADX features (5)
|
|
let adx_features: Vec<_> = features
|
|
.iter()
|
|
.filter(|f| f.index >= 211 && f.index <= 215)
|
|
.collect();
|
|
assert_eq!(adx_features.len(), 5);
|
|
|
|
// Verify transition features (5)
|
|
let transition_features: Vec<_> = features
|
|
.iter()
|
|
.filter(|f| f.index >= 216 && f.index <= 220)
|
|
.collect();
|
|
assert_eq!(transition_features.len(), 5);
|
|
|
|
// Verify adaptive features (4)
|
|
let adaptive_features: Vec<_> = features
|
|
.iter()
|
|
.filter(|f| f.index >= 221 && f.index <= 224)
|
|
.collect();
|
|
assert_eq!(adaptive_features.len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_feature_indices_wave_a() {
|
|
let config = FeatureConfig::wave_a();
|
|
let indices = config.feature_indices();
|
|
|
|
assert_eq!(indices.ohlcv, Some((0, 5)));
|
|
assert_eq!(indices.technical_indicators, Some((5, 26)));
|
|
assert_eq!(indices.microstructure, None); // Not in Wave A phase
|
|
assert_eq!(indices.alternative_bars, None); // Not in Wave A phase
|
|
}
|
|
|
|
#[test]
|
|
fn test_feature_indices_wave_b() {
|
|
let config = FeatureConfig::wave_b();
|
|
let indices = config.feature_indices();
|
|
|
|
assert_eq!(indices.ohlcv, Some((0, 5)));
|
|
assert_eq!(indices.technical_indicators, Some((5, 26)));
|
|
assert_eq!(indices.alternative_bars, Some((26, 36)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_feature_indices_wave_d() {
|
|
let config = FeatureConfig::wave_d();
|
|
let indices = config.feature_indices();
|
|
|
|
// Verify Wave D indices exist and are at the end
|
|
assert!(indices.wave_d_regime.is_some());
|
|
let (start, end) = indices.wave_d_regime.unwrap();
|
|
assert_eq!(end - start, 24); // 24 Wave D features
|
|
// Wave D features should start after Wave C features (201+)
|
|
assert!(start >= 201);
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_enabled() {
|
|
let config = FeatureConfig::wave_a();
|
|
assert!(config.is_enabled(FeatureGroup::OHLCV));
|
|
assert!(config.is_enabled(FeatureGroup::TechnicalIndicators));
|
|
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]
|
|
fn test_get_wave_d_features() {
|
|
let config = FeatureConfig::wave_d();
|
|
let features = config.get_wave_d_features();
|
|
assert_eq!(features.len(), 24);
|
|
|
|
let config = FeatureConfig::wave_c();
|
|
let features = config.get_wave_d_features();
|
|
assert_eq!(features.len(), 0); // Wave C phase doesn't include Wave D features
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_is_wave_a() {
|
|
let config = FeatureConfig::default();
|
|
assert_eq!(config.phase, FeaturePhase::WaveA);
|
|
assert_eq!(config.feature_count(), 26);
|
|
}
|
|
}
|