Skip to main content

egui_charts/studies/builtin/
stochastic_rsi.rs

1//! Stochastic RSI Indicator
2//!
3//! Stochastic RSI applies the Stochastic formula to RSI values instead of
4//! price, creating a more sensitive oscillator for identifying overbought
5//! and oversold conditions.
6//!
7//! # Formula
8//! StochRSI = (RSI - Lowest RSI) / (Highest RSI - Lowest RSI)
9//! %K = SMA(StochRSI, smoothK)
10//! %D = SMA(%K, smoothD)
11//!
12//! # Interpretation
13//! - Above 0.8: Overbought
14//! - Below 0.2: Oversold
15//! - %K crossing %D: Trading signals
16//!
17//! # Example
18//! ```
19//! # 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();
20//! use egui_charts::studies::{StochasticRSI, Indicator};
21//!
22//! let mut stoch_rsi = StochasticRSI::new(14, 14, 3, 3);
23//! stoch_rsi.calculate(&bars);
24//! ```
25
26use crate::model::Bar;
27use crate::studies::palette;
28use crate::studies::stepper::Stepper;
29use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
30use egui::Color32;
31
32/// Stochastic RSI indicator
33#[derive(Clone)]
34pub struct StochasticRSI {
35    rsi_period: usize,
36    stoch_period: usize,
37    smooth_k: usize,
38    smooth_d: usize,
39    values: IndicatorValues,
40    stepper: Stepper<State>,
41    k_color: Color32,
42    d_color: Color32,
43    visible: bool,
44}
45
46#[derive(Clone, Default)]
47struct State {
48    previous: Option<f64>,
49    count: usize,
50    gain: f64,
51    loss: f64,
52    rsi: std::collections::VecDeque<f64>,
53    k: std::collections::VecDeque<f64>,
54    d: std::collections::VecDeque<f64>,
55}
56
57impl StochasticRSI {
58    /// Create a new Stochastic RSI indicator
59    ///
60    /// # Arguments
61    /// * `rsi_period` - RSI calculation period (typically 14)
62    /// * `stoch_period` - Stochastic lookback period (typically 14)
63    /// * `smooth_k` - %K smoothing period (typically 3)
64    /// * `smooth_d` - %D smoothing period (typically 3)
65    pub fn new(rsi_period: usize, stoch_period: usize, smooth_k: usize, smooth_d: usize) -> Self {
66        Self {
67            rsi_period: rsi_period.max(1),
68            stoch_period: stoch_period.max(1),
69            smooth_k: smooth_k.max(1),
70            smooth_d: smooth_d.max(1),
71            values: IndicatorValues::new(),
72            stepper: Stepper::default(),
73            k_color: palette::BLUE,
74            d_color: palette::ORANGE,
75            visible: true,
76        }
77    }
78
79    /// Create with default params (14, 14, 3, 3)
80    pub fn default_params() -> Self {
81        Self::new(14, 14, 3, 3)
82    }
83
84    /// Set colors for %K and %D lines
85    pub fn with_colors(mut self, k_color: Color32, d_color: Color32) -> Self {
86        self.k_color = k_color;
87        self.d_color = d_color;
88        self
89    }
90}
91
92/// Construct with the conventional default parameters.
93impl Default for StochasticRSI {
94    fn default() -> Self {
95        Self::new(14, 14, 3, 3)
96    }
97}
98
99impl Indicator for StochasticRSI {
100    fn source(&self) -> Option<crate::studies::StudySource> {
101        Some(crate::studies::StudySource::Price(
102            crate::model::PriceSource::Close,
103        ))
104    }
105
106    crate::studies::inputs::study_inputs!(self;
107        rsi_period: length("RSI period", 1, usize::MAX),
108        stoch_period: length("Stoch period", 1, usize::MAX),
109        smooth_k: length("Smooth k", 1, usize::MAX),
110        smooth_d: length("Smooth d", 1, usize::MAX)
111    ; {});
112
113    fn name(&self) -> &str {
114        "StochRSI"
115    }
116
117    fn desc(&self) -> &str {
118        "Stochastic RSI - Stochastic oscillator of RSI"
119    }
120
121    fn calculate(&mut self, data: &[Bar]) {
122        self.update(data, 0);
123    }
124
125    fn update(&mut self, data: &[Bar], from: usize) {
126        self.stepper
127            .run(data, from, &mut self.values, |state, bar| {
128                if !bar.close.is_finite() {
129                    return IndicatorValue::None;
130                }
131                let Some(previous) = state.previous.replace(bar.close) else {
132                    return IndicatorValue::None;
133                };
134                let change = bar.close - previous;
135                state.count += 1;
136                let gain = change.max(0.0);
137                let loss = (-change).max(0.0);
138                if state.count <= self.rsi_period {
139                    state.gain += gain;
140                    state.loss += loss;
141                    if state.count < self.rsi_period {
142                        return IndicatorValue::None;
143                    }
144                    state.gain /= self.rsi_period as f64;
145                    state.loss /= self.rsi_period as f64;
146                } else {
147                    state.gain =
148                        (state.gain * (self.rsi_period - 1) as f64 + gain) / self.rsi_period as f64;
149                    state.loss =
150                        (state.loss * (self.rsi_period - 1) as f64 + loss) / self.rsi_period as f64;
151                }
152                let rsi = super::rsi::from_averages(state.gain, state.loss);
153                state.rsi.push_back(rsi);
154                if state.rsi.len() > self.stoch_period {
155                    state.rsi.pop_front();
156                }
157                if state.rsi.len() < self.stoch_period {
158                    return IndicatorValue::None;
159                }
160                let low = state.rsi.iter().copied().fold(f64::INFINITY, f64::min);
161                let high = state.rsi.iter().copied().fold(f64::NEG_INFINITY, f64::max);
162                let stoch = if (high - low).abs() < 1e-10 {
163                    0.5
164                } else {
165                    (rsi - low) / (high - low)
166                };
167                state.k.push_back(stoch);
168                if state.k.len() > self.smooth_k {
169                    state.k.pop_front();
170                }
171                if state.k.len() < self.smooth_k {
172                    return IndicatorValue::None;
173                }
174                let k = state.k.iter().sum::<f64>() / self.smooth_k as f64;
175                state.d.push_back(k);
176                if state.d.len() > self.smooth_d {
177                    state.d.pop_front();
178                }
179                if state.d.len() < self.smooth_d {
180                    return IndicatorValue::Multiple(vec![k * 100.0, f64::NAN]);
181                }
182                let d = state.d.iter().sum::<f64>() / self.smooth_d as f64;
183                if k.is_nan() || d.is_nan() {
184                    IndicatorValue::None
185                } else {
186                    IndicatorValue::Multiple(vec![k * 100.0, d * 100.0])
187                }
188            });
189    }
190
191    fn is_incremental(&self) -> bool {
192        true
193    }
194
195    fn values(&self) -> &IndicatorValues {
196        &self.values
197    }
198
199    fn colors(&self) -> Vec<Color32> {
200        vec![self.k_color, self.d_color]
201    }
202
203    fn set_colors(&mut self, colors: Vec<Color32>) {
204        if !colors.is_empty() {
205            self.k_color = colors[0];
206        }
207        if colors.len() > 1 {
208            self.d_color = colors[1];
209        }
210    }
211
212    fn is_overlay(&self) -> bool {
213        false // StochRSI is a separate oscillator pane
214    }
215
216    fn line_count(&self) -> usize {
217        2 // %K and %D
218    }
219
220    fn is_visible(&self) -> bool {
221        self.visible
222    }
223
224    fn set_visible(&mut self, visible: bool) {
225        self.visible = visible;
226    }
227
228    fn clone_box(&self) -> Box<dyn Indicator> {
229        Box::new(self.clone())
230    }
231
232    fn line_names(&self) -> Vec<String> {
233        vec![
234            format!(
235                "%K({}, {}, {})",
236                self.rsi_period, self.stoch_period, self.smooth_k
237            ),
238            format!("%D({})", self.smooth_d),
239        ]
240    }
241}