egui_charts/studies/builtin/
envelopes.rs1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum EnvelopeAverage {
13 Simple,
15 Exponential,
17}
18
19impl EnvelopeAverage {
20 fn label(self) -> &'static str {
22 match self {
23 Self::Simple => "SMA",
24 Self::Exponential => "EMA",
25 }
26 }
27}
28
29#[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 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, palette::ORANGE, palette::BLUE, palette::BLUE.gamma_multiply_u8(13), ],
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}