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