egui_charts/studies/builtin/
rsi.rs1use crate::model::Bar;
51use crate::studies::palette;
52use crate::studies::stepper::Stepper;
53use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
54use egui::Color32;
55
56#[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
89pub(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 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 pub fn with_color(mut self, color: Color32) -> Self {
147 self.set_colors(vec![color]);
148 self
149 }
150}
151
152impl 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 }
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 #[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}