Skip to main content

egui_charts/studies/builtin/
ulcer_index.rs

1//! Ulcer Index
2//!
3//! Peter Martin's Ulcer Index measures downside risk: the root mean square of the percentage
4//! drawdowns of the close from its highest value over the lookback. It rises with both the depth
5//! and the duration of declines, and ignores rises.
6//!
7//! # Formula
8//! ```text
9//! highest  = highest close over the last `length` bars
10//! drawdown = 100 * (close - highest) / highest
11//! Ulcer Index = sqrt(SMA(drawdown^2, length))
12//! ```
13//! with conventional length 14. A drawdown needs a complete window, so the first value is on
14//! bar `2 * length - 2`.
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, UlcerIndex};
20//!
21//! let mut ulcer = UlcerIndex::new(14);
22//! ulcer.calculate(&bars);
23//! assert!(ulcer.values().value(0, 25).is_none());
24//! assert!(ulcer.values().value(0, 26).is_some());
25//! ```
26
27use super::over_finite_bars;
28use crate::model::Bar;
29use crate::studies::palette;
30use crate::studies::stepper::{rolling_max, rolling_mean};
31use crate::studies::stepper::{update_window, usable_behind};
32use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
33use egui::Color32;
34
35/// Root mean square drawdown from the rolling highest close.
36#[derive(Clone)]
37pub struct UlcerIndex {
38    period: usize,
39    values: IndicatorValues,
40    color: Color32,
41    visible: bool,
42}
43
44impl UlcerIndex {
45    /// Create an Ulcer Index over `period` bars (conventionally 14).
46    pub fn new(period: usize) -> Self {
47        Self {
48            period: period.max(1),
49            values: IndicatorValues::new(),
50            color: palette::BLUE,
51            visible: true,
52        }
53    }
54
55    /// The values over `data`, every bar of which has a finite close.
56    fn compute(&self, data: &[Bar]) -> IndicatorValues {
57        let highest = rolling_max(data.iter().map(|b| b.close), self.period);
58        let squares: Vec<f64> = data
59            .iter()
60            .zip(highest)
61            .enumerate()
62            .map(|(i, (bar, highest))| {
63                if i + 1 < self.period {
64                    return f64::NAN;
65                }
66                let drawdown = 100.0 * (bar.close - highest) / highest;
67                drawdown * drawdown
68            })
69            .collect();
70        rolling_mean(&squares, self.period)
71            .into_iter()
72            .map(|mean| IndicatorValue::Single(mean.sqrt()))
73            .collect()
74    }
75}
76
77impl Default for UlcerIndex {
78    fn default() -> Self {
79        Self::new(14)
80    }
81}
82
83impl Indicator for UlcerIndex {
84    fn source(&self) -> Option<crate::studies::StudySource> {
85        Some(crate::studies::StudySource::Price(
86            crate::model::PriceSource::Close,
87        ))
88    }
89
90    crate::studies::inputs::study_inputs!(self;
91        period: length("Length", 1, usize::MAX)
92    ; {});
93
94    fn name(&self) -> &str {
95        "Ulcer Index"
96    }
97
98    fn desc(&self) -> &str {
99        "Ulcer Index - Depth and duration of drawdowns from the recent high"
100    }
101
102    fn calculate(&mut self, data: &[Bar]) {
103        self.update(data, 0);
104    }
105
106    fn update(&mut self, data: &[Bar], from: usize) {
107        let mut values = std::mem::take(&mut self.values);
108        update_window(
109            &mut values,
110            data,
111            from,
112            usable_behind((self.period - 1).saturating_mul(2), |b| b.close.is_finite()),
113            |data| over_finite_bars(data, |b| b.close.is_finite(), |data| self.compute(data)),
114        );
115        self.values = values;
116    }
117
118    fn is_incremental(&self) -> bool {
119        true
120    }
121
122    fn values(&self) -> &IndicatorValues {
123        &self.values
124    }
125
126    fn colors(&self) -> Vec<Color32> {
127        vec![self.color]
128    }
129
130    fn set_colors(&mut self, colors: Vec<Color32>) {
131        if let Some(&color) = colors.first() {
132            self.color = color;
133        }
134    }
135
136    fn is_overlay(&self) -> bool {
137        false
138    }
139
140    fn is_visible(&self) -> bool {
141        self.visible
142    }
143
144    fn set_visible(&mut self, visible: bool) {
145        self.visible = visible;
146    }
147
148    fn clone_box(&self) -> Box<dyn Indicator> {
149        Box::new(self.clone())
150    }
151
152    fn line_names(&self) -> Vec<String> {
153        vec![format!("Ulcer Index({})", self.period)]
154    }
155}