Skip to main content

quantwave_core/indicators/
mesa_stochastic.rs

1use 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/// 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 { 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        // Multiplied by 100 to match the 20/80 levels in the paper
64        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            // Batch implementation
131            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}