Skip to main content

egui_charts/studies/builtin/
alma.rs

1//! Arnaud Legoux Moving Avg (ALMA) Indicator
2//!
3//! ALMA uses a Gaussian distribution to weight prices, providing a smooth
4//! moving avg with reduced lag. The offset param controls the
5//! weight distribution, and sigma controls the smoothness.
6//!
7//! # Formula
8//! ALMA = Σ(weight[i] * price[i]) / Σ(weight[i])
9//! Where weight[i] = exp(-((i - offset * (period - 1))^2) / (2 * sigma^2 * period^2))
10//!
11//! # Params
12//! - period: Lookback period (typically 9)
13//! - offset: Weight offset 0-1 (typically 0.85, higher = more weight to recent)
14//! - sigma: Gaussian sigma (typically 6, higher = smoother)
15//!
16//! # Example
17//! ```
18//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
19//! use egui_charts::studies::{ALMA, Indicator};
20//!
21//! let mut alma = ALMA::new(9, 0.85, 6.0);
22//! alma.calculate(&bars);
23//! ```
24
25use 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/// Arnaud Legoux Moving Avg indicator
33#[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    /// Create a new ALMA indicator
45    ///
46    /// # Arguments
47    /// * `period` - Lookback period (typically 9)
48    /// * `offset` - Weight offset (typically 0.85), clamped into 0 to 1, NaN to 0
49    /// * `sigma` - Gaussian sigma (typically 6.0), clamped into 0.001 to 100, NaN
50    ///   to 0.001; a larger sigma narrows the weights until they all underflow to zero
51    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    /// Create with default params (9, 0.85, 6.0)
67    pub fn default_params() -> Self {
68        Self::new(9, 0.85, 6.0)
69    }
70
71    /// Set the line color
72    pub fn with_color(mut self, color: Color32) -> Self {
73        self.color = color;
74        self
75    }
76}
77
78/// Construct with the conventional default parameters.
79impl 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        // Gaussian weights, computed here so their size is bounded by the data
95        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        // First period-1 values should be None
235        for i in 0..8 {
236            assert!(matches!(alma.values().to_rows()[i], IndicatorValue::None));
237        }
238
239        // Check valid values exist
240        for value in alma.values().to_rows().iter().skip(8) {
241            assert!(matches!(value, IndicatorValue::Single(_)));
242        }
243    }
244}