Skip to main content

quantwave_core/indicators/
alma.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
6pub struct ALMA {
7    period: usize,
8    _offset: f64,
9    _sigma: f64,
10    window: VecDeque<f64>,
11    weights: Vec<f64>,
12}
13
14impl ALMA {
15    pub fn new(period: usize, offset: f64, sigma: f64) -> Self {
16        let m = offset * (period as f64 - 1.0);
17        let s = period as f64 / sigma;
18        let mut weights = Vec::with_capacity(period);
19        let mut sum_w = 0.0;
20
21        for i in 0..period {
22            let weight = (-((i as f64 - m).powi(2) / (2.0 * s.powi(2)))).exp();
23            weights.push(weight);
24            sum_w += weight;
25        }
26
27        // Normalize weights
28        for w in weights.iter_mut() {
29            *w /= sum_w;
30        }
31
32        Self {
33            period,
34            _offset: offset,
35            _sigma: sigma,
36            window: VecDeque::with_capacity(period),
37            weights,
38        }
39    }
40}
41
42impl Next<f64> for ALMA {
43    type Output = f64;
44
45    fn next(&mut self, input: f64) -> Self::Output {
46        self.window.push_back(input);
47        if self.window.len() > self.period {
48            self.window.pop_front();
49        }
50
51        if self.window.len() < self.period {
52            let mut sum_w = 0.0;
53            let mut weighted_val_sum = 0.0;
54            for (i, &val) in self.window.iter().enumerate() {
55                let weight = self.weights[i + self.period - self.window.len()];
56                weighted_val_sum += val * weight;
57                sum_w += weight;
58            }
59            if sum_w == 0.0 {
60                0.0
61            } else {
62                weighted_val_sum / sum_w
63            }
64        } else {
65            let mut weighted_val_sum = 0.0;
66            for (i, &val) in self.window.iter().enumerate() {
67                weighted_val_sum += val * self.weights[i];
68            }
69            weighted_val_sum
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::test_utils::{
78        assert_indicator_parity, check_batch_streaming_parity, load_gold_standard,
79    };
80    use proptest::prelude::*;
81
82    #[test]
83    fn test_alma_gold_standard() {
84        let case = load_gold_standard("alma_9_085_6");
85        let alma = ALMA::new(9, 0.85, 6.0);
86        assert_indicator_parity(alma, &case.input, &case.expected);
87    }
88
89    fn alma_batch(data: Vec<f64>, period: usize, offset: f64, sigma: f64) -> Vec<f64> {
90        let mut alma = ALMA::new(period, offset, sigma);
91        data.into_iter().map(|x| alma.next(x)).collect()
92    }
93
94    proptest! {
95        #[test]
96        fn test_alma_parity(input in prop::collection::vec(0.0..1000.0, 1..100)) {
97            let period = 9;
98            let offset = 0.85;
99            let sigma = 6.0;
100            let indicator = ALMA::new(period, offset, sigma);
101            check_batch_streaming_parity(input, indicator, |data| alma_batch(data, period, offset, sigma));
102        }
103    }
104
105    #[test]
106    fn test_alma_basic() {
107        let mut alma = ALMA::new(9, 0.85, 6.0);
108        for i in 1..20 {
109            let val = alma.next(i as f64);
110            if i >= 9 {
111                assert!(val > 0.0);
112            }
113        }
114    }
115}
116
117pub const ALMA_METADATA: IndicatorMetadata = IndicatorMetadata {
118    name: "Arnaud Legoux Moving Average",
119    description: "ALMA is designed to reduce lag while providing high smoothness.",
120    usage: "Use as a low-latency moving average that reduces lag compared to EMA while controlling overshoot through the Gaussian offset parameter. Well-suited for momentum systems.",
121    keywords: &["moving-average", "smoothing", "low-latency", "adaptive"],
122    ehlers_summary: "The Arnaud Legoux Moving Average applies a Gaussian-shaped weight distribution offset toward the recent end of the lookback window. The sigma parameter controls weight spread and the offset parameter controls how far the Gaussian peak is positioned from the current bar, enabling a lag-accuracy trade-off unavailable in standard MAs.",
123    params: &[
124        ParamDef {
125            name: "period",
126            default: "9",
127            description: "Period",
128        },
129        ParamDef {
130            name: "offset",
131            default: "0.85",
132            description: "Offset",
133        },
134        ParamDef {
135            name: "sigma",
136            default: "6.0",
137            description: "Sigma",
138        },
139    ],
140    formula_source: "https://www.prorealcode.com/prorealtime-indicators/arnaud-legoux-moving-average-alma/",
141    formula_latex: r#"
142\[
143ALMA = \sum (W_i \times P_i) / \sum W_i
144\]
145"#,
146    gold_standard_file: "alma.json",
147    category: "Classic",
148};