Skip to main content

quantwave_core/indicators/
hurst.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Hurst Exponent
6///
7/// Measures the long-term memory of time series.
8/// H < 0.5: Mean reverting (anti-persistent)
9/// H = 0.5: Random walk
10/// H > 0.5: Trending (persistent)
11///
12/// This implementation uses the Rescaled Range (R/S) analysis over a fixed window.
13#[derive(Debug, Clone)]
14pub struct HurstExponent {
15    period: usize,
16    window: VecDeque<f64>,
17}
18
19impl HurstExponent {
20    pub fn new(period: usize) -> Self {
21        Self {
22            period: period.max(2),
23            window: VecDeque::with_capacity(period),
24        }
25    }
26}
27
28impl Next<f64> for HurstExponent {
29    type Output = f64;
30
31    fn next(&mut self, input: f64) -> Self::Output {
32        self.window.push_back(input);
33        if self.window.len() > self.period {
34            self.window.pop_front();
35        }
36
37        if self.window.len() < self.period {
38            return f64::NAN;
39        }
40
41        // 1. Calculate Mean
42        let sum: f64 = self.window.iter().sum();
43        let mean = sum / self.period as f64;
44
45        // 2. Mean-adjusted series and cumulative deviates
46        // 3. Calculate Range
47        let mut cumulative_deviate = 0.0;
48        let mut max_z = f64::MIN;
49        let mut min_z = f64::MAX;
50
51        for &val in self.window.iter() {
52            cumulative_deviate += val - mean;
53            if cumulative_deviate > max_z {
54                max_z = cumulative_deviate;
55            }
56            if cumulative_deviate < min_z {
57                min_z = cumulative_deviate;
58            }
59        }
60
61        let range = max_z - min_z;
62
63        // 4. Calculate Standard Deviation
64        let mut variance_sum = 0.0;
65        for &val in self.window.iter() {
66            let diff = val - mean;
67            variance_sum += diff * diff;
68        }
69        let std_dev = (variance_sum / self.period as f64).sqrt();
70
71        if std_dev == 0.0 {
72            return 0.5; // Random walk if no variance
73        }
74
75        // 5. Calculate Hurst Exponent
76        // H = log(R/S) / log(N)
77        let rs = range / std_dev;
78        (rs.ln()) / (self.period as f64).ln()
79    }
80}
81
82pub const HURST_EXPONENT_METADATA: IndicatorMetadata = IndicatorMetadata {
83    name: "Hurst Exponent",
84    description: "Measures the persistence or anti-persistence of a time series using R/S analysis.",
85    usage: "Use to classify the current market regime. H > 0.5 suggests a trending market (persistent); H < 0.5 suggests a mean-reverting market (anti-persistent). Useful as a filter for trend-following or mean-reversion strategies.",
86    keywords: &["statistics", "regime-detection", "hurst", "ml", "trending", "mean-reversion"],
87    ehlers_summary: "The Hurst Exponent, pioneered by Harold Edwin Hurst in 1951, quantifies the 'memory' of a time series. In technical analysis, it distinguishes between trending, mean-reverting, and random walk price action. It is a critical feature for machine learning models to adapt their logic to the underlying market structure.",
88    params: &[
89        ParamDef {
90            name: "period",
91            default: "100",
92            description: "Lookback period for R/S analysis",
93        },
94    ],
95    formula_source: "https://en.wikipedia.org/wiki/Hurst_exponent",
96    formula_latex: r#"
97\[
98H = \frac{\ln(R/S)}{\ln(N)}
99\]
100"#,
101    gold_standard_file: "hurst_exponent.json",
102    category: "ML Features",
103};
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::traits::Next;
109    use proptest::prelude::*;
110
111    #[test]
112    fn test_hurst_basic() {
113        let period = 10;
114        let mut hurst = HurstExponent::new(period);
115        // Upward trend should have H > 0.5
116        for i in 0..period {
117            let res = hurst.next(i as f64);
118            if i < period - 1 {
119                assert!(res.is_nan());
120            } else {
121                assert!(res > 0.5);
122            }
123        }
124    }
125
126    proptest! {
127        #[test]
128        fn test_hurst_parity(
129            inputs in prop::collection::vec(1.0..100.0, 50..100),
130        ) {
131            let period = 20;
132            let mut hurst = HurstExponent::new(period);
133            let streaming_results: Vec<f64> = inputs.iter().map(|&x| hurst.next(x)).collect();
134
135            // Reference implementation
136            let mut batch_results = Vec::with_capacity(inputs.len());
137            for i in 0..inputs.len() {
138                if i < period - 1 {
139                    batch_results.push(f64::NAN);
140                    continue;
141                }
142
143                let slice = &inputs[i + 1 - period..=i];
144                let sum: f64 = slice.iter().sum();
145                let mean = sum / period as f64;
146
147                let mut cum_dev = 0.0;
148                let mut max_z = f64::MIN;
149                let mut min_z = f64::MAX;
150                for &val in slice {
151                    cum_dev += val - mean;
152                    if cum_dev > max_z { max_z = cum_dev; }
153                    if cum_dev < min_z { min_z = cum_dev; }
154                }
155                let range = max_z - min_z;
156
157                let var_sum: f64 = slice.iter().map(|&v| (v - mean).powi(2)).sum();
158                let std_dev = (var_sum / period as f64).sqrt();
159
160                let res = if std_dev == 0.0 { 0.5 } else { (range / std_dev).ln() / (period as f64).ln() };
161                batch_results.push(res);
162            }
163
164            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
165                if s.is_nan() {
166                    assert!(b.is_nan());
167                } else {
168                    approx::assert_relative_eq!(s, b, epsilon = 1e-10);
169                }
170            }
171        }
172    }
173}