Skip to main content

quantwave_core/features/
hurst.rs

1//! Hurst Exponent feature extractor wrapper.
2//!
3//! Wraps the existing `HurstExponent` to provide a clean ML feature interface
4//! (persistence value + optional regime classification).
5//!
6//! This is one of the highest-ROI single features for ML/regime work:
7//! - H < 0.5 → mean-reverting (anti-persistent)
8//! - H ≈ 0.5 → random walk
9//! - H > 0.5 → trending (persistent)
10//!
11//! Source: quantwave-core/src/indicators/hurst.rs (Rescaled Range implementation)
12
13use crate::indicators::hurst::HurstExponent;
14use crate::traits::Next;
15
16/// Rich output for Hurst-based features.
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct HurstFeatures {
19    pub persistence: f64,
20    /// Optional discrete regime label for convenience in ML pipelines.
21    /// -1 = mean-reverting, 0 = random, +1 = trending (thresholds configurable later).
22    pub regime_label: Option<i8>,
23}
24
25impl HurstFeatures {
26    pub fn new(persistence: f64, regime_label: Option<i8>) -> Self {
27        Self {
28            persistence,
29            regime_label,
30        }
31    }
32}
33
34/// Wrapper that turns the existing HurstExponent into a feature-rich extractor.
35#[derive(Debug, Clone)]
36pub struct HurstFeatureExtractor {
37    inner: HurstExponent,
38    /// Thresholds for regime classification (can be made configurable later).
39    mean_reverting_threshold: f64,
40    trending_threshold: f64,
41}
42
43impl HurstFeatureExtractor {
44    pub fn new(period: usize) -> Self {
45        Self {
46            inner: HurstExponent::new(period),
47            mean_reverting_threshold: 0.45,
48            trending_threshold: 0.55,
49        }
50    }
51
52    pub fn with_thresholds(mut self, mean_rev: f64, trending: f64) -> Self {
53        self.mean_reverting_threshold = mean_rev;
54        self.trending_threshold = trending;
55        self
56    }
57}
58
59impl Next<f64> for HurstFeatureExtractor {
60    type Output = HurstFeatures;
61
62    fn next(&mut self, input: f64) -> Self::Output {
63        let persistence = self.inner.next(input);
64
65        let regime_label = if persistence.is_nan() {
66            None
67        } else if persistence < self.mean_reverting_threshold {
68            Some(-1)
69        } else if persistence > self.trending_threshold {
70            Some(1)
71        } else {
72            Some(0)
73        };
74
75        HurstFeatures::new(persistence, regime_label)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_hurst_feature_basic() {
85        let mut extractor = HurstFeatureExtractor::new(20);
86
87        // Feed trending data
88        for i in 0..30 {
89            let val = 100.0 + (i as f64) * 0.5;
90            let f = extractor.next(val);
91            if !f.persistence.is_nan() {
92                assert!(
93                    f.persistence > 0.5,
94                    "Expected trending persistence, got {}",
95                    f.persistence
96                );
97            }
98        }
99    }
100
101    #[test]
102    fn test_hurst_feature_regime_labels() {
103        let mut extractor = HurstFeatureExtractor::new(10).with_thresholds(0.4, 0.6);
104
105        // Force a high persistence value via dummy (real impl will vary)
106        // This is a smoke test for the wrapper logic
107        let f = extractor.next(100.0);
108        // After warmup it should produce a label
109        if !f.persistence.is_nan() {
110            assert!(f.regime_label.is_some());
111        }
112    }
113}