Skip to main content

egui_charts/studies/builtin/
median.rs

1//! Median
2//!
3//! A short rolling median of the bar midpoint with an EMA of it and average-true-range bands,
4//! for reading direction (the median against its EMA) and volatility (the band width) together.
5//!
6//! # Formula
7//! - median = the median of (high + low) / 2 over the last `n` bars (3 by default), taken by
8//!   nearest rank: the `ceil(n / 2)`-th smallest value, so the lower middle one for an even `n`
9//! - Median EMA = EMA(median, n), starting with the simple mean of the first `n` medians
10//! - upper and lower bands = median +/- k * ATR(m), Wilder's average true range with `m` = 14
11//!   and `k` = 2 by default
12//!
13//! The band between the median and its EMA is filled green while the median is at or above
14//! its EMA and violet below.
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::{Indicator, Median};
20//!
21//! let mut median = Median::new(3, 14, 2.0);
22//! median.calculate(&bars);
23//! ```
24
25use super::clamp_param;
26use super::smoothing::Smoothing;
27use crate::model::Bar;
28use crate::studies::palette;
29use crate::studies::stepper::Stepper;
30use crate::studies::{ATR, Indicator, IndicatorValue, IndicatorValues};
31use egui::Color32;
32
33/// Median: rolling median of hl2, its EMA and ATR bands around it.
34#[derive(Clone)]
35pub struct Median {
36    length: usize,
37    atr_length: usize,
38    multiplier: f64,
39    values: IndicatorValues,
40    atr: ATR,
41    stepper: Stepper<(usize, std::collections::VecDeque<f64>, Smoothing)>,
42    /// Median, upper band, lower band, Median EMA, rising fill, falling fill.
43    colors: [Color32; 6],
44    visible: bool,
45}
46
47impl Median {
48    /// Create the study from the median length (typically 3), the ATR length (typically 14)
49    /// and the ATR multiplier (typically 2, clamped into 0 to `f64::MAX`, NaN to 0).
50    pub fn new(length: usize, atr_length: usize, multiplier: f64) -> Self {
51        Self {
52            length: length.max(1),
53            atr_length: atr_length.max(1),
54            multiplier: clamp_param(multiplier, 0.0, f64::MAX),
55            values: IndicatorValues::new(),
56            atr: ATR::new(atr_length),
57            stepper: Stepper::default(),
58            colors: [
59                palette::RED,
60                Color32::from_rgb(0, 230, 118),
61                Color32::from_rgb(224, 64, 251),
62                palette::BLUE,
63                Color32::from_rgb(0, 230, 118),
64                Color32::from_rgb(224, 64, 251),
65            ],
66            visible: true,
67        }
68    }
69}
70
71/// Construct with the conventional default parameters.
72impl Default for Median {
73    fn default() -> Self {
74        Self::new(3, 14, 2.0)
75    }
76}
77
78impl Indicator for Median {
79    fn line_style(&self, line: usize) -> crate::studies::IndicatorLineStyle {
80        crate::studies::IndicatorLineStyle {
81            width: if line == 0 { 3.0 } else { 1.0 },
82            ..Default::default()
83        }
84    }
85
86    crate::studies::inputs::study_inputs!(self;
87        length: length("Median length", 1, usize::MAX),
88        atr_length: length("ATR length", 1, usize::MAX),
89        multiplier: number("ATR multiplier", 0.0, f64::MAX)
90    ; {});
91
92    fn fills(&self) -> Vec<crate::studies::StudyFill> {
93        let alpha = |c: Color32| Color32::from_rgba_unmultiplied(c.r(), c.g(), c.b(), 230);
94        vec![crate::studies::StudyFill {
95            first: crate::studies::FillBoundary::Plot(0),
96            second: crate::studies::FillBoundary::Plot(3),
97            color: alpha(self.colors[4]),
98            inverted_color: Some(alpha(self.colors[5])),
99        }]
100    }
101
102    fn name(&self) -> &str {
103        "Median"
104    }
105
106    fn desc(&self) -> &str {
107        "Median - Rolling median of the bar midpoint with its EMA and ATR bands"
108    }
109
110    fn calculate(&mut self, data: &[Bar]) {
111        self.update(data, 0);
112    }
113
114    fn update(&mut self, data: &[Bar], from: usize) {
115        if from == 0 {
116            self.atr = ATR::new(self.atr_length);
117        }
118        self.atr.update(data, from);
119        self.stepper
120            .run(data, from, &mut self.values, |state, bar| {
121                let index = state.0;
122                state.0 += 1;
123                if ![bar.high, bar.low, bar.close].iter().all(|x| x.is_finite()) {
124                    return IndicatorValue::None;
125                }
126                state.1.push_back((bar.high + bar.low) / 2.0);
127                if state.1.len() > self.length {
128                    state.1.pop_front();
129                }
130                let median = if state.1.len() < self.length {
131                    f64::NAN
132                } else {
133                    let mut window: Vec<_> = state.1.iter().copied().collect();
134                    *window
135                        .select_nth_unstable_by(self.length.div_ceil(2) - 1, f64::total_cmp)
136                        .1
137                };
138                let ema = state
139                    .2
140                    .next(median, self.length, 2.0 / (self.length as f64 + 1.0));
141                let band = self
142                    .atr
143                    .values()
144                    .value(0, index)
145                    .map_or(f64::NAN, |atr| self.multiplier * atr);
146                IndicatorValue::Multiple(vec![median, median + band, median - band, ema])
147            });
148    }
149
150    fn is_incremental(&self) -> bool {
151        true
152    }
153
154    fn values(&self) -> &IndicatorValues {
155        &self.values
156    }
157
158    fn colors(&self) -> Vec<Color32> {
159        self.colors.to_vec()
160    }
161
162    fn set_colors(&mut self, colors: Vec<Color32>) {
163        for (old, new) in self.colors.iter_mut().zip(colors) {
164            *old = new;
165        }
166    }
167
168    fn is_visible(&self) -> bool {
169        self.visible
170    }
171
172    fn set_visible(&mut self, visible: bool) {
173        self.visible = visible;
174    }
175
176    fn clone_box(&self) -> Box<dyn Indicator> {
177        Box::new(self.clone())
178    }
179
180    fn line_names(&self) -> Vec<String> {
181        vec![
182            "Median".into(),
183            "Upper Band".into(),
184            "Lower Band".into(),
185            "Median EMA".into(),
186        ]
187    }
188}