Skip to main content

quantwave_core/indicators/
mesa_stochastic.rs

1use 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/// MESA Stochastic
8///
9/// Based on John Ehlers' "Predictive and Successful Indicators" (2014)
10/// and "Anticipating Turning Points".
11/// It applies a standard Stochastic formula to price data preprocessed by
12/// a Roofing Filter, followed by a SuperSmoother filter.
13#[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        // Multiplied by 100 to match the 20/80 levels in the paper
68        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            // Batch implementation
147            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}