Skip to main content

egui_charts/studies/builtin/
rsi.rs

1//! Relative Strength Index (RSI) indicator.
2//!
3//! RSI is a momentum oscillator that measures the speed and magnitude of
4//! price movements on a 0-100 scale. Developed by J. Welles Wilder Jr.,
5//! it is one of the most popular technical indicators for identifying
6//! overbought and oversold conditions.
7//!
8//! # Formula
9//!
10//! ```text
11//! RS = Average Gain / Average Loss   (over N periods, using Wilder's smoothing)
12//! RSI = 100 - (100 / (1 + RS))
13//! ```
14//!
15//! The initial average gain/loss is a simple mean of the first `N` changes.
16//! Subsequent values use Wilder's smoothing:
17//! `Avg = (Prev_Avg * (N-1) + Current) / N`.
18//!
19//! The formula is evaluated as `100 * Gain / (Gain + Loss)`, which is identical but needs no
20//! price-unit floor, so scaling the prices does not change the result. A window with gains and
21//! no losses is 100, one with losses and no gains is 0, and a fully flat window is the neutral
22//! 50.
23//!
24//! # Interpretation
25//!
26//! - RSI > 70: overbought -- potential pullback.
27//! - RSI < 30: oversold -- potential bounce.
28//! - Divergence between RSI and price often precedes reversals.
29//! - Default period: 14.
30//!
31//! This is a **non-overlay** indicator (drawn in its own sub-pane).
32//!
33//! # Default colour
34//!
35//! Purple (#7E57C2), with a one-pixel line.
36//!
37//! # Example
38//!
39//! ```rust
40//! # 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();
41//! use egui_charts::studies::{RSI, Indicator, IndicatorValue};
42//!
43//! let mut rsi = RSI::new(14);
44//! rsi.calculate(&bars);
45//! // The first `period` values are warmup.
46//! assert!(rsi.values().to_rows()[..14].iter().all(|v| matches!(v, IndicatorValue::None)));
47//! assert!(matches!(rsi.values().to_rows()[14], IndicatorValue::Single(_)));
48//! ```
49
50use crate::model::Bar;
51use crate::studies::palette;
52use crate::studies::stepper::Stepper;
53use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
54use egui::Color32;
55
56/// Relative Strength Index (RSI) indicator.
57///
58/// A bounded momentum oscillator (0-100) that compares the magnitude of
59/// recent gains to recent losses. Drawn in a separate sub-pane.
60#[derive(Clone)]
61pub struct RSI {
62    period: usize,
63    values: IndicatorValues,
64    stepper: Stepper<State>,
65    color: Color32,
66    fill_color: Color32,
67    visible: bool,
68}
69
70#[derive(Clone)]
71pub(super) struct State {
72    previous: Option<f64>,
73    usable: usize,
74    gain: f64,
75    loss: f64,
76}
77
78impl Default for State {
79    fn default() -> Self {
80        Self {
81            previous: None,
82            usable: 0,
83            gain: -0.0,
84            loss: -0.0,
85        }
86    }
87}
88
89/// RSI from Wilder's average gain and loss: 100 with no loss, 0 with no gain, 50 when both are 0.
90pub(super) fn from_averages(gain: f64, loss: f64) -> f64 {
91    let total = gain + loss;
92    if total > 0.0 {
93        100.0 * gain / total
94    } else {
95        50.0
96    }
97}
98
99impl State {
100    pub(super) fn next(&mut self, bar: &Bar, period: usize) -> IndicatorValue {
101        if !bar.close.is_finite() {
102            return IndicatorValue::None;
103        }
104        let previous = self.previous.replace(bar.close);
105        let i = self.usable;
106        self.usable += 1;
107        let Some(previous) = previous else {
108            return IndicatorValue::None;
109        };
110        let change = bar.close - previous;
111        let gain = if change > 0.0 { change } else { 0.0 };
112        let loss = if change < 0.0 { -change } else { 0.0 };
113        if i <= period {
114            self.gain += gain;
115            self.loss += loss;
116            if i < period {
117                return IndicatorValue::None;
118            }
119            self.gain /= period as f64;
120            self.loss /= period as f64;
121        } else {
122            self.gain = (self.gain * (period - 1) as f64 + gain) / period as f64;
123            self.loss = (self.loss * (period - 1) as f64 + loss) / period as f64;
124        }
125        IndicatorValue::Single(from_averages(self.gain, self.loss))
126    }
127}
128
129impl RSI {
130    /// Create a new RSI indicator.
131    ///
132    /// # Arguments
133    /// * `period` -- The lookback period (default convention: 14).
134    pub fn new(period: usize) -> Self {
135        Self {
136            period: period.max(1),
137            values: IndicatorValues::new(),
138            stepper: Stepper::default(),
139            color: palette::PURPLE,
140            fill_color: palette::PURPLE.gamma_multiply_u8(26),
141            visible: true,
142        }
143    }
144
145    /// Set a custom line colour (builder pattern).
146    pub fn with_color(mut self, color: Color32) -> Self {
147        self.set_colors(vec![color]);
148        self
149    }
150}
151
152/// Construct with the conventional default parameters.
153impl Default for RSI {
154    fn default() -> Self {
155        Self::new(14)
156    }
157}
158
159impl Indicator for RSI {
160    fn level_fill_color(&self) -> Color32 {
161        self.fill_color
162    }
163
164    fn source(&self) -> Option<crate::studies::StudySource> {
165        Some(crate::studies::StudySource::Price(
166            crate::model::PriceSource::Close,
167        ))
168    }
169
170    crate::studies::inputs::study_inputs!(self;
171        period: length("Period", 1, usize::MAX)
172    ; {});
173
174    fn name(&self) -> &str {
175        "RSI"
176    }
177
178    fn precision(&self) -> Option<usize> {
179        Some(2)
180    }
181
182    fn desc(&self) -> &str {
183        "Relative Strength Index - Momentum oscillator (0-100)"
184    }
185
186    fn calculate(&mut self, data: &[Bar]) {
187        self.update(data, 0);
188    }
189
190    fn update(&mut self, data: &[Bar], from: usize) {
191        let period = self.period;
192        self.stepper
193            .run(data, from, &mut self.values, |state, bar| {
194                state.next(bar, period)
195            });
196    }
197
198    fn is_incremental(&self) -> bool {
199        true
200    }
201
202    fn values(&self) -> &IndicatorValues {
203        &self.values
204    }
205
206    fn colors(&self) -> Vec<Color32> {
207        vec![self.color, self.fill_color]
208    }
209
210    fn set_colors(&mut self, colors: Vec<Color32>) {
211        if !colors.is_empty() {
212            self.color = colors[0];
213            self.fill_color = colors.get(1).copied().unwrap_or_else(|| {
214                Color32::from_rgba_unmultiplied(self.color.r(), self.color.g(), self.color.b(), 16)
215            });
216        }
217    }
218
219    fn pane(&self) -> crate::studies::StudyPane {
220        crate::studies::StudyPane {
221            height: Some(138.0),
222            range: Some((0.0, 100.0)),
223            levels: vec![
224                (70.0, palette::MUTED, "Overbought"),
225                (30.0, palette::MUTED, "Oversold"),
226            ],
227            fill_between_levels: true,
228        }
229    }
230
231    fn is_overlay(&self) -> bool {
232        false // RSI is drawn in separate pane
233    }
234
235    fn is_visible(&self) -> bool {
236        self.visible
237    }
238
239    fn set_visible(&mut self, visible: bool) {
240        self.visible = visible;
241    }
242
243    fn clone_box(&self) -> Box<dyn Indicator> {
244        Box::new(self.clone())
245    }
246
247    fn line_names(&self) -> Vec<String> {
248        vec![format!("RSI({})", self.period)]
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use chrono::{TimeZone, Utc};
256
257    fn bars_from_closes(closes: &[f64]) -> Vec<Bar> {
258        closes
259            .iter()
260            .enumerate()
261            .map(|(i, &c)| {
262                Bar::new(
263                    Utc.timestamp_opt(i as i64 * 60, 0).unwrap(),
264                    c,
265                    c,
266                    c,
267                    c,
268                    1.0,
269                )
270            })
271            .collect()
272    }
273
274    fn rsi_of(closes: &[f64], period: usize) -> Vec<Option<f64>> {
275        let mut rsi = RSI::new(period);
276        rsi.calculate(&bars_from_closes(closes));
277        rsi.values()
278            .to_rows()
279            .into_iter()
280            .map(|v| match v {
281                IndicatorValue::Single(v) => Some(v),
282                _ => None,
283            })
284            .collect()
285    }
286
287    /// Endpoints and scale: hand values for period 2 are RSI[2] = 100 * 1 / (1 + 0.5) from the
288    /// mean gain 1 and loss 0.5, then 100 * 1.5 / (1.5 + 0.25) after Wilder smoothing. Scaling
289    /// the prices must not move them; no loss is 100, no gain 0 and a flat window the neutral 50.
290    #[test]
291    fn endpoints_and_scale_invariance() {
292        let ordinary = [10.0, 12.0, 11.0, 13.0];
293        for scale in [1.0, 1e-12, 1e12] {
294            let closes: Vec<f64> = ordinary.iter().map(|c| c * scale).collect();
295            let got = rsi_of(&closes, 2);
296            assert_eq!(got[..2], [None, None]);
297            assert!((got[2].unwrap() - 200.0 / 3.0).abs() < 1e-9, "{scale}");
298            assert!((got[3].unwrap() - 600.0 / 7.0).abs() < 1e-9, "{scale}");
299        }
300        assert_eq!(rsi_of(&[1e-12, 2e-12, 3e-12, 4e-12], 2)[2], Some(100.0));
301        assert_eq!(rsi_of(&[4e-12, 3e-12, 2e-12, 1e-12], 2)[2], Some(0.0));
302        assert_eq!(rsi_of(&[5.0, 5.0, 5.0, 5.0], 2)[2..], [Some(50.0); 2]);
303    }
304}