Skip to main content

quantwave_core/indicators/
ehlers_stochastic.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::roofing_filter::RoofingFilter;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6/// Ehlers Stochastic (MESA Stochastic)
7/// 
8/// Based on John Ehlers' "Anticipating Turning Points".
9/// It is a standard Stochastic calculation applied to the output of a Roofing Filter.
10/// This removes the distortion caused by Spectral Dilation in standard Stochastics.
11#[derive(Debug, Clone)]
12pub struct EhlersStochastic {
13    roof: RoofingFilter,
14    stoch_period: usize,
15    roof_window: VecDeque<f64>,
16}
17
18impl EhlersStochastic {
19    pub fn new(hp_period: usize, ss_period: usize, stoch_period: usize) -> Self {
20        Self {
21            roof: RoofingFilter::new(hp_period, ss_period),
22            stoch_period,
23            roof_window: VecDeque::with_capacity(stoch_period),
24        }
25    }
26}
27
28impl Next<f64> for EhlersStochastic {
29    type Output = f64;
30
31    fn next(&mut self, input: f64) -> Self::Output {
32        let roof_val = self.roof.next(input);
33        self.roof_window.push_front(roof_val);
34        if self.roof_window.len() > self.stoch_period {
35            self.roof_window.pop_back();
36        }
37        
38        let mut min = f64::MAX;
39        let mut max = f64::MIN;
40        for &v in &self.roof_window {
41            if v < min { min = v; }
42            if v > max { max = v; }
43        }
44        
45        if max == min {
46            50.0
47        } else {
48            100.0 * (roof_val - min) / (max - min)
49        }
50    }
51}
52
53pub const EHLERS_STOCHASTIC_METADATA: IndicatorMetadata = IndicatorMetadata {
54    name: "Ehlers Stochastic",
55    description: "A Stochastic oscillator applied to the output of a Roofing Filter to eliminate Spectral Dilation.",
56    params: &[
57        ParamDef { name: "hp_period", default: "48", description: "HighPass critical period" },
58        ParamDef { name: "ss_period", default: "10", description: "SuperSmoother critical period" },
59        ParamDef { name: "stoch_period", default: "20", description: "Stochastic lookback period" },
60    ],
61    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Anticipating Turning Points.pdf",
62    formula_latex: r#"
63\[
64Roof = RoofingFilter(HP, SS)
65\]
66\[
67Stoch = 100 \times \frac{Roof - \min(Roof, L)}{\max(Roof, L) - \min(Roof, L)}
68\]
69"#,
70    gold_standard_file: "ehlers_stochastic.json",
71    category: "Ehlers DSP",
72};
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::traits::Next;
78    use crate::test_utils::{load_gold_standard, assert_indicator_parity};
79    use proptest::prelude::*;
80
81    #[test]
82    fn test_ehlers_stochastic_gold_standard() {
83        let case = load_gold_standard("ehlers_stochastic");
84        let es = EhlersStochastic::new(48, 10, 20);
85        assert_indicator_parity(es, &case.input, &case.expected);
86    }
87
88    #[test]
89    fn test_ehlers_stochastic_basic() {
90        let mut es = EhlersStochastic::new(48, 10, 20);
91        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
92        for input in inputs {
93            let res = es.next(input);
94            assert!(res >= 0.0 && res <= 100.0);
95        }
96    }
97
98    proptest! {
99        #[test]
100        fn test_ehlers_stochastic_parity(
101            inputs in prop::collection::vec(1.0..100.0, 50..100),
102        ) {
103            let hp = 48;
104            let ss = 10;
105            let stoch = 20;
106            let mut es = EhlersStochastic::new(hp, ss, stoch);
107            let streaming_results: Vec<f64> = inputs.iter().map(|&x| es.next(x)).collect();
108            
109            // Batch implementation
110            let mut batch_results = Vec::with_capacity(inputs.len());
111            let mut roof = RoofingFilter::new(hp, ss);
112            let mut roof_vals = Vec::new();
113            
114            for &input in &inputs {
115                let r_val = roof.next(input);
116                roof_vals.push(r_val);
117                
118                let start = if roof_vals.len() > stoch { roof_vals.len() - stoch } else { 0 };
119                let window = &roof_vals[start..];
120                
121                let mut min = f64::MAX;
122                let mut max = f64::MIN;
123                for &v in window {
124                    if v < min { min = v; }
125                    if v > max { max = v; }
126                }
127                
128                let res = if max == min {
129                    50.0
130                } else {
131                    100.0 * (r_val - min) / (max - min)
132                };
133                batch_results.push(res);
134            }
135            
136            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
137                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
138            }
139        }
140    }
141}