Files
foxhunt/crates/ml-regime-detection/src/lib.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

297 lines
9.5 KiB
Rust

//! Regime Detection Models for Market State Identification
//!
//! Implements regime detection algorithms to identify different market states
//! and adapt ML models accordingly.
//!
//! ## Sub-modules
//!
//! - [`feature_classifier`] -- Real-time regime classification using ADX, Hurst exponent,
//! and volatility z-score thresholds.
//! - [`hmm`] -- Offline 3-state Gaussian Hidden Markov Model for regime transition estimation.
#![deny(clippy::unwrap_used, clippy::expect_used)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(missing_debug_implementations)]
#![allow(unused_crate_dependencies)]
#![allow(clippy::float_arithmetic)]
#![allow(clippy::partial_pub_fields)]
#![allow(clippy::shadow_reuse)]
#![allow(clippy::shadow_unrelated)]
#![allow(clippy::shadow_same)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::indexing_slicing)]
#![allow(clippy::missing_const_for_fn)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::integer_division)]
#![allow(clippy::similar_names)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::as_conversions)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::default_numeric_fallback)]
#![allow(clippy::arithmetic_side_effects)]
pub mod feature_classifier;
pub mod hmm;
use serde::{Deserialize, Serialize};
use ml_core::MLError;
use ml_dqn::RegimeType;
pub use feature_classifier::{ClassifierConfig, FeatureClassifier};
pub use hmm::{HMMConfig, HiddenMarkovModel};
/// Configuration for regime detection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegimeDetectionConfig {
pub window_size: usize,
pub min_regime_duration: usize,
pub threshold: f64,
}
impl Default for RegimeDetectionConfig {
fn default() -> Self {
Self {
window_size: 100,
min_regime_duration: 10,
threshold: 0.05,
}
}
}
/// Regime detection engine combining feature-based classification and HMM.
///
/// The engine accumulates feature data via [`update_features`](Self::update_features)
/// and uses a [`FeatureClassifier`] to classify the current regime from the most
/// recent ADX, Hurst exponent, and volatility observations.
///
/// Feature vector layout (expected indices):
/// - Index 0: ADX (Average Directional Index)
/// - Index 1: Hurst exponent
/// - Index 2: Current volatility
///
/// For legacy compatibility, arbitrary feature slices are also accepted and
/// accumulated; the classifier only uses the first 3 elements when present.
#[derive(Debug)]
pub struct RegimeDetectionEngine {
pub total_updates: u64,
pub feature_data: Vec<f64>,
config: RegimeDetectionConfig,
classifier: FeatureClassifier,
/// Most recent ADX value (updated via `update_features`)
last_adx: f64,
/// Most recent Hurst exponent (updated via `update_features`)
last_hurst: f64,
}
impl RegimeDetectionEngine {
/// Create a new regime detection engine.
pub fn new(config: RegimeDetectionConfig) -> Result<Self, MLError> {
let classifier_config = ClassifierConfig {
vol_window: config.window_size,
..ClassifierConfig::default()
};
let classifier = FeatureClassifier::new(classifier_config);
Ok(Self {
total_updates: 0,
feature_data: Vec::new(),
config,
classifier,
last_adx: 0.0,
last_hurst: 0.5, // neutral default
})
}
/// Create engine with custom classifier configuration.
pub fn with_classifier_config(
config: RegimeDetectionConfig,
classifier_config: ClassifierConfig,
) -> Result<Self, MLError> {
let classifier = FeatureClassifier::new(classifier_config);
Ok(Self {
total_updates: 0,
feature_data: Vec::new(),
config,
classifier,
last_adx: 0.0,
last_hurst: 0.5,
})
}
/// Update the engine with new feature observations.
///
/// Expected feature layout:
/// - `features[0]` = ADX
/// - `features[1]` = Hurst exponent
/// - `features[2]` = Current volatility
///
/// If fewer elements are provided, only the available ones are used.
pub fn update_features(&mut self, features: &[f64]) -> Result<(), MLError> {
self.feature_data.extend_from_slice(features);
self.total_updates += 1;
// Cap feature_data to avoid unbounded growth in long-running sessions.
// Keep the most recent `window_size * 3` entries (3 features per update).
let max_len = self.config.window_size * 3;
if self.feature_data.len() > max_len * 2 {
let drain_to = self.feature_data.len() - max_len;
self.feature_data.drain(..drain_to);
}
// Extract structured features when available
if let Some(&adx) = features.first() {
self.last_adx = adx;
}
if let Some(&hurst) = features.get(1) {
self.last_hurst = hurst;
}
if let Some(&vol) = features.get(2) {
self.classifier.update_volatility(vol);
}
Ok(())
}
/// Detect the current market regime using the feature classifier.
///
/// Returns a [`RegimeType`] based on accumulated ADX, Hurst exponent,
/// and volatility data.
pub fn detect_regime(&self) -> Result<RegimeType, MLError> {
Ok(self.classifier.classify(self.last_adx, self.last_hurst))
}
/// Get a reference to the internal feature classifier.
pub fn classifier(&self) -> &FeatureClassifier {
&self.classifier
}
/// Get the engine configuration.
pub fn config(&self) -> &RegimeDetectionConfig {
&self.config
}
}
#[cfg(test)]
#[allow(
clippy::assertions_on_result_states,
clippy::float_equality_without_abs,
clippy::unreachable
)]
mod tests {
use super::*;
#[tokio::test]
async fn test_regime_detection_engine_creation() -> Result<(), Box<dyn std::error::Error>> {
let config = RegimeDetectionConfig::default();
let engine = RegimeDetectionEngine::new(config)?;
assert_eq!(engine.total_updates, 0);
assert!(engine.feature_data.is_empty());
Ok(())
}
#[tokio::test]
async fn test_feature_data_update() -> Result<(), Box<dyn std::error::Error>> {
let mut engine = RegimeDetectionEngine::new(RegimeDetectionConfig::default())?;
let features = vec![0.1, 0.01];
let result = engine.update_features(&features);
assert!(result.is_ok());
assert_eq!(engine.total_updates, 1);
assert_eq!(engine.feature_data.len(), 2);
Ok(())
}
#[tokio::test]
async fn test_regime_detection_default_ranging() -> Result<(), Box<dyn std::error::Error>> {
let engine = RegimeDetectionEngine::new(RegimeDetectionConfig::default())?;
// No features -> ADX=0, Hurst=0.5, no vol -> Ranging fallback
let regime = engine.detect_regime()?;
assert_eq!(regime, RegimeType::Ranging);
Ok(())
}
#[tokio::test]
async fn test_regime_detection_trending() -> Result<(), Box<dyn std::error::Error>> {
let mut engine = RegimeDetectionEngine::new(RegimeDetectionConfig::default())?;
// Feed trending features: ADX=30, Hurst=0.65, vol=0.02
for _ in 0..20 {
engine.update_features(&[30.0, 0.65, 0.02])?;
}
let regime = engine.detect_regime()?;
assert_eq!(regime, RegimeType::Trending);
Ok(())
}
#[tokio::test]
async fn test_regime_detection_volatile() -> Result<(), Box<dyn std::error::Error>> {
let mut engine = RegimeDetectionEngine::new(RegimeDetectionConfig::default())?;
// Fill with low vol first
for _ in 0..50 {
engine.update_features(&[10.0, 0.50, 0.01])?;
}
// Spike volatility
engine.update_features(&[10.0, 0.50, 0.50])?;
let regime = engine.detect_regime()?;
assert_eq!(regime, RegimeType::Volatile);
Ok(())
}
#[test]
fn test_config_defaults() {
let config = RegimeDetectionConfig::default();
assert_eq!(config.window_size, 100);
assert_eq!(config.min_regime_duration, 10);
assert_eq!(config.threshold, 0.05);
}
#[test]
fn test_config_serialization() {
let config = RegimeDetectionConfig::default();
let serialized =
serde_json::to_string(&config).unwrap_or_else(|_| "{}".to_owned());
let deserialized: RegimeDetectionConfig =
serde_json::from_str(&serialized).unwrap_or_default();
assert_eq!(config.window_size, deserialized.window_size);
assert_eq!(config.min_regime_duration, deserialized.min_regime_duration);
assert!(config.threshold - deserialized.threshold < f64::EPSILON);
}
#[test]
fn test_with_classifier_config() {
let regime_config = RegimeDetectionConfig::default();
let classifier_config = ClassifierConfig {
adx_trending_threshold: 30.0,
hurst_trending_threshold: 0.60,
..ClassifierConfig::default()
};
let engine =
RegimeDetectionEngine::with_classifier_config(regime_config, classifier_config);
assert!(engine.is_ok());
let engine = engine.unwrap_or_else(|_| {
RegimeDetectionEngine::new(RegimeDetectionConfig::default())
.unwrap_or_else(|_| unreachable!())
});
assert_eq!(engine.classifier().config().adx_trending_threshold, 30.0);
}
}