quantwave_core/indicators/
ehlers_stochastic.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::roofing_filter::RoofingFilter;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6#[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 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}