Skip to main content

egui_charts/studies/builtin/
envelopes.rs

1use super::clamp_param;
2use super::smoothing::Smoothing;
3use crate::model::Bar;
4use crate::studies::palette;
5use crate::studies::stepper::Stepper;
6use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
7use egui::Color32;
8
9/// The moving average in the middle of [`Envelopes`].
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum EnvelopeAverage {
13    /// Arithmetic mean of a complete window, named `SMA`.
14    Simple,
15    /// Exponential smoothing with alpha 2/(period+1), seeded by the first price, named `EMA`.
16    Exponential,
17}
18
19impl EnvelopeAverage {
20    /// The abbreviation in the middle line's name.
21    fn label(self) -> &'static str {
22        match self {
23            Self::Simple => "SMA",
24            Self::Exponential => "EMA",
25        }
26    }
27}
28
29/// Moving Average Envelopes. The band between the upper and lower lines is filled.
30#[derive(Clone)]
31pub struct Envelopes {
32    period: usize,
33    percent: f64,
34    ma_type: EnvelopeAverage,
35    values: IndicatorValues,
36    stepper: Stepper<State>,
37    colors: Vec<Color32>,
38    visible: bool,
39}
40
41#[derive(Clone, Default)]
42struct State {
43    ema: Smoothing,
44    window: std::collections::VecDeque<f64>,
45}
46
47impl Envelopes {
48    /// Bands `percent` above and below a `ma_type` average of `period` closes. `percent` is
49    /// clamped into 0 to `f64::MAX`, NaN to 0.
50    pub fn new(period: usize, percent: f64, ma_type: EnvelopeAverage) -> Self {
51        Self {
52            period: period.max(1),
53            percent: clamp_param(percent, 0.0, f64::MAX),
54            ma_type,
55            values: IndicatorValues::new(),
56            stepper: Stepper::default(),
57            colors: vec![
58                palette::BLUE,                       // Upper
59                palette::ORANGE,                     // Middle
60                palette::BLUE,                       // Lower
61                palette::BLUE.gamma_multiply_u8(13), // Fill
62            ],
63            visible: true,
64        }
65    }
66}
67
68impl Default for Envelopes {
69    fn default() -> Self {
70        Self::new(20, 10.0, EnvelopeAverage::Simple)
71    }
72}
73
74impl Indicator for Envelopes {
75    fn source(&self) -> Option<crate::studies::StudySource> {
76        Some(crate::studies::StudySource::Price(
77            crate::model::PriceSource::Close,
78        ))
79    }
80
81    crate::studies::inputs::study_inputs!(self;
82        period: length("Period", 1, usize::MAX),
83        percent: number("Percent", 0.0, f64::MAX),
84        ma_type: choice("Average", [EnvelopeAverage::Simple, EnvelopeAverage::Exponential])
85    ; {});
86
87    fn fills(&self) -> Vec<crate::studies::StudyFill> {
88        vec![crate::studies::StudyFill {
89            first: crate::studies::FillBoundary::Plot(0),
90            second: crate::studies::FillBoundary::Plot(2),
91            color: self.colors[3],
92            inverted_color: None,
93        }]
94    }
95
96    fn name(&self) -> &str {
97        "Envelopes"
98    }
99
100    fn desc(&self) -> &str {
101        "Moving Average Envelopes - Percentage bands around a moving average"
102    }
103
104    fn calculate(&mut self, data: &[Bar]) {
105        self.update(data, 0);
106    }
107
108    fn update(&mut self, data: &[Bar], from: usize) {
109        self.stepper
110            .run(data, from, &mut self.values, |state, bar| {
111                let ma = if self.ma_type == EnvelopeAverage::Exponential {
112                    state
113                        .ema
114                        .next(bar.close, 1, 2.0 / (self.period as f64 + 1.0))
115                } else {
116                    state.window.push_back(bar.close);
117                    if state.window.len() > self.period {
118                        state.window.pop_front();
119                    }
120                    if state.window.len() < self.period
121                        || state.window.iter().any(|v| !v.is_finite())
122                    {
123                        f64::NAN
124                    } else {
125                        state.window.iter().sum::<f64>() / self.period as f64
126                    }
127                };
128                let multiplier = self.percent / 100.0;
129                IndicatorValue::Multiple(vec![ma * (1.0 + multiplier), ma, ma * (1.0 - multiplier)])
130            });
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        self.colors.clone()
143    }
144
145    fn set_colors(&mut self, colors: Vec<Color32>) {
146        if colors.len() >= 3 {
147            let n = colors.len().min(self.colors.len());
148            self.colors[..n].copy_from_slice(&colors[..n]);
149        }
150    }
151
152    fn is_overlay(&self) -> bool {
153        true
154    }
155
156    fn line_count(&self) -> usize {
157        3
158    }
159
160    fn is_visible(&self) -> bool {
161        self.visible
162    }
163
164    fn set_visible(&mut self, visible: bool) {
165        self.visible = visible;
166    }
167
168    fn clone_box(&self) -> Box<dyn Indicator> {
169        Box::new(self.clone())
170    }
171
172    fn line_names(&self) -> Vec<String> {
173        vec![
174            format!("Upper(+{}%)", self.percent),
175            format!("{}({})", self.ma_type.label(), self.period),
176            format!("Lower(-{}%)", self.percent),
177        ]
178    }
179}