quantwave_core/indicators/
alma.rs1use 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 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};