quantwave_core/indicators/
hurst.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as 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: &[
87 "statistics",
88 "regime-detection",
89 "hurst",
90 "ml",
91 "trending",
92 "mean-reversion",
93 ],
94 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.",
95 params: &[ParamDef {
96 name: "period",
97 default: "100",
98 description: "Lookback period for R/S analysis",
99 }],
100 formula_source: "https://en.wikipedia.org/wiki/Hurst_exponent",
101 formula_latex: r#"
102\[
103H = \frac{\ln(R/S)}{\ln(N)}
104\]
105"#,
106 gold_standard_file: "hurst_exponent.json",
107 category: "ML Features",
108};
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::traits::Next;
114 use proptest::prelude::*;
115
116 #[test]
117 fn test_hurst_basic() {
118 let period = 10;
119 let mut hurst = HurstExponent::new(period);
120 for i in 0..period {
122 let res = hurst.next(i as f64);
123 if i < period - 1 {
124 assert!(res.is_nan());
125 } else {
126 assert!(res > 0.5);
127 }
128 }
129 }
130
131 proptest! {
132 #[test]
133 fn test_hurst_parity(
134 inputs in prop::collection::vec(1.0..100.0, 50..100),
135 ) {
136 let period = 20;
137 let mut hurst = HurstExponent::new(period);
138 let streaming_results: Vec<f64> = inputs.iter().map(|&x| hurst.next(x)).collect();
139
140 let mut batch_results = Vec::with_capacity(inputs.len());
142 for i in 0..inputs.len() {
143 if i < period - 1 {
144 batch_results.push(f64::NAN);
145 continue;
146 }
147
148 let slice = &inputs[i + 1 - period..=i];
149 let sum: f64 = slice.iter().sum();
150 let mean = sum / period as f64;
151
152 let mut cum_dev = 0.0;
153 let mut max_z = f64::MIN;
154 let mut min_z = f64::MAX;
155 for &val in slice {
156 cum_dev += val - mean;
157 if cum_dev > max_z { max_z = cum_dev; }
158 if cum_dev < min_z { min_z = cum_dev; }
159 }
160 let range = max_z - min_z;
161
162 let var_sum: f64 = slice.iter().map(|&v| (v - mean).powi(2)).sum();
163 let std_dev = (var_sum / period as f64).sqrt();
164
165 let res = if std_dev == 0.0 { 0.5 } else { (range / std_dev).ln() / (period as f64).ln() };
166 batch_results.push(res);
167 }
168
169 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
170 if s.is_nan() {
171 assert!(b.is_nan());
172 } else {
173 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
174 }
175 }
176 }
177 }
178}