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 { persistence, regime_label }
28    }
29}
30
31/// Wrapper that turns the existing HurstExponent into a feature-rich extractor.
32#[derive(Debug, Clone)]
33pub struct HurstFeatureExtractor {
34    inner: HurstExponent,
35    /// Thresholds for regime classification (can be made configurable later).
36    mean_reverting_threshold: f64,
37    trending_threshold: f64,
38}
39
40impl HurstFeatureExtractor {
41    pub fn new(period: usize) -> Self {
42        Self {
43            inner: HurstExponent::new(period),
44            mean_reverting_threshold: 0.45,
45            trending_threshold: 0.55,
46        }
47    }
48
49    pub fn with_thresholds(mut self, mean_rev: f64, trending: f64) -> Self {
50        self.mean_reverting_threshold = mean_rev;
51        self.trending_threshold = trending;
52        self
53    }
54}
55
56impl Next<f64> for HurstFeatureExtractor {
57    type Output = HurstFeatures;
58
59    fn next(&mut self, input: f64) -> Self::Output {
60        let persistence = self.inner.next(input);
61
62        let regime_label = if persistence.is_nan() {
63            None
64        } else if persistence < self.mean_reverting_threshold {
65            Some(-1)
66        } else if persistence > self.trending_threshold {
67            Some(1)
68        } else {
69            Some(0)
70        };
71
72        HurstFeatures::new(persistence, regime_label)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use approx::assert_relative_eq;
80
81    #[test]
82    fn test_hurst_feature_basic() {
83        let mut extractor = HurstFeatureExtractor::new(20);
84
85        // Feed trending data
86        for i in 0..30 {
87            let val = 100.0 + (i as f64) * 0.5;
88            let f = extractor.next(val);
89            if !f.persistence.is_nan() {
90                assert!(f.persistence > 0.5, "Expected trending persistence, got {}", f.persistence);
91            }
92        }
93    }
94
95    #[test]
96    fn test_hurst_feature_regime_labels() {
97        let mut extractor = HurstFeatureExtractor::new(10)
98            .with_thresholds(0.4, 0.6);
99
100        // Force a high persistence value via dummy (real impl will vary)
101        // This is a smoke test for the wrapper logic
102        let f = extractor.next(100.0);
103        // After warmup it should produce a label
104        if !f.persistence.is_nan() {
105            assert!(f.regime_label.is_some());
106        }
107    }
108}