use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use crate::indicators::roofing_filter::RoofingFilter;
use crate::indicators::super_smoother::SuperSmoother;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct MESAStochastic {
roofing_filter: RoofingFilter,
stoch_smoother: SuperSmoother,
length: usize,
filt_history: VecDeque<f64>,
}
impl MESAStochastic {
pub fn new(length: usize, hp_period: usize, ss_period: usize) -> Self {
Self {
roofing_filter: RoofingFilter::new(hp_period, ss_period),
stoch_smoother: SuperSmoother::new(ss_period),
length,
filt_history: VecDeque::with_capacity(length),
}
}
}
impl Default for MESAStochastic {
fn default() -> Self {
Self::new(20, 48, 10)
}
}
impl Next<f64> for MESAStochastic {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
let filt = self.roofing_filter.next(input);
self.filt_history.push_front(filt);
if self.filt_history.len() > self.length {
self.filt_history.pop_back();
}
let mut highest_c = f64::NEG_INFINITY;
let mut lowest_c = f64::INFINITY;
for &val in &self.filt_history {
if val > highest_c { highest_c = val; }
if val < lowest_c { lowest_c = val; }
}
let stoch = if (highest_c - lowest_c).abs() > 1e-10 {
(filt - lowest_c) / (highest_c - lowest_c)
} else {
0.0
};
self.stoch_smoother.next(stoch * 100.0)
}
}
pub const MESA_STOCHASTIC_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "MESA Stochastic",
description: "Standard Stochastic calculation applied to Roofing Filtered data, followed by SuperSmoothing.",
usage: "Use as a cycle-synchronized stochastic that automatically scales its lookback to the measured dominant cycle period for consistent overbought/oversold signals.",
keywords: &["oscillator", "stochastic", "ehlers", "cycle", "adaptive"],
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.",
params: &[
ParamDef { name: "length", default: "20", description: "Stochastic lookback length" },
ParamDef { name: "hp_period", default: "48", description: "HighPass critical period" },
ParamDef { name: "ss_period", default: "10", description: "SuperSmoother critical period" },
],
formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Anticipating%20Turning%20Points.pdf",
formula_latex: r#"
\[
Filt = \text{RoofingFilter}(Price, P_{hp}, P_{ss})
\]
\[
Stoc = \frac{Filt - \min(Filt, L)}{\max(Filt, L) - \min(Filt, L)}
\]
\[
MESAStoch = \text{SuperSmoother}(Stoc \times 100, P_{ss})
\]
"#,
gold_standard_file: "mesa_stochastic.json",
category: "Ehlers DSP",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use crate::test_utils::{load_gold_standard, assert_indicator_parity};
use proptest::prelude::*;
#[test]
fn test_mesa_stochastic_gold_standard() {
let case = load_gold_standard("mesa_stochastic");
let ms = MESAStochastic::new(20, 48, 10);
assert_indicator_parity(ms, &case.input, &case.expected);
}
#[test]
fn test_mesa_stochastic_basic() {
let mut ms = MESAStochastic::default();
let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
for input in inputs {
let res = ms.next(input);
assert!(!res.is_nan());
}
}
proptest! {
#[test]
fn test_mesa_stochastic_parity(
inputs in prop::collection::vec(1.0..100.0, 60..120),
) {
let length = 20;
let hp_period = 48;
let ss_period = 10;
let mut ms = MESAStochastic::new(length, hp_period, ss_period);
let streaming_results: Vec<f64> = inputs.iter().map(|&x| ms.next(x)).collect();
let mut batch_results = Vec::with_capacity(inputs.len());
let mut rf = RoofingFilter::new(hp_period, ss_period);
let mut ss = SuperSmoother::new(ss_period);
let mut filt_hist = VecDeque::new();
for &input in &inputs {
let filt = rf.next(input);
filt_hist.push_front(filt);
if filt_hist.len() > length {
filt_hist.pop_back();
}
let mut highest_c = f64::NEG_INFINITY;
let mut lowest_c = f64::INFINITY;
for &val in &filt_hist {
if val > highest_c { highest_c = val; }
if val < lowest_c { lowest_c = val; }
}
let stoch = if (highest_c - lowest_c).abs() > 1e-10 {
(filt - lowest_c) / (highest_c - lowest_c)
} else {
0.0
};
let res = ss.next(stoch * 100.0);
batch_results.push(res);
}
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s, b, epsilon = 1e-10);
}
}
}
}