quantwave_core/indicators/
hurst.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[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 let sum: f64 = self.window.iter().sum();
43 let mean = sum / self.period as f64;
44
45 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 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; }
74
75 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 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 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}