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 usage: "Use as a cycle-synchronized stochastic that automatically scales its lookback to the measured dominant cycle period for consistent overbought/oversold signals.",
72 keywords: &["oscillator", "stochastic", "ehlers", "cycle", "adaptive"],
73 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.",
74 params: &[
75 ParamDef { name: "length", default: "20", description: "Stochastic lookback length" },
76 ParamDef { name: "hp_period", default: "48", description: "HighPass critical period" },
77 ParamDef { name: "ss_period", default: "10", description: "SuperSmoother critical period" },
78 ],
79 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Anticipating%20Turning%20Points.pdf",
80 formula_latex: r#"
81\[
82Filt = \text{RoofingFilter}(Price, P_{hp}, P_{ss})
83\]
84\[
85Stoc = \frac{Filt - \min(Filt, L)}{\max(Filt, L) - \min(Filt, L)}
86\]
87\[
88MESAStoch = \text{SuperSmoother}(Stoc \times 100, P_{ss})
89\]
90"#,
91 gold_standard_file: "mesa_stochastic.json",
92 category: "Ehlers DSP",
93};
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::traits::Next;
99 use crate::test_utils::{load_gold_standard, assert_indicator_parity};
100 use proptest::prelude::*;
101
102 #[test]
103 fn test_mesa_stochastic_gold_standard() {
104 let case = load_gold_standard("mesa_stochastic");
105 let ms = MESAStochastic::new(20, 48, 10);
106 assert_indicator_parity(ms, &case.input, &case.expected);
107 }
108
109 #[test]
110 fn test_mesa_stochastic_basic() {
111 let mut ms = MESAStochastic::default();
112 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
113 for input in inputs {
114 let res = ms.next(input);
115 assert!(!res.is_nan());
116 }
117 }
118
119 proptest! {
120 #[test]
121 fn test_mesa_stochastic_parity(
122 inputs in prop::collection::vec(1.0..100.0, 60..120),
123 ) {
124 let length = 20;
125 let hp_period = 48;
126 let ss_period = 10;
127 let mut ms = MESAStochastic::new(length, hp_period, ss_period);
128 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ms.next(x)).collect();
129
130 let mut batch_results = Vec::with_capacity(inputs.len());
132 let mut rf = RoofingFilter::new(hp_period, ss_period);
133 let mut ss = SuperSmoother::new(ss_period);
134 let mut filt_hist = VecDeque::new();
135
136 for &input in &inputs {
137 let filt = rf.next(input);
138 filt_hist.push_front(filt);
139 if filt_hist.len() > length {
140 filt_hist.pop_back();
141 }
142
143 let mut highest_c = f64::NEG_INFINITY;
144 let mut lowest_c = f64::INFINITY;
145 for &val in &filt_hist {
146 if val > highest_c { highest_c = val; }
147 if val < lowest_c { lowest_c = val; }
148 }
149
150 let stoch = if (highest_c - lowest_c).abs() > 1e-10 {
151 (filt - lowest_c) / (highest_c - lowest_c)
152 } else {
153 0.0
154 };
155
156 let res = ss.next(stoch * 100.0);
157 batch_results.push(res);
158 }
159
160 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
161 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
162 }
163 }
164 }
165}