wickra_core/indicators/
alma.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
45pub struct Alma {
46 period: usize,
47 offset: f64,
48 sigma: f64,
49 weights: Vec<f64>,
52 window: VecDeque<f64>,
53 current: Option<f64>,
54}
55
56impl Alma {
57 pub fn new(period: usize, offset: f64, sigma: f64) -> Result<Self> {
65 if period == 0 {
66 return Err(Error::PeriodZero);
67 }
68 if !offset.is_finite() || !(0.0..=1.0).contains(&offset) {
69 return Err(Error::InvalidPeriod {
70 message: "ALMA offset must be a finite value in [0, 1]",
71 });
72 }
73 if !sigma.is_finite() || sigma <= 0.0 {
74 return Err(Error::InvalidPeriod {
75 message: "ALMA sigma must be a finite positive value",
76 });
77 }
78 let m = offset * (period as f64 - 1.0);
79 let s = period as f64 / sigma;
80 let denom = 2.0 * s * s;
81 let mut raw: Vec<f64> = (0..period)
85 .map(|i| (-((i as f64 - m).powi(2)) / denom).exp())
86 .collect();
87 let sum: f64 = raw.iter().sum();
88 for w in &mut raw {
89 *w /= sum;
90 }
91 Ok(Self {
92 period,
93 offset,
94 sigma,
95 weights: raw,
96 window: VecDeque::with_capacity(period),
97 current: None,
98 })
99 }
100
101 pub fn classic() -> Self {
104 Self::new(9, 0.85, 6.0).expect("classic ALMA parameters are valid")
105 }
106
107 pub const fn period(&self) -> usize {
109 self.period
110 }
111
112 pub const fn offset(&self) -> f64 {
114 self.offset
115 }
116
117 pub const fn sigma(&self) -> f64 {
119 self.sigma
120 }
121}
122
123impl Indicator for Alma {
124 type Input = f64;
125 type Output = f64;
126
127 fn update(&mut self, input: f64) -> Option<f64> {
128 if !input.is_finite() {
129 return self.current;
130 }
131 if self.window.len() == self.period {
132 self.window.pop_front();
133 }
134 self.window.push_back(input);
135 if self.window.len() < self.period {
136 return None;
137 }
138 let mut acc = 0.0;
139 for (w, p) in self.weights.iter().zip(self.window.iter()) {
140 acc += w * p;
141 }
142 self.current = Some(acc);
143 Some(acc)
144 }
145
146 fn reset(&mut self) {
147 self.window.clear();
148 self.current = None;
149 }
150
151 fn warmup_period(&self) -> usize {
152 self.period
153 }
154
155 fn is_ready(&self) -> bool {
156 self.current.is_some()
157 }
158
159 fn name(&self) -> &'static str {
160 "ALMA"
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::traits::BatchExt;
168 use approx::assert_relative_eq;
169
170 #[test]
171 fn rejects_zero_period() {
172 assert!(matches!(Alma::new(0, 0.85, 6.0), Err(Error::PeriodZero)));
173 }
174
175 #[test]
176 fn rejects_invalid_offset() {
177 assert!(matches!(
178 Alma::new(9, -0.1, 6.0),
179 Err(Error::InvalidPeriod { .. })
180 ));
181 assert!(matches!(
182 Alma::new(9, 1.1, 6.0),
183 Err(Error::InvalidPeriod { .. })
184 ));
185 assert!(matches!(
186 Alma::new(9, f64::NAN, 6.0),
187 Err(Error::InvalidPeriod { .. })
188 ));
189 }
190
191 #[test]
192 fn rejects_invalid_sigma() {
193 assert!(matches!(
194 Alma::new(9, 0.85, 0.0),
195 Err(Error::InvalidPeriod { .. })
196 ));
197 assert!(matches!(
198 Alma::new(9, 0.85, -1.0),
199 Err(Error::InvalidPeriod { .. })
200 ));
201 assert!(matches!(
202 Alma::new(9, 0.85, f64::INFINITY),
203 Err(Error::InvalidPeriod { .. })
204 ));
205 }
206
207 #[test]
208 fn accessors_and_metadata() {
209 let alma = Alma::new(9, 0.85, 6.0).unwrap();
210 assert_eq!(alma.period(), 9);
211 assert_eq!(alma.warmup_period(), 9);
212 assert_eq!(alma.name(), "ALMA");
213 assert!((alma.offset() - 0.85).abs() < 1e-12);
214 assert!((alma.sigma() - 6.0).abs() < 1e-12);
215 let sum: f64 = alma.weights.iter().sum();
217 assert_relative_eq!(sum, 1.0, epsilon = 1e-12);
218 }
219
220 #[test]
221 fn classic_factory() {
222 let a = Alma::classic();
223 assert_eq!(a.period(), 9);
224 assert!((a.offset() - 0.85).abs() < 1e-12);
225 assert!((a.sigma() - 6.0).abs() < 1e-12);
226 }
227
228 #[test]
229 fn constant_series_yields_the_constant() {
230 let mut alma = Alma::new(9, 0.85, 6.0).unwrap();
232 let out = alma.batch(&[42.0_f64; 40]);
233 for v in out.iter().skip(8).flatten() {
234 assert_relative_eq!(*v, 42.0, epsilon = 1e-12);
235 }
236 }
237
238 #[test]
239 fn warmup_emits_first_value_at_period() {
240 let mut alma = Alma::new(5, 0.85, 6.0).unwrap();
241 for i in 0..4 {
242 assert_eq!(alma.update(f64::from(i)), None);
243 }
244 assert!(alma.update(4.0).is_some());
245 }
246
247 #[test]
248 fn reference_value_period_3() {
249 let mut alma = Alma::new(3, 0.85, 6.0).unwrap();
256 alma.update(10.0);
257 alma.update(20.0);
258 let v = alma.update(30.0).expect("ALMA emits after period");
259
260 let w0 = (-((0.0_f64 - 1.7).powi(2)) / 0.5).exp();
261 let w1 = (-((1.0_f64 - 1.7).powi(2)) / 0.5).exp();
262 let w2 = (-((2.0_f64 - 1.7).powi(2)) / 0.5).exp();
263 let s = w0 + w1 + w2;
264 let expected = (10.0 * w0 + 20.0 * w1 + 30.0 * w2) / s;
265
266 assert!(v > 25.0 && v < 30.0, "ALMA(3) on [10,20,30] = {v}");
269 assert_relative_eq!(v, expected, epsilon = 1e-12);
270 }
271
272 #[test]
273 fn offset_zero_centres_on_oldest_sample() {
274 let mut alma = Alma::new(5, 0.0, 6.0).unwrap();
277 let series: Vec<f64> = (1..=5).map(f64::from).collect();
278 let mut last = None;
279 for p in &series {
280 last = alma.update(*p);
281 }
282 let v = last.unwrap();
283 let mean = series.iter().sum::<f64>() / series.len() as f64;
284 assert!(v < mean, "{v} should be less than {mean}");
287 }
288
289 #[test]
290 fn offset_one_centres_on_newest_sample() {
291 let mut alma = Alma::new(5, 1.0, 6.0).unwrap();
293 let series: Vec<f64> = (1..=5).map(f64::from).collect();
294 let mut last = None;
295 for p in &series {
296 last = alma.update(*p);
297 }
298 let v = last.unwrap();
299 let mean = series.iter().sum::<f64>() / series.len() as f64;
300 assert!(v > mean, "{v} should exceed {mean}");
301 }
302
303 #[test]
304 fn batch_equals_streaming() {
305 let prices: Vec<f64> = (1..=100)
306 .map(|i| (f64::from(i) * 0.2).sin() * 5.0 + f64::from(i) * 0.1)
307 .collect();
308 let mut a = Alma::new(9, 0.85, 6.0).unwrap();
309 let mut b = Alma::new(9, 0.85, 6.0).unwrap();
310 assert_eq!(
311 a.batch(&prices),
312 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
313 );
314 }
315
316 #[test]
317 fn reset_clears_state() {
318 let mut alma = Alma::new(9, 0.85, 6.0).unwrap();
319 alma.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
320 assert!(alma.is_ready());
321 alma.reset();
322 assert!(!alma.is_ready());
323 assert_eq!(alma.update(1.0), None);
324 }
325
326 #[test]
327 fn ignores_non_finite_input() {
328 let mut alma = Alma::new(5, 0.85, 6.0).unwrap();
329 alma.batch(&(1..=5).map(f64::from).collect::<Vec<_>>());
330 let before = alma.update(6.0).unwrap();
331 assert_eq!(alma.update(f64::NAN), Some(before));
333 assert_eq!(alma.update(f64::INFINITY), Some(before));
334 }
335}