egui_charts/studies/builtin/
alma.rs1use super::clamp_param;
26use crate::model::Bar;
27use crate::studies::palette;
28use crate::studies::stepper::update_window;
29use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
30use egui::Color32;
31
32#[derive(Clone)]
34pub struct ALMA {
35 period: usize,
36 offset: f64,
37 sigma: f64,
38 values: IndicatorValues,
39 color: Color32,
40 visible: bool,
41}
42
43impl ALMA {
44 pub fn new(period: usize, offset: f64, sigma: f64) -> Self {
52 let period = period.max(1);
53 let offset = clamp_param(offset, 0.0, 1.0);
54 let sigma = clamp_param(sigma, 0.001, 100.0);
55
56 Self {
57 period,
58 offset,
59 sigma,
60 values: IndicatorValues::new(),
61 color: palette::LIGHT_BLUE,
62 visible: true,
63 }
64 }
65
66 pub fn default_params() -> Self {
68 Self::new(9, 0.85, 6.0)
69 }
70
71 pub fn with_color(mut self, color: Color32) -> Self {
73 self.color = color;
74 self
75 }
76}
77
78impl Default for ALMA {
80 fn default() -> Self {
81 Self::new(9, 0.85, 6.0)
82 }
83}
84
85impl ALMA {
86 fn compute(&self, data: &[Bar]) -> IndicatorValues {
87 let mut values = IndicatorValues::new();
88
89 if data.len() < self.period {
90 values.resize(data.len(), IndicatorValue::None);
91 return values;
92 }
93
94 let m = self.offset * (self.period - 1) as f64;
96 let s = self.period as f64 / self.sigma;
97 let weights: Vec<f64> = (0..self.period)
98 .map(|i| (-((i as f64 - m).powi(2)) / (2.0 * s * s)).exp())
99 .collect();
100 let weight_sum: f64 = weights.iter().sum();
101
102 for i in 0..data.len() {
103 if i + 1 < self.period {
104 values.push(IndicatorValue::None);
105 } else {
106 let mut weighted_sum = 0.0;
107 for j in 0..self.period {
108 let idx = i + 1 - self.period + j;
109 weighted_sum += weights[j] * data[idx].close;
110 }
111 let alma = weighted_sum / weight_sum;
112 values.push(IndicatorValue::Single(alma));
113 }
114 }
115 values
116 }
117}
118
119impl Indicator for ALMA {
120 fn source(&self) -> Option<crate::studies::StudySource> {
121 Some(crate::studies::StudySource::Price(
122 crate::model::PriceSource::Close,
123 ))
124 }
125
126 crate::studies::inputs::study_inputs!(self;
127 period: length("Period", 1, usize::MAX),
128 offset: number("Offset", 0.0, 1.0),
129 sigma: number("Sigma", 0.001, 100.0)
130 ; {});
131
132 fn name(&self) -> &str {
133 "ALMA"
134 }
135
136 fn desc(&self) -> &str {
137 "Arnaud Legoux Moving Avg - Gaussian weighted moving avg"
138 }
139
140 fn calculate(&mut self, data: &[Bar]) {
141 self.update(data, 0);
142 }
143
144 fn update(&mut self, data: &[Bar], from: usize) {
145 let mut values = std::mem::take(&mut self.values);
146 update_window(
147 &mut values,
148 data,
149 from,
150 |_data, from| from.saturating_sub(self.period - 1),
151 |data| self.compute(data),
152 );
153 self.values = values;
154 }
155
156 fn is_incremental(&self) -> bool {
157 true
158 }
159
160 fn values(&self) -> &IndicatorValues {
161 &self.values
162 }
163
164 fn colors(&self) -> Vec<Color32> {
165 vec![self.color]
166 }
167
168 fn set_colors(&mut self, colors: Vec<Color32>) {
169 if !colors.is_empty() {
170 self.color = colors[0];
171 }
172 }
173
174 fn is_overlay(&self) -> bool {
175 true
176 }
177
178 fn line_count(&self) -> usize {
179 1
180 }
181
182 fn is_visible(&self) -> bool {
183 self.visible
184 }
185
186 fn set_visible(&mut self, visible: bool) {
187 self.visible = visible;
188 }
189
190 fn clone_box(&self) -> Box<dyn Indicator> {
191 Box::new(self.clone())
192 }
193
194 fn line_names(&self) -> Vec<String> {
195 vec![format!(
196 "ALMA({}, {}, {})",
197 self.period, self.offset, self.sigma
198 )]
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use chrono::{Duration, Utc};
206
207 fn create_test_bars() -> Vec<Bar> {
208 let start = Utc::now();
209 (0..30)
210 .map(|i| {
211 let price = 100.0 + i as f64;
212 Bar {
213 time: start + Duration::minutes(i * 5),
214 open: price,
215 high: price + 1.0,
216 low: price - 1.0,
217 close: price,
218 volume: 1000.0,
219 colors: None,
220 payload: None,
221 }
222 })
223 .collect()
224 }
225
226 #[test]
227 fn test_alma_calculation() {
228 let bars = create_test_bars();
229 let mut alma = ALMA::new(9, 0.85, 6.0);
230 alma.calculate(&bars);
231
232 assert_eq!(alma.values().len(), bars.len());
233
234 for i in 0..8 {
236 assert!(matches!(alma.values().to_rows()[i], IndicatorValue::None));
237 }
238
239 for value in alma.values().to_rows().iter().skip(8) {
241 assert!(matches!(value, IndicatorValue::Single(_)));
242 }
243 }
244}