Skip to main content

egui_charts/studies/builtin/
swma.rs

1//! Symmetrically Weighted Moving Average (SWMA) indicator.
2//!
3//! SWMA is a fixed 4-bar moving average using symmetric weights
4//! `[1/6, 2/6, 2/6, 1/6]`. The middle bars receive twice the weight
5//! of the outer bars, producing a smooth curve with less lag than SMA
6//! and less overshoot than EMA.
7//!
8//! # Formula
9//!
10//! ```text
11//! SWMA = (1*P[t-3] + 2*P[t-2] + 2*P[t-1] + 1*P[t]) / 6
12//! ```
13//!
14//! # Example
15//!
16//! ```rust
17//! # 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();
18//! use egui_charts::studies::{SWMA, Indicator};
19//!
20//! let mut swma = SWMA::new();
21//! swma.calculate(&bars);
22//! ```
23
24use crate::model::Bar;
25use crate::studies::palette;
26use crate::studies::stepper::update_window;
27use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
28use egui::Color32;
29
30/// Symmetrically Weighted Moving Average indicator.
31///
32/// A fixed 4-bar weighted average with weights `[1, 2, 2, 1] / 6`.
33/// Overlay indicator.
34#[derive(Clone)]
35pub struct SWMA {
36    values: IndicatorValues,
37    color: Color32,
38    visible: bool,
39}
40
41impl Default for SWMA {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl SWMA {
48    /// Create a new SWMA indicator (fixed 4-bar window, no parameters).
49    pub fn new() -> Self {
50        Self {
51            values: IndicatorValues::new(),
52            color: palette::BLUE,
53            visible: true,
54        }
55    }
56
57    /// Set a custom line colour (builder pattern).
58    pub fn with_color(mut self, color: Color32) -> Self {
59        self.color = color;
60        self
61    }
62
63    /// Compute the SWMA value for four consecutive closing prices.
64    ///
65    /// `SWMA = (1*P1 + 2*P2 + 2*P3 + 1*P4) / 6`
66    fn swma_val(p1: f64, p2: f64, p3: f64, p4: f64) -> f64 {
67        (p1 + 2.0 * p2 + 2.0 * p3 + p4) / 6.0
68    }
69}
70
71impl SWMA {
72    fn compute(&self, data: &[Bar]) -> IndicatorValues {
73        let mut values = IndicatorValues::new();
74
75        // SWMA requires 4 bars min
76        if data.len() < 4 {
77            for _ in 0..data.len() {
78                values.push(IndicatorValue::None);
79            }
80            return values;
81        }
82
83        // First 3 bars have no value
84        for _ in 0..3 {
85            values.push(IndicatorValue::None);
86        }
87
88        // Calculate SWMA for remaining bars
89        for i in 3..data.len() {
90            let p1 = data[i - 3].close;
91            let p2 = data[i - 2].close;
92            let p3 = data[i - 1].close;
93            let p4 = data[i].close;
94
95            let swma = Self::swma_val(p1, p2, p3, p4);
96            values.push(IndicatorValue::Single(swma));
97        }
98        values
99    }
100}
101
102impl Indicator for SWMA {
103    fn source(&self) -> Option<crate::studies::StudySource> {
104        Some(crate::studies::StudySource::Price(
105            crate::model::PriceSource::Close,
106        ))
107    }
108
109    fn name(&self) -> &str {
110        "SWMA"
111    }
112
113    fn desc(&self) -> &str {
114        "Symmetrically Weighted Moving Avg - 4-bar symmetric weighted MA"
115    }
116
117    fn calculate(&mut self, data: &[Bar]) {
118        self.update(data, 0);
119    }
120
121    fn update(&mut self, data: &[Bar], from: usize) {
122        let mut values = std::mem::take(&mut self.values);
123        update_window(
124            &mut values,
125            data,
126            from,
127            |_data, from| from.saturating_sub(3),
128            |data| self.compute(data),
129        );
130        self.values = values;
131    }
132
133    fn is_incremental(&self) -> bool {
134        true
135    }
136
137    fn values(&self) -> &IndicatorValues {
138        &self.values
139    }
140
141    fn colors(&self) -> Vec<Color32> {
142        vec![self.color]
143    }
144
145    fn set_colors(&mut self, colors: Vec<Color32>) {
146        if !colors.is_empty() {
147            self.color = colors[0];
148        }
149    }
150
151    fn is_overlay(&self) -> bool {
152        true
153    }
154
155    fn is_visible(&self) -> bool {
156        self.visible
157    }
158
159    fn set_visible(&mut self, visible: bool) {
160        self.visible = visible;
161    }
162
163    fn clone_box(&self) -> Box<dyn Indicator> {
164        Box::new(self.clone())
165    }
166
167    fn line_names(&self) -> Vec<String> {
168        vec!["SWMA".to_string()]
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::studies::test_support::make_bar;
176
177    #[test]
178    fn test_swma_weighted() {
179        // Middle values should have more weight
180        // (1*100 + 2*120 + 2*120 + 1*100) / 6 = (100 + 240 + 240 + 100) / 6 = 680 / 6 = 113.33
181        let swma = SWMA::swma_val(100.0, 120.0, 120.0, 100.0);
182        assert!((swma - 113.33).abs() < 0.01);
183    }
184
185    #[test]
186    fn test_swma_calculation() {
187        let mut swma = SWMA::new();
188
189        let data = vec![
190            make_bar(100.0),
191            make_bar(102.0),
192            make_bar(101.0),
193            make_bar(103.0),
194            make_bar(104.0),
195        ];
196
197        swma.calculate(&data);
198
199        assert_eq!(swma.values.len(), 5);
200        assert!(matches!(swma.values.to_rows()[0], IndicatorValue::None));
201        assert!(matches!(swma.values.to_rows()[1], IndicatorValue::None));
202        assert!(matches!(swma.values.to_rows()[2], IndicatorValue::None));
203
204        // Fourth bar: (100 + 2*102 + 2*101 + 103) / 6 = 609 / 6 = 101.5
205        let IndicatorValue::Single(v) = swma.values.to_rows()[3] else {
206            panic!("no value: {:?}", swma.values.to_rows()[3]);
207        };
208        assert!((v - 101.5).abs() < 0.01);
209    }
210}