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 params: &[
121 ParamDef {
122 name: "period",
123 default: "9",
124 description: "Period",
125 },
126 ParamDef {
127 name: "offset",
128 default: "0.85",
129 description: "Offset",
130 },
131 ParamDef {
132 name: "sigma",
133 default: "6.0",
134 description: "Sigma",
135 },
136 ],
137 formula_source: "https://www.prorealcode.com/prorealtime-indicators/arnaud-legoux-moving-average-alma/",
138 formula_latex: r#"
139\[
140ALMA = \sum (W_i \times P_i) / \sum W_i
141\]
142"#,
143 gold_standard_file: "alma.json",
144 category: "Classic",
145};