Skip to main content

egui_charts/studies/builtin/
smi_ergodic.rs

1//! SMI Ergodic Indicator and SMI Ergodic Oscillator, William Blau.
2//!
3//! The Stochastic Momentum Index of Blau's ergodic system is his True Strength Index as a ratio,
4//! with an exponential signal line. The oscillator is their difference, drawn as a histogram.
5//!
6//! # Formula
7//!
8//! ```text
9//! m          = close - previous close
10//! SMI        = EMA(EMA(m, long), short) / EMA(EMA(|m|, long), short)
11//! Signal     = EMA(SMI, signal)
12//! Oscillator = SMI - Signal
13//! ```
14//!
15//! The SMI runs from -1 to 1. Every EMA starts at its first input, as in
16//! [`TrueStrengthIndex`](super::TrueStrengthIndex), which computes the same lines scaled by 100: the
17//! changes start on bar 1 and values start on bar `long + short`. Defaults: long 20, short 5,
18//! signal 5.
19//!
20//! # Example
21//! ```
22//! # 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();
23//! use egui_charts::studies::{Indicator, SMIErgodicIndicator, SMIErgodicOscillator};
24//!
25//! let mut lines = SMIErgodicIndicator::default();
26//! lines.calculate(&bars);
27//! let mut histogram = SMIErgodicOscillator::default();
28//! histogram.calculate(&bars);
29//! let (smi, signal) = (lines.values().value(0, 40).unwrap(), lines.values().value(1, 40).unwrap());
30//! assert!((histogram.values().value(0, 40).unwrap() - (smi - signal)).abs() < 1e-12);
31//! ```
32
33use super::TrueStrengthIndex;
34use crate::model::Bar;
35use crate::studies::palette;
36use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
37use egui::Color32;
38
39/// SMI and signal lines of the SMI Ergodic Indicator.
40pub type SMIErgodicIndicator = SMIErgodic<false>;
41
42/// Histogram of the SMI minus its signal, the SMI Ergodic Oscillator.
43pub type SMIErgodicOscillator = SMIErgodic<true>;
44
45/// Blau's SMI Ergodic study: the SMI and signal lines, or with `OSCILLATOR` their difference.
46#[derive(Clone)]
47pub struct SMIErgodic<const OSCILLATOR: bool> {
48    long_length: usize,
49    short_length: usize,
50    signal_length: usize,
51    values: IndicatorValues,
52    tsi: TrueStrengthIndex,
53    /// Colors: [SMI, Signal], or [Oscillator]
54    colors: Vec<Color32>,
55    visible: bool,
56}
57
58impl<const OSCILLATOR: bool> SMIErgodic<OSCILLATOR> {
59    /// Create the study with the long and short smoothing and signal lengths (20, 5 and 5 by convention).
60    pub fn new(long_length: usize, short_length: usize, signal_length: usize) -> Self {
61        Self {
62            long_length: long_length.max(1),
63            short_length: short_length.max(1),
64            signal_length: signal_length.max(1),
65            values: IndicatorValues::new(),
66            tsi: TrueStrengthIndex::new(long_length, short_length, signal_length),
67            colors: if OSCILLATOR {
68                vec![Color32::from_rgb(255, 82, 82)]
69            } else {
70                vec![palette::BLUE, palette::ORANGE]
71            },
72            visible: true,
73        }
74    }
75}
76
77impl<const OSCILLATOR: bool> Default for SMIErgodic<OSCILLATOR> {
78    fn default() -> Self {
79        Self::new(20, 5, 5)
80    }
81}
82
83impl<const OSCILLATOR: bool> Indicator for SMIErgodic<OSCILLATOR> {
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        long_length: length("Long length", 1, usize::MAX),
92        short_length: length("Short length", 1, usize::MAX),
93        signal_length: length("Signal line length", 1, usize::MAX)
94    ; {});
95
96    fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
97        crate::studies::PlotStyle {
98            color: self.colors[line.min(self.colors.len() - 1)],
99            width: 1.0,
100            pattern: crate::model::LineStyle::Solid,
101            plot_type: if OSCILLATOR {
102                crate::studies::PlotType::Histogram
103            } else {
104                crate::studies::PlotType::Line
105            },
106            visible: true,
107        }
108    }
109
110    fn value_format(&self) -> crate::scales::StudyFormat {
111        crate::scales::StudyFormat::Fixed(4)
112    }
113
114    fn name(&self) -> &str {
115        if OSCILLATOR {
116            "SMI Ergodic Oscillator"
117        } else {
118            "SMI Ergodic Indicator"
119        }
120    }
121
122    fn desc(&self) -> &str {
123        if OSCILLATOR {
124            "SMI Ergodic Oscillator - SMI minus its signal line"
125        } else {
126            "SMI Ergodic Indicator - True strength ratio with a signal line"
127        }
128    }
129
130    fn calculate(&mut self, data: &[Bar]) {
131        self.update(data, 0);
132    }
133
134    fn update(&mut self, data: &[Bar], from: usize) {
135        if from == 0 {
136            self.tsi =
137                TrueStrengthIndex::new(self.long_length, self.short_length, self.signal_length);
138        }
139        self.tsi.update(data, from);
140        let start = from.min(self.values.len()).min(data.len());
141        self.values.truncate(start);
142        for bar in start..data.len() {
143            let (Some(smi), Some(signal)) = (
144                self.tsi.values().value(0, bar),
145                self.tsi.values().value(1, bar),
146            ) else {
147                self.values.push(IndicatorValue::None);
148                continue;
149            };
150            let (smi, signal) = (smi / 100.0, signal / 100.0);
151            if OSCILLATOR {
152                self.values.push(IndicatorValue::Single(smi - signal));
153            } else {
154                self.values.push_lines(&[smi, signal]);
155            }
156        }
157    }
158
159    fn is_incremental(&self) -> bool {
160        true
161    }
162
163    fn values(&self) -> &IndicatorValues {
164        &self.values
165    }
166
167    fn colors(&self) -> Vec<Color32> {
168        self.colors.clone()
169    }
170
171    fn set_colors(&mut self, colors: Vec<Color32>) {
172        if colors.len() >= self.line_count() {
173            self.colors = colors;
174        }
175    }
176
177    fn is_overlay(&self) -> bool {
178        false
179    }
180
181    fn is_visible(&self) -> bool {
182        self.visible
183    }
184
185    fn set_visible(&mut self, visible: bool) {
186        self.visible = visible;
187    }
188
189    fn clone_box(&self) -> Box<dyn Indicator> {
190        Box::new(self.clone())
191    }
192
193    fn line_count(&self) -> usize {
194        if OSCILLATOR { 1 } else { 2 }
195    }
196
197    fn line_names(&self) -> Vec<String> {
198        let smi = format!("SMI({},{})", self.long_length, self.short_length);
199        if OSCILLATOR {
200            vec![format!("{smi} - Signal({})", self.signal_length)]
201        } else {
202            vec![smi, format!("Signal({})", self.signal_length)]
203        }
204    }
205}