Skip to main content

egui_charts/studies/builtin/
rsi_divergence.rs

1//! RSI Divergence Indicator.
2//!
3//! The RSI with the divergences between its swings and the price's: a regular divergence,
4//! where price makes a new extreme the RSI does not confirm, warns of a reversal; a hidden
5//! divergence, where the RSI makes the new extreme, of a continuation.
6//!
7//! # Rule
8//!
9//! The RSI is Wilder's RSI of the close ([`RSI`](super::RSI)). Its pivot lows and highs are found
10//! with `left` and `right` bars on each side as in
11//! [`PivotPointsHighLow`](super::PivotPointsHighLow), so a pivot is known `right` bars later.
12//! Each pivot low is compared with the previous pivot low `q` when `min_range <= p - q - 1 <=
13//! max_range` bars lie between them:
14//!
15//! ```text
16//! Regular bullish: RSI[p] > RSI[q] and low[p] < low[q]
17//! Hidden bullish:  RSI[p] < RSI[q] and low[p] > low[q]
18//! Regular bearish: RSI[p] < RSI[q] and high[p] > high[q]   (pivot highs)
19//! Hidden bearish:  RSI[p] > RSI[q] and high[p] < high[q]   (pivot highs)
20//! ```
21//!
22//! Each divergence is a segment of the RSI from `q` to `p`, the values interpolated so that
23//! the line joins the two pivots, and a "Bull", "H Bull", "Bear" or "H Bear" mark at the RSI
24//! of `p`. Hidden divergences are off by default. The defaults are an RSI of 14, 5 bars on
25//! each side of a pivot and 5 to 60 bars between pivots.
26//!
27//! # Example
28//!
29//! ```rust
30//! # let bars: Vec<egui_charts::model::Bar> = (0..120).map(|i| { let p = 100.0 + (i as f64 * 0.2).sin() * (5.0 + i as f64 * 0.05); egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p, 1_000.0) }).collect();
31//! use egui_charts::studies::{Indicator, RsiDivergence};
32//!
33//! let mut divergence = RsiDivergence::default();
34//! divergence.calculate(&bars);
35//! // Each "Bull" mark sits on the RSI at the second pivot of a regular bullish divergence.
36//! for bar in 0..bars.len() {
37//!     if let Some(text) = divergence.plot_text(5, bar) {
38//!         assert_eq!(text, "Bull");
39//!         assert_eq!(divergence.values().value(5, bar), divergence.values().value(0, bar));
40//!     }
41//! }
42//! ```
43
44use super::rsi::State as RsiState;
45use crate::model::Bar;
46use crate::studies::palette;
47use crate::studies::stepper::Stepper;
48use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
49use egui::Color32;
50
51/// RSI values closer than this are equal: the RSI is the same number whenever the averages
52/// scale together, as on a flat bar, but rounds to neighbouring floats.
53const RSI_TIE: f64 = 1e-9;
54
55/// Text of the marks on outputs 5 to 8.
56const MARKS: [&str; 4] = ["Bull", "H Bull", "Bear", "H Bear"];
57
58/// RSI Divergence Indicator: the RSI with regular and hidden divergences against price.
59#[derive(Clone)]
60pub struct RsiDivergence {
61    period: usize,
62    left: usize,
63    right: usize,
64    min_range: usize,
65    max_range: usize,
66    bullish: bool,
67    hidden_bullish: bool,
68    bearish: bool,
69    hidden_bearish: bool,
70    values: IndicatorValues,
71    stepper: Stepper<State>,
72    undo: Vec<(usize, usize, f64)>,
73    rsi_color: Color32,
74    fill_color: Color32,
75    bull_color: Color32,
76    bear_color: Color32,
77    visible: bool,
78}
79
80#[derive(Clone, Copy)]
81struct Pivot {
82    source: usize,
83    position: usize,
84    rsi: f64,
85    high: f64,
86    low: f64,
87}
88
89#[derive(Clone, Default)]
90struct State {
91    source: usize,
92    position: usize,
93    rsi: RsiState,
94    window: std::collections::VecDeque<Pivot>,
95    previous: [Option<Pivot>; 2],
96}
97
98struct Segment {
99    line: usize,
100    sources: Vec<usize>,
101    from: f64,
102    to: f64,
103    confirming: usize,
104}
105
106impl RsiDivergence {
107    /// RSI of `period`, pivots with `left` and `right` bars on each side, lengths raised to
108    /// at least 1, and `min_range` to `max_range` bars between two pivots compared.
109    pub fn new(
110        period: usize,
111        left: usize,
112        right: usize,
113        min_range: usize,
114        max_range: usize,
115    ) -> Self {
116        Self {
117            period: period.max(1),
118            left: left.max(1),
119            right: right.max(1),
120            min_range,
121            max_range,
122            bullish: true,
123            hidden_bullish: false,
124            bearish: true,
125            hidden_bearish: false,
126            values: IndicatorValues::new(),
127            stepper: Stepper::default(),
128            undo: Vec::new(),
129            rsi_color: palette::BLUE,
130            fill_color: palette::LIGHT_BLUE.gamma_multiply_u8(26),
131            bull_color: palette::GREEN,
132            bear_color: palette::RED,
133            visible: true,
134        }
135    }
136
137    /// Which divergences to show: regular bullish, hidden bullish, regular bearish and hidden
138    /// bearish.
139    pub fn with_divergences(
140        mut self,
141        bullish: bool,
142        hidden_bullish: bool,
143        bearish: bool,
144        hidden_bearish: bool,
145    ) -> Self {
146        (
147            self.bullish,
148            self.hidden_bullish,
149            self.bearish,
150            self.hidden_bearish,
151        ) = (bullish, hidden_bullish, bearish, hidden_bearish);
152        self
153    }
154}
155
156/// Construct with the conventional default parameters.
157impl Default for RsiDivergence {
158    fn default() -> Self {
159        Self::new(14, 5, 5, 5, 60)
160    }
161}
162
163impl Indicator for RsiDivergence {
164    fn level_style(&self, _value: f64) -> crate::model::LineStyle {
165        crate::model::LineStyle::Dotted
166    }
167
168    fn level_fill_color(&self) -> Color32 {
169        self.fill_color
170    }
171
172    crate::studies::inputs::study_inputs!(self;
173        period: length("RSI period", 1, usize::MAX),
174        right: length("Pivot lookback right", 1, usize::MAX),
175        left: length("Pivot lookback left", 1, usize::MAX),
176        max_range: length("Max of lookback range", 0, usize::MAX),
177        min_range: length("Min of lookback range", 0, usize::MAX),
178        bullish: toggle("Plot bullish"),
179        hidden_bullish: toggle("Plot hidden bullish"),
180        bearish: toggle("Plot bearish"),
181        hidden_bearish: toggle("Plot hidden bearish")
182    ; {});
183
184    fn name(&self) -> &str {
185        "RSI Divergence Indicator"
186    }
187
188    fn desc(&self) -> &str {
189        "RSI Divergence Indicator - Regular and hidden divergences between RSI and price swings"
190    }
191
192    fn calculate(&mut self, data: &[Bar]) {
193        self.update(data, 0);
194    }
195
196    fn update(&mut self, data: &[Bar], from: usize) {
197        let len = self.values.len();
198        if len > 0 && from == len && data.len() == len {
199            return;
200        }
201        if len > 0 && from == len - 1 && data.len() >= len {
202            // A developing confirmation can be withdrawn, restoring the earlier segment it replaced.
203            for &(line, bar, value) in &self.undo {
204                self.values.set_value(line, bar, value);
205            }
206        }
207        self.undo.clear();
208        let mut segments = Vec::new();
209        let keep = self
210            .left
211            .max(self.max_range.saturating_add(1))
212            .saturating_add(self.right)
213            .saturating_add(1);
214        self.stepper
215            .run(data, from, &mut self.values, |state, bar| {
216                let source = state.source;
217                state.source += 1;
218                if ![bar.high, bar.low, bar.close].iter().all(|v| v.is_finite()) {
219                    return IndicatorValue::None;
220                }
221                let rsi = match state.rsi.next(bar, self.period) {
222                    IndicatorValue::Single(value) if value.is_finite() => value,
223                    _ => f64::NAN,
224                };
225                let current = Pivot {
226                    source,
227                    position: state.position,
228                    rsi,
229                    high: bar.high,
230                    low: bar.low,
231                };
232                state.position += 1;
233                state.window.push_back(current);
234                if state.window.len() > keep {
235                    state.window.pop_front();
236                }
237                if let Some(at) = state
238                    .window
239                    .len()
240                    .checked_sub(self.right.saturating_add(1))
241                    .filter(|&at| at >= self.left)
242                {
243                    let p = state.window[at];
244                    let window = state.window.range(at - self.left..);
245                    if window.clone().all(|pivot| pivot.rsi.is_finite()) {
246                        for high in [false, true] {
247                            let before = state.window.range(at - self.left..at).all(|v| {
248                                if high {
249                                    v.rsi <= p.rsi + RSI_TIE
250                                } else {
251                                    v.rsi >= p.rsi - RSI_TIE
252                                }
253                            });
254                            let after = state.window.range(at + 1..).all(|v| {
255                                if high {
256                                    v.rsi < p.rsi - RSI_TIE
257                                } else {
258                                    v.rsi > p.rsi + RSI_TIE
259                                }
260                            });
261                            if !(before && after) {
262                                continue;
263                            }
264                            let previous = state.previous[usize::from(high)].replace(p);
265                            let Some(q) = previous else {
266                                continue;
267                            };
268                            if !(self.min_range..=self.max_range)
269                                .contains(&(p.position - q.position - 1))
270                            {
271                                continue;
272                            }
273                            let divergences = if high {
274                                [
275                                    (self.bearish, 3, false, true),
276                                    (self.hidden_bearish, 4, true, false),
277                                ]
278                            } else {
279                                [
280                                    (self.bullish, 1, true, false),
281                                    (self.hidden_bullish, 2, false, true),
282                                ]
283                            };
284                            for (shown, line, rsi_rises, price_rises) in divergences {
285                                // Prices compare exactly, so the rule holds at any price scale.
286                                let moved = |rises: bool, from: f64, to: f64, tie: f64| {
287                                    if rises {
288                                        to > from + tie
289                                    } else {
290                                        to < from - tie
291                                    }
292                                };
293                                let price =
294                                    |pivot: Pivot| if high { pivot.high } else { pivot.low };
295                                if shown
296                                    && moved(rsi_rises, q.rsi, p.rsi, RSI_TIE)
297                                    && moved(price_rises, price(q), price(p), 0.0)
298                                {
299                                    segments.push(Segment {
300                                        line,
301                                        sources: state
302                                            .window
303                                            .iter()
304                                            .filter(|v| {
305                                                (q.position..=p.position).contains(&v.position)
306                                            })
307                                            .map(|v| v.source)
308                                            .collect(),
309                                        from: q.rsi,
310                                        to: p.rsi,
311                                        confirming: source,
312                                    });
313                                }
314                            }
315                        }
316                    }
317                }
318                let mut row = vec![f64::NAN; 9];
319                row[0] = rsi;
320                IndicatorValue::Multiple(row)
321            });
322        for segment in segments {
323            let last = segment.sources.len() - 1;
324            for (position, &bar) in segment.sources.iter().enumerate() {
325                if segment.confirming + 1 == data.len() {
326                    self.undo.push((
327                        segment.line,
328                        bar,
329                        self.values.value(segment.line, bar).unwrap_or(f64::NAN),
330                    ));
331                }
332                let t = position as f64 / last as f64;
333                self.values.set_value(
334                    segment.line,
335                    bar,
336                    segment.from + (segment.to - segment.from) * t,
337                );
338            }
339            let bar = segment.sources[last];
340            if segment.confirming + 1 == data.len() {
341                self.undo.push((
342                    segment.line + 4,
343                    bar,
344                    self.values.value(segment.line + 4, bar).unwrap_or(f64::NAN),
345                ));
346            }
347            self.values.set_value(segment.line + 4, bar, segment.to);
348        }
349    }
350
351    fn is_incremental(&self) -> bool {
352        true
353    }
354
355    fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
356        use crate::studies::PlotType;
357        let hidden = |c: Color32| Color32::from_rgba_unmultiplied(c.r(), c.g(), c.b(), 51);
358        let (color, plot_type) = match line {
359            0 => (self.rsi_color, PlotType::Line),
360            1 => (self.bull_color, PlotType::Line),
361            2 => (hidden(self.bull_color), PlotType::Line),
362            3 => (self.bear_color, PlotType::Line),
363            4 => (hidden(self.bear_color), PlotType::Line),
364            5 => (self.bull_color, PlotType::TriangleUp),
365            6 => (self.bull_color, PlotType::TriangleUp),
366            7 => (self.bear_color, PlotType::TriangleDown),
367            _ => (self.bear_color, PlotType::TriangleDown),
368        };
369        crate::studies::PlotStyle {
370            color,
371            width: 2.0,
372            pattern: crate::model::LineStyle::Solid,
373            plot_type,
374            visible: true,
375        }
376    }
377
378    fn plot_label_colors(&self, line: usize) -> Option<(Color32, Color32)> {
379        (line >= 5).then_some((
380            if line < 7 {
381                self.bull_color
382            } else {
383                self.bear_color
384            },
385            Color32::WHITE,
386        ))
387    }
388
389    fn plot_text(&self, line: usize, bar: usize) -> Option<String> {
390        let mark = MARKS.get(line.checked_sub(5)?)?;
391        self.values.value(line, bar).map(|_| mark.to_string())
392    }
393
394    fn precision(&self) -> Option<usize> {
395        Some(2)
396    }
397
398    fn values(&self) -> &IndicatorValues {
399        &self.values
400    }
401
402    fn colors(&self) -> Vec<Color32> {
403        vec![
404            self.rsi_color,
405            self.bull_color,
406            self.bear_color,
407            self.fill_color,
408        ]
409    }
410
411    fn set_colors(&mut self, colors: Vec<Color32>) {
412        if let Some(&color) = colors.first() {
413            self.fill_color = colors.get(3).copied().unwrap_or_else(|| {
414                Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 16)
415            });
416        }
417        for (color, slot) in colors.into_iter().zip([
418            &mut self.rsi_color,
419            &mut self.bull_color,
420            &mut self.bear_color,
421        ]) {
422            *slot = color;
423        }
424    }
425
426    fn set_color(&mut self, line: usize, color: Color32) {
427        match line {
428            0 => self.rsi_color = color,
429            1 => self.bull_color = color,
430            2 => self.bear_color = color,
431            _ => {}
432        }
433    }
434
435    fn pane(&self) -> crate::studies::StudyPane {
436        let gray = palette::GRAY;
437        crate::studies::StudyPane {
438            height: Some(138.0),
439            range: Some((0.0, 100.0)),
440            levels: vec![
441                (70.0, gray, "Overbought"),
442                (50.0, gray, "Middle"),
443                (30.0, gray, "Oversold"),
444            ],
445            fill_between_levels: true,
446        }
447    }
448
449    fn is_overlay(&self) -> bool {
450        false
451    }
452
453    fn is_visible(&self) -> bool {
454        self.visible
455    }
456
457    fn set_visible(&mut self, visible: bool) {
458        self.visible = visible;
459    }
460
461    fn clone_box(&self) -> Box<dyn Indicator> {
462        Box::new(self.clone())
463    }
464
465    fn line_names(&self) -> Vec<String> {
466        [
467            "RSI",
468            "Regular Bullish",
469            "Hidden Bullish",
470            "Regular Bearish",
471            "Hidden Bearish",
472            "Regular Bullish Label",
473            "Hidden Bullish Label",
474            "Regular Bearish Label",
475            "Hidden Bearish Label",
476        ]
477        .map(String::from)
478        .to_vec()
479    }
480}