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