Skip to main content

quantwave_core/indicators/
alma.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::WarmupPolicy;
3use crate::traits::Next;
4use crate::utils::RingBuffer as VecDeque;
5
6#[derive(Debug, Clone)]
7pub struct ALMA {
8    period: usize,
9    _offset: f64,
10    _sigma: f64,
11    window: VecDeque<f64>,
12    weights: Vec<f64>,
13    warmup: WarmupPolicy,
14}
15
16impl ALMA {
17    pub fn new(period: usize, offset: f64, sigma: f64) -> Self {
18        Self::with_warmup(period, offset, sigma, WarmupPolicy::Partial)
19    }
20
21    pub fn with_warmup(period: usize, offset: f64, sigma: f64, warmup: WarmupPolicy) -> Self {
22        let m = offset * (period as f64 - 1.0);
23        let s = period as f64 / sigma;
24        let mut weights = Vec::with_capacity(period);
25        let mut sum_w = 0.0;
26
27        for i in 0..period {
28            let weight = (-((i as f64 - m).powi(2) / (2.0 * s.powi(2)))).exp();
29            weights.push(weight);
30            sum_w += weight;
31        }
32
33        // Normalize weights
34        for w in weights.iter_mut() {
35            *w /= sum_w;
36        }
37
38        Self {
39            period,
40            _offset: offset,
41            _sigma: sigma,
42            window: VecDeque::with_capacity(period),
43            weights,
44            warmup,
45        }
46    }
47}
48
49impl Next<f64> for ALMA {
50    type Output = f64;
51
52    fn next(&mut self, input: f64) -> Self::Output {
53        if input.is_nan() {
54            return f64::NAN;
55        }
56        self.window.push_back(input);
57        if self.window.len() > self.period {
58            self.window.pop_front();
59        }
60
61        if self.window.len() < self.period {
62            if self.warmup == WarmupPolicy::Nan {
63                return f64::NAN;
64            }
65            let mut sum_w = 0.0;
66            let mut weighted_val_sum = 0.0;
67            for (i, &val) in self.window.iter().enumerate() {
68                let weight = self.weights[i + self.period - self.window.len()];
69                weighted_val_sum += val * weight;
70                sum_w += weight;
71            }
72            if sum_w == 0.0 {
73                0.0
74            } else {
75                weighted_val_sum / sum_w
76            }
77        } else {
78            let mut weighted_val_sum = 0.0;
79            for (i, &val) in self.window.iter().enumerate() {
80                weighted_val_sum += val * self.weights[i];
81            }
82            weighted_val_sum
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::test_utils::{
91        assert_indicator_parity, check_batch_streaming_parity, load_gold_standard,
92    };
93    use proptest::prelude::*;
94
95    #[test]
96    fn test_alma_gold_standard() {
97        let case = load_gold_standard("alma_9_085_6");
98        let alma = ALMA::new(9, 0.85, 6.0);
99        assert_indicator_parity(alma, &case.input, &case.expected);
100    }
101
102    fn alma_batch(data: Vec<f64>, period: usize, offset: f64, sigma: f64) -> Vec<f64> {
103        let mut alma = ALMA::new(period, offset, sigma);
104        data.into_iter().map(|x| alma.next(x)).collect()
105    }
106
107    proptest! {
108        #[test]
109        fn test_alma_parity(input in prop::collection::vec(0.0..1000.0, 1..100)) {
110            let period = 9;
111            let offset = 0.85;
112            let sigma = 6.0;
113            let indicator = ALMA::new(period, offset, sigma);
114            check_batch_streaming_parity(input, indicator, |data| alma_batch(data, period, offset, sigma));
115        }
116    }
117
118    #[test]
119    fn test_alma_basic() {
120        let mut alma = ALMA::new(9, 0.85, 6.0);
121        for i in 1..20 {
122            let val = alma.next(i as f64);
123            if i >= 9 {
124                assert!(val > 0.0);
125            }
126        }
127    }
128
129    #[test]
130    fn test_alma_warmup_nan() {
131        let mut alma = ALMA::with_warmup(5, 0.85, 6.0, WarmupPolicy::Nan);
132        for i in 1..5 {
133            assert!(alma.next(i as f64).is_nan());
134        }
135        let val5 = alma.next(5.0);
136        assert!(!val5.is_nan());
137        assert!(val5 > 0.0);
138    }
139
140    #[test]
141    fn test_alma_nan_input_recovery() {
142        let mut alma = ALMA::new(5, 0.85, 6.0);
143        for i in 1..5 {
144            alma.next(i as f64);
145        }
146        assert!(alma.next(f64::NAN).is_nan());
147        // Ring buffer flushes the NaN after 5 bars
148        for _ in 0..5 {
149            alma.next(10.0);
150        }
151        let clean = alma.next(10.0);
152        assert!(!clean.is_nan());
153        assert!((clean - 10.0).abs() < 1e-4);
154    }
155}
156
157pub const ALMA_METADATA: IndicatorMetadata = IndicatorMetadata {
158    name: "Arnaud Legoux Moving Average",
159    description: "ALMA is designed to reduce lag while providing high smoothness. During warmup (< period bars), partial-window Gaussian weights are normalized over available bars to emit finite values immediately from bar 0.",
160    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. Supports WarmupPolicy (Partial default, or Nan).",
161    keywords: &["moving-average", "smoothing", "low-latency", "adaptive"],
162    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.",
163    params: &[
164        ParamDef {
165            name: "period",
166            default: "9",
167            description: "Period",
168        },
169        ParamDef {
170            name: "offset",
171            default: "0.85",
172            description: "Offset",
173        },
174        ParamDef {
175            name: "sigma",
176            default: "6.0",
177            description: "Sigma",
178        },
179    ],
180    formula_source: "https://www.prorealcode.com/prorealtime-indicators/arnaud-legoux-moving-average-alma/",
181    formula_latex: r#"
182\[
183ALMA = \sum (W_i \times P_i) / \sum W_i
184\]
185"#,
186    gold_standard_file: "alma.json",
187    category: "Classic",
188};