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    use approx::assert_relative_eq;
83
84    #[test]
85    fn test_hurst_feature_basic() {
86        let mut extractor = HurstFeatureExtractor::new(20);
87
88        // Feed trending data
89        for i in 0..30 {
90            let val = 100.0 + (i as f64) * 0.5;
91            let f = extractor.next(val);
92            if !f.persistence.is_nan() {
93                assert!(
94                    f.persistence > 0.5,
95                    "Expected trending persistence, got {}",
96                    f.persistence
97                );
98            }
99        }
100    }
101
102    #[test]
103    fn test_hurst_feature_regime_labels() {
104        let mut extractor = HurstFeatureExtractor::new(10).with_thresholds(0.4, 0.6);
105
106        // Force a high persistence value via dummy (real impl will vary)
107        // This is a smoke test for the wrapper logic
108        let f = extractor.next(100.0);
109        // After warmup it should produce a label
110        if !f.persistence.is_nan() {
111            assert!(f.regime_label.is_some());
112        }
113    }
114}