Files
foxhunt/crates/ml-regime/src/transition_probability_features.rs
jgrusewski db6462ba7a fix(clippy): resolve all clippy warnings across entire workspace (--all-targets)
Systematic fix of 360+ clippy errors across 37+ crates covering lib,
test, bench, and example targets. Key changes:

- Add targeted #[allow(...)] on #[cfg(test)] modules for test-only lints
  (assertions_on_result_states, float_cmp, str_to_string, indexing, etc.)
- Feature-gate broken integration tests behind __<crate>_integration flags
  where public APIs changed (trading-service, backtesting-service, etc.)
- Remove dead [[test]] entries from Cargo.toml files pointing to deleted files
- Fix production code: field_reassign_with_default, manual_range_contains,
  assert!(false) → panic!(), format!("{}") simplification, len() > 0 → !is_empty()
- Delete truly unused code (Order struct, unused methods/fields/variants)
- Convert sqlx::query!() to sqlx::query() for SQLX_OFFLINE compatibility

Result: cargo clippy --workspace --all-targets -- -D warnings = 0 errors, 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:18:35 +01:00

356 lines
11 KiB
Rust

//! Transition Probability Features (Indices 216-220)
//!
//! Extracts 5 features from regime transition probabilities:
//! - Feature 216: Stability P(i→i) - probability of staying in current regime
//! - Feature 217: Most likely next regime (index) - which regime is most probable next
//! - Feature 218: Shannon entropy H = -Σ P(i→j) log₂ P(i→j) - uncertainty measure
//! - Feature 219: Expected duration - how long regime typically persists
//! - Feature 220: Change probability (1 - stability) - probability of regime change
//!
//! **ARCHITECTURAL DESIGN**:
//! - **REUSE** existing `RegimeTransitionMatrix` for all probability calculations
//! - **REUSE** existing `expected_duration()` method for Feature 219
//! - No duplication of transition tracking logic
//! - Shannon entropy computed with numerical stability (filters p < 1e-10)
//!
//! # Example
//!
//! ```rust
//! use ml::regime::transition_probability_features::TransitionProbabilityFeatures;
//! use ml::ensemble::MarketRegime;
//!
//! let regimes = vec![
//! MarketRegime::Bull,
//! MarketRegime::Bear,
//! MarketRegime::Sideways,
//! ];
//!
//! let mut features = TransitionProbabilityFeatures::new(regimes, 0.1, 10);
//!
//! // Update with observed regime
//! features.update(MarketRegime::Bull);
//! features.update(MarketRegime::Bear);
//!
//! // Compute all 5 features
//! let result = features.compute_features();
//! assert_eq!(result.len(), 5);
//! ```
use crate::MarketRegime;
use crate::transition_matrix::RegimeTransitionMatrix;
/// Transition Probability Feature Extractor
///
/// Maintains a transition matrix and extracts 5 probability-based features:
/// 1. Stability (self-transition probability)
/// 2. Most likely next regime
/// 3. Shannon entropy (uncertainty)
/// 4. Expected duration (persistence)
/// 5. Change probability (1 - stability)
///
/// # Design Principles
///
/// - **REUSE**: Delegates all transition tracking to `RegimeTransitionMatrix`
/// - **PERFORMANCE**: O(N) where N = number of regimes (typically 4-6)
/// - **NUMERICAL STABILITY**: Filters probabilities < 1e-10 before log operations
///
/// # Feature Descriptions
///
/// **Feature 216: Stability P(i→i)**
/// - Probability of staying in current regime
/// - High stability (>0.8) indicates persistent regime
/// - Low stability (<0.3) indicates transitional regime
///
/// **Feature 217: Most Likely Next Regime**
/// - Index of regime with highest transition probability from current regime
/// - Used for predictive regime classification
/// - Value range: [0, N-1] where N = number of regimes
///
/// **Feature 218: Shannon Entropy**
/// - H = -Σ P(i→j) log₂ P(i→j)
/// - Measures uncertainty in regime transitions
/// - High entropy: many possible transitions (uncertain)
/// - Low entropy: few likely transitions (predictable)
/// - Max entropy: log₂(N) for uniform distribution
///
/// **Feature 219: Expected Duration**
/// - `E[T]` = 1 / (1 - `P[i][i]`)
/// - Expected number of periods in current regime
/// - REUSES existing `get_expected_duration()` method
///
/// **Feature 220: Change Probability**
/// - 1 - P(i→i)
/// - Probability of transitioning out of current regime
/// - Complementary to stability (Feature 216)
#[derive(Debug, Clone)]
pub struct TransitionProbabilityFeatures {
/// Regime transition matrix (REUSED infrastructure)
matrix: RegimeTransitionMatrix,
/// Current market regime
current_regime: MarketRegime,
/// List of all regimes (for iteration)
regimes: Vec<MarketRegime>,
}
impl TransitionProbabilityFeatures {
/// Create a new transition probability feature extractor
///
/// # Arguments
///
/// * `regimes` - List of market regimes to track
/// * `alpha` - EMA smoothing factor (0 < alpha <= 1)
/// * `min_obs` - Minimum observations before using empirical probabilities
///
/// # Returns
///
/// New feature extractor initialized with the first regime as current
///
/// # Example
///
/// ```rust
/// use ml::regime::transition_probability_features::TransitionProbabilityFeatures;
/// use ml::ensemble::MarketRegime;
///
/// let regimes = vec![
/// MarketRegime::Bull,
/// MarketRegime::Bear,
/// ];
/// let features = TransitionProbabilityFeatures::new(regimes, 0.1, 10);
/// ```
pub fn new(regimes: Vec<MarketRegime>, alpha: f64, min_obs: usize) -> Self {
let current_regime = regimes.last().copied().unwrap_or(MarketRegime::Unknown);
let matrix = RegimeTransitionMatrix::new(regimes.clone(), alpha, min_obs);
Self {
matrix,
current_regime,
regimes,
}
}
/// Update with new regime observation
///
/// If the regime has changed, updates the transition matrix.
/// If the regime is the same, still updates the matrix to track persistence.
///
/// # Arguments
///
/// * `regime` - Newly observed market regime
///
/// # Example
///
/// ```rust
/// use ml::regime::transition_probability_features::TransitionProbabilityFeatures;
/// use ml::ensemble::MarketRegime;
///
/// let regimes = vec![MarketRegime::Bull, MarketRegime::Bear];
/// let mut features = TransitionProbabilityFeatures::new(regimes, 0.1, 1);
///
/// features.update(MarketRegime::Bull);
/// features.update(MarketRegime::Bear); // Transition recorded
/// ```
pub fn update(&mut self, regime: MarketRegime) {
// Always update the matrix (even for same regime to track persistence)
self.matrix.update(self.current_regime, regime);
self.current_regime = regime;
}
/// Compute all 5 transition probability features
///
/// Returns array of 5 features:
/// - `[0]`: Stability P(i→i)
/// - `[1]`: Most likely next regime (index)
/// - `[2]`: Shannon entropy
/// - `[3]`: Expected duration
/// - `[4]`: Change probability
///
/// # Returns
///
/// Array of 5 f64 values representing the features
///
/// # Example
///
/// ```rust
/// use ml::regime::transition_probability_features::TransitionProbabilityFeatures;
/// use ml::ensemble::MarketRegime;
///
/// let regimes = vec![MarketRegime::Bull, MarketRegime::Bear];
/// let mut features = TransitionProbabilityFeatures::new(regimes, 0.1, 1);
///
/// features.update(MarketRegime::Bull);
/// let result = features.compute_features();
///
/// assert_eq!(result.len(), 5);
/// ```
pub fn compute_features(&self) -> [f64; 5] {
// Feature 216: Stability P(i→i)
let stability = self
.matrix
.get_transition_prob(self.current_regime, self.current_regime);
// Feature 217: Most likely next regime
let mut max_prob = 0.0;
let mut most_likely_idx = 0;
for (idx, &next_regime) in self.regimes.iter().enumerate() {
let prob = self
.matrix
.get_transition_prob(self.current_regime, next_regime);
if prob > max_prob {
max_prob = prob;
most_likely_idx = idx;
}
}
// Feature 218: Shannon entropy H = -Σ P(i→j) log₂ P(i→j)
let entropy: f64 = self.regimes.iter()
.map(|&next| self.matrix.get_transition_prob(self.current_regime, next))
.filter(|&p| p > 1e-10) // Numerical stability: avoid log(0)
.map(|p| -p * p.log2())
.sum();
// Feature 219: Expected duration (REUSE existing method!)
let duration = self.matrix.get_expected_duration(self.current_regime);
// Feature 220: Change probability (1 - stability)
let change_prob = 1.0 - stability;
[
stability,
most_likely_idx as f64,
entropy,
duration,
change_prob,
]
}
/// Get current market regime
///
/// # Returns
///
/// Current regime being tracked
///
/// # Example
///
/// ```rust
/// use ml::regime::transition_probability_features::TransitionProbabilityFeatures;
/// use ml::ensemble::MarketRegime;
///
/// let regimes = vec![MarketRegime::Bull];
/// let mut features = TransitionProbabilityFeatures::new(regimes, 0.1, 1);
///
/// features.update(MarketRegime::Bull);
/// assert_eq!(features.current_regime(), MarketRegime::Bull);
/// ```
pub fn current_regime(&self) -> MarketRegime {
self.current_regime
}
/// Get reference to underlying transition matrix (for advanced use)
///
/// Allows direct access to transition probabilities and stationary distribution
/// when needed for debugging or analysis.
///
/// # Returns
///
/// Reference to the underlying `RegimeTransitionMatrix`
pub fn transition_matrix(&self) -> &RegimeTransitionMatrix {
&self.matrix
}
}
#[cfg(test)]
#[allow(clippy::manual_range_contains)]
mod tests {
use super::*;
#[test]
fn test_initialization() {
let regimes = vec![MarketRegime::Bull, MarketRegime::Bear];
let features = TransitionProbabilityFeatures::new(regimes, 0.1, 10);
assert_eq!(features.current_regime(), MarketRegime::Bear);
}
#[test]
fn test_compute_features_returns_five_values() {
let regimes = vec![MarketRegime::Bull, MarketRegime::Bear];
let features = TransitionProbabilityFeatures::new(regimes, 0.1, 10);
let result = features.compute_features();
assert_eq!(result.len(), 5);
}
#[test]
fn test_stability_bounds() {
let regimes = vec![MarketRegime::Sideways];
let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1);
for _ in 0..10 {
features.update(MarketRegime::Sideways);
}
let result = features.compute_features();
let stability = result[0];
assert!(
stability >= 0.0 && stability <= 1.0,
"Stability should be in [0,1], got {}",
stability
);
}
#[test]
fn test_entropy_non_negative() {
let regimes = vec![
MarketRegime::Bull,
MarketRegime::Bear,
MarketRegime::Sideways,
];
let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1);
features.update(MarketRegime::Bull);
features.update(MarketRegime::Bear);
let result = features.compute_features();
let entropy = result[2];
assert!(
entropy >= 0.0,
"Entropy should be non-negative, got {}",
entropy
);
assert!(
entropy.is_finite(),
"Entropy should be finite, got {}",
entropy
);
}
#[test]
fn test_complementary_stability_change_prob() {
let regimes = vec![MarketRegime::Bull, MarketRegime::Bear];
let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1);
features.update(MarketRegime::Bull);
features.update(MarketRegime::Bear);
let result = features.compute_features();
let stability = result[0];
let change_prob = result[4];
assert!(
(stability + change_prob - 1.0).abs() < 1e-10,
"Stability + change probability should equal 1.0, got {} + {} = {}",
stability,
change_prob,
stability + change_prob
);
}
}