Skip to main content

quantwave_core/indicators/
cycle_trend_analytics.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4
5/// Cycle/Trend Analytics Indicator
6///
7/// Based on John Ehlers' "Cycle/Trend Analytics And The MAD Indicator" (2021).
8/// It computes a series of oscillators: Price - SMA(Price, Length) for Length 5 to 30.
9#[derive(Debug, Clone)]
10pub struct CycleTrendAnalytics {
11    smas: Vec<SMA>,
12}
13
14impl CycleTrendAnalytics {
15    pub fn new(min_length: usize, max_length: usize) -> Self {
16        let smas = (min_length..=max_length).map(SMA::new).collect();
17        Self {
18            smas,
19        }
20    }
21}
22
23impl Next<f64> for CycleTrendAnalytics {
24    type Output = Vec<f64>; // Price - SMA for each length from min to max
25
26    fn next(&mut self, input: f64) -> Self::Output {
27        self.smas.iter_mut().map(|sma| input - sma.next(input)).collect()
28    }
29}
30
31pub const CYCLE_TREND_ANALYTICS_METADATA: IndicatorMetadata = IndicatorMetadata {
32    name: "Cycle/Trend Analytics",
33    description: "A set of oscillators (Price - SMA) with lengths from 5 to 30 used to visualize cycles and trends.",
34    params: &[
35        ParamDef {
36            name: "min_length",
37            default: "5",
38            description: "Minimum SMA length",
39        },
40        ParamDef {
41            name: "max_length",
42            default: "30",
43            description: "Maximum SMA length",
44        },
45    ],
46    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - OCTOBER 2021.html",
47    formula_latex: r#"
48\[
49Osc(L) = Price - SMA(Price, L) \quad \text{for } L \in [min, max]
50\]
51"#,
52    gold_standard_file: "cycle_trend_analytics.json",
53    category: "Ehlers DSP",
54};
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::traits::Next;
60    use crate::test_utils::{load_gold_standard_vec, assert_indicator_parity_vec};
61    use proptest::prelude::*;
62
63    #[test]
64    fn test_cycle_trend_analytics_gold_standard() {
65        let case = load_gold_standard_vec("cycle_trend_analytics");
66        let cta = CycleTrendAnalytics::new(5, 15);
67        assert_indicator_parity_vec(cta, &case.input, &case.expected);
68    }
69
70    #[test]
71    fn test_cycle_trend_analytics_basic() {
72        let mut cta = CycleTrendAnalytics::new(5, 10);
73        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
74        for input in inputs {
75            let res = cta.next(input);
76            assert_eq!(res.len(), 6);
77        }
78    }
79
80    proptest! {
81        #[test]
82        fn test_cycle_trend_analytics_parity(
83            inputs in prop::collection::vec(1.0..100.0, 30..100),
84        ) {
85            let min = 5;
86            let max = 15;
87            let mut cta = CycleTrendAnalytics::new(min, max);
88            let streaming_results: Vec<Vec<f64>> = inputs.iter().map(|&x| cta.next(x)).collect();
89
90            // Batch implementation
91            let mut batch_results = Vec::with_capacity(inputs.len());
92            for i in 0..inputs.len() {
93                let mut bar_results = Vec::with_capacity(max - min + 1);
94                for length in min..=max {
95                    let sum: f64 = inputs[(i.saturating_sub(length - 1))..=i].iter().sum();
96                    let count = (i + 1).min(length);
97                    let sma = sum / count as f64;
98                    bar_results.push(inputs[i] - sma);
99                }
100                batch_results.push(bar_results);
101            }
102
103            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
104                for (sv, bv) in s.iter().zip(b.iter()) {
105                    approx::assert_relative_eq!(sv, bv, epsilon = 1e-10);
106                }
107            }
108        }
109    }
110}