quantwave_core/indicators/
mesa_stochastic.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::roofing_filter::RoofingFilter;
4use crate::indicators::super_smoother::SuperSmoother;
5use std::collections::VecDeque;
6
7#[derive(Debug, Clone)]
14pub struct MESAStochastic {
15 roofing_filter: RoofingFilter,
16 stoch_smoother: SuperSmoother,
17 length: usize,
18 filt_history: VecDeque<f64>,
19}
20
21impl MESAStochastic {
22 pub fn new(length: usize, hp_period: usize, ss_period: usize) -> Self {
23 Self {
24 roofing_filter: RoofingFilter::new(hp_period, ss_period),
25 stoch_smoother: SuperSmoother::new(ss_period),
26 length,
27 filt_history: VecDeque::with_capacity(length),
28 }
29 }
30}
31
32impl Default for MESAStochastic {
33 fn default() -> Self {
34 Self::new(20, 48, 10)
35 }
36}
37
38impl Next<f64> for MESAStochastic {
39 type Output = f64;
40
41 fn next(&mut self, input: f64) -> Self::Output {
42 let filt = self.roofing_filter.next(input);
43
44 self.filt_history.push_front(filt);
45 if self.filt_history.len() > self.length {
46 self.filt_history.pop_back();
47 }
48
49 let mut highest_c = f64::NEG_INFINITY;
50 let mut lowest_c = f64::INFINITY;
51
52 for &val in &self.filt_history {
53 if val > highest_c { highest_c = val; }
54 if val < lowest_c { lowest_c = val; }
55 }
56
57 let stoch = if (highest_c - lowest_c).abs() > 1e-10 {
58 (filt - lowest_c) / (highest_c - lowest_c)
59 } else {
60 0.0
61 };
62
63 self.stoch_smoother.next(stoch * 100.0)
65 }
66}
67
68pub const MESA_STOCHASTIC_METADATA: IndicatorMetadata = IndicatorMetadata {
69 name: "MESA Stochastic",
70 description: "Standard Stochastic calculation applied to Roofing Filtered data, followed by SuperSmoothing.",
71 params: &[
72 ParamDef { name: "length", default: "20", description: "Stochastic lookback length" },
73 ParamDef { name: "hp_period", default: "48", description: "HighPass critical period" },
74 ParamDef { name: "ss_period", default: "10", description: "SuperSmoother critical period" },
75 ],
76 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Anticipating%20Turning%20Points.pdf",
77 formula_latex: r#"
78\[
79Filt = \text{RoofingFilter}(Price, P_{hp}, P_{ss})
80\]
81\[
82Stoc = \frac{Filt - \min(Filt, L)}{\max(Filt, L) - \min(Filt, L)}
83\]
84\[
85MESAStoch = \text{SuperSmoother}(Stoc \times 100, P_{ss})
86\]
87"#,
88 gold_standard_file: "mesa_stochastic.json",
89 category: "Ehlers DSP",
90};
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::traits::Next;
96 use crate::test_utils::{load_gold_standard, assert_indicator_parity};
97 use proptest::prelude::*;
98
99 #[test]
100 fn test_mesa_stochastic_gold_standard() {
101 let case = load_gold_standard("mesa_stochastic");
102 let ms = MESAStochastic::new(20, 48, 10);
103 assert_indicator_parity(ms, &case.input, &case.expected);
104 }
105
106 #[test]
107 fn test_mesa_stochastic_basic() {
108 let mut ms = MESAStochastic::default();
109 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
110 for input in inputs {
111 let res = ms.next(input);
112 assert!(!res.is_nan());
113 }
114 }
115
116 proptest! {
117 #[test]
118 fn test_mesa_stochastic_parity(
119 inputs in prop::collection::vec(1.0..100.0, 60..120),
120 ) {
121 let length = 20;
122 let hp_period = 48;
123 let ss_period = 10;
124 let mut ms = MESAStochastic::new(length, hp_period, ss_period);
125 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ms.next(x)).collect();
126
127 let mut batch_results = Vec::with_capacity(inputs.len());
129 let mut rf = RoofingFilter::new(hp_period, ss_period);
130 let mut ss = SuperSmoother::new(ss_period);
131 let mut filt_hist = VecDeque::new();
132
133 for &input in &inputs {
134 let filt = rf.next(input);
135 filt_hist.push_front(filt);
136 if filt_hist.len() > length {
137 filt_hist.pop_back();
138 }
139
140 let mut highest_c = f64::NEG_INFINITY;
141 let mut lowest_c = f64::INFINITY;
142 for &val in &filt_hist {
143 if val > highest_c { highest_c = val; }
144 if val < lowest_c { lowest_c = val; }
145 }
146
147 let stoch = if (highest_c - lowest_c).abs() > 1e-10 {
148 (filt - lowest_c) / (highest_c - lowest_c)
149 } else {
150 0.0
151 };
152
153 let res = ss.next(stoch * 100.0);
154 batch_results.push(res);
155 }
156
157 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
158 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
159 }
160 }
161 }
162}