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    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            // Batch implementation
128            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}