quantwave_core/indicators/
mesa_stochastic.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::roofing_filter::RoofingFilter;
3use crate::indicators::super_smoother::SuperSmoother;
4use crate::traits::Next;
5use crate::utils::RingBuffer as 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 {
54 highest_c = val;
55 }
56 if val < lowest_c {
57 lowest_c = val;
58 }
59 }
60
61 let stoch = if (highest_c - lowest_c).abs() > 1e-10 {
62 (filt - lowest_c) / (highest_c - lowest_c)
63 } else {
64 0.0
65 };
66
67 self.stoch_smoother.next(stoch * 100.0)
69 }
70}
71
72pub const MESA_STOCHASTIC_METADATA: IndicatorMetadata = IndicatorMetadata {
73 name: "MESA Stochastic",
74 description: "Standard Stochastic calculation applied to Roofing Filtered data, followed by SuperSmoothing.",
75 usage: "Use as a cycle-synchronized stochastic that automatically scales its lookback to the measured dominant cycle period for consistent overbought/oversold signals.",
76 keywords: &["oscillator", "stochastic", "ehlers", "cycle", "adaptive"],
77 ehlers_summary: "The MESA Stochastic extends Ehlers adaptive stochastic concept by using the MESA-measured dominant cycle period as the lookback window. Unlike traditional stochastics with fixed periods, it adapts to the current market rhythm, keeping the oscillator calibrated to one full cycle at all times.",
78 params: &[
79 ParamDef {
80 name: "length",
81 default: "20",
82 description: "Stochastic lookback length",
83 },
84 ParamDef {
85 name: "hp_period",
86 default: "48",
87 description: "HighPass critical period",
88 },
89 ParamDef {
90 name: "ss_period",
91 default: "10",
92 description: "SuperSmoother critical period",
93 },
94 ],
95 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Anticipating%20Turning%20Points.pdf",
96 formula_latex: r#"
97\[
98Filt = \text{RoofingFilter}(Price, P_{hp}, P_{ss})
99\]
100\[
101Stoc = \frac{Filt - \min(Filt, L)}{\max(Filt, L) - \min(Filt, L)}
102\]
103\[
104MESAStoch = \text{SuperSmoother}(Stoc \times 100, P_{ss})
105\]
106"#,
107 gold_standard_file: "mesa_stochastic.json",
108 category: "Ehlers DSP",
109};
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::test_utils::{assert_indicator_parity, load_gold_standard};
115 use crate::traits::Next;
116 use proptest::prelude::*;
117
118 #[test]
119 fn test_mesa_stochastic_gold_standard() {
120 let case = load_gold_standard("mesa_stochastic");
121 let ms = MESAStochastic::new(20, 48, 10);
122 assert_indicator_parity(ms, &case.input, &case.expected);
123 }
124
125 #[test]
126 fn test_mesa_stochastic_basic() {
127 let mut ms = MESAStochastic::default();
128 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
129 for input in inputs {
130 let res = ms.next(input);
131 assert!(!res.is_nan());
132 }
133 }
134
135 proptest! {
136 #[test]
137 fn test_mesa_stochastic_parity(
138 inputs in prop::collection::vec(1.0..100.0, 60..120),
139 ) {
140 let length = 20;
141 let hp_period = 48;
142 let ss_period = 10;
143 let mut ms = MESAStochastic::new(length, hp_period, ss_period);
144 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ms.next(x)).collect();
145
146 let mut batch_results = Vec::with_capacity(inputs.len());
148 let mut rf = RoofingFilter::new(hp_period, ss_period);
149 let mut ss = SuperSmoother::new(ss_period);
150 let mut filt_hist = VecDeque::with_capacity(40);
151
152 for &input in &inputs {
153 let filt = rf.next(input);
154 filt_hist.push_front(filt);
155 if filt_hist.len() > length {
156 filt_hist.pop_back();
157 }
158
159 let mut highest_c = f64::NEG_INFINITY;
160 let mut lowest_c = f64::INFINITY;
161 for &val in &filt_hist {
162 if val > highest_c { highest_c = val; }
163 if val < lowest_c { lowest_c = val; }
164 }
165
166 let stoch = if (highest_c - lowest_c).abs() > 1e-10 {
167 (filt - lowest_c) / (highest_c - lowest_c)
168 } else {
169 0.0
170 };
171
172 let res = ss.next(stoch * 100.0);
173 batch_results.push(res);
174 }
175
176 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
177 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
178 }
179 }
180 }
181}