1use 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
51const RSI_TIE: f64 = 1e-9;
54
55const MARKS: [&str; 4] = ["Bull", "H Bull", "Bear", "H Bear"];
57
58#[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 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 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
156impl 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 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 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}