egui_charts/studies/builtin/
knoxville_divergence.rs1use super::rsi::State as RsiState;
26use crate::model::Bar;
27use crate::studies::palette;
28use crate::studies::stepper::Stepper;
29use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
30use egui::Color32;
31use std::collections::VecDeque;
32
33#[derive(Clone)]
35pub struct KnoxvilleDivergence {
36 momentum_period: usize,
37 rsi_period: usize,
38 left: usize,
39 right: usize,
40 lookback: usize,
41 oversold: usize,
42 overbought: usize,
43 values: IndicatorValues,
44 stepper: Stepper<State>,
45 colors: [Color32; 3],
46 visible: bool,
47}
48
49#[derive(Clone, Copy)]
50struct Pivot {
51 position: usize,
52 high: f64,
53 low: f64,
54 momentum: f64,
55 rsi: f64,
56}
57
58#[derive(Clone, Default)]
59struct State {
60 position: usize,
61 rsi: RsiState,
62 closes: VecDeque<f64>,
63 window: VecDeque<Pivot>,
64 previous: [Option<Pivot>; 2],
66}
67
68impl KnoxvilleDivergence {
69 pub fn new(
73 momentum_period: usize,
74 rsi_period: usize,
75 left: usize,
76 right: usize,
77 lookback: usize,
78 ) -> Self {
79 Self {
80 momentum_period: momentum_period.max(1),
81 rsi_period: rsi_period.max(1),
82 left: left.max(1),
83 right: right.max(1),
84 lookback: lookback.max(1),
85 oversold: 30,
86 overbought: 70,
87 values: IndicatorValues::new(),
88 stepper: Stepper::default(),
89 colors: [palette::ACCENT, palette::GREEN, palette::RED],
90 visible: true,
91 }
92 }
93}
94
95impl Default for KnoxvilleDivergence {
97 fn default() -> Self {
98 Self::new(20, 14, 5, 5, 50)
99 }
100}
101
102impl Indicator for KnoxvilleDivergence {
103 crate::studies::inputs::study_inputs!(self;
104 momentum_period: length("Momentum period", 1, usize::MAX),
105 rsi_period: length("RSI period", 1, usize::MAX),
106 left: length("Pivot bars left", 1, usize::MAX),
107 right: length("Pivot bars right", 1, usize::MAX),
108 lookback: length("Bars between pivots", 1, usize::MAX),
109 oversold: length("RSI oversold", 0, 100),
110 overbought: length("RSI overbought", 0, 100)
111 ; {});
112
113 fn name(&self) -> &str {
114 "Knoxville Divergence"
115 }
116
117 fn desc(&self) -> &str {
118 "Knoxville Divergence - Price and momentum swings that disagree at an RSI extreme"
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 let keep = self.left.saturating_add(self.right).saturating_add(1);
127 self.stepper
128 .run(data, from, &mut self.values, |state, bar| {
129 if ![bar.high, bar.low, bar.close].iter().all(|v| v.is_finite()) {
130 return IndicatorValue::None;
131 }
132 let rsi = match state.rsi.next(bar, self.rsi_period) {
133 IndicatorValue::Single(value) if value.is_finite() => value,
134 _ => f64::NAN,
135 };
136 state.closes.push_back(bar.close);
137 if state.closes.len() > self.momentum_period.saturating_add(1) {
138 state.closes.pop_front();
139 }
140 let momentum = if state.closes.len() > self.momentum_period {
141 bar.close - state.closes[0]
142 } else {
143 f64::NAN
144 };
145 state.window.push_back(Pivot {
146 position: state.position,
147 high: bar.high,
148 low: bar.low,
149 momentum,
150 rsi,
151 });
152 state.position += 1;
153 if state.window.len() > keep {
154 state.window.pop_front();
155 }
156 let mut row = [momentum, f64::NAN, f64::NAN];
157 if let Some(at) = state
158 .window
159 .len()
160 .checked_sub(self.right.saturating_add(1))
161 .filter(|&at| at >= self.left)
162 {
163 let p = state.window[at];
164 for high in [false, true] {
165 let price = |pivot: &Pivot| if high { pivot.high } else { pivot.low };
166 let before = state.window.range(at - self.left..at).all(|v| {
167 if high {
168 price(v) <= price(&p)
169 } else {
170 price(v) >= price(&p)
171 }
172 });
173 let after = state.window.range(at + 1..).all(|v| {
174 if high {
175 price(v) < price(&p)
176 } else {
177 price(v) > price(&p)
178 }
179 });
180 if !(before && after) {
181 continue;
182 }
183 let Some(q) = state.previous[usize::from(high)].replace(p) else {
184 continue;
185 };
186 let diverges = if high {
187 p.high > q.high
188 && p.momentum < q.momentum
189 && p.rsi >= self.overbought as f64
190 } else {
191 p.low < q.low
192 && p.momentum > q.momentum
193 && p.rsi <= self.oversold as f64
194 };
195 if diverges && p.position - q.position <= self.lookback {
197 row[1 + usize::from(high)] = p.momentum;
198 }
199 }
200 }
201 IndicatorValue::Multiple(row.to_vec())
202 });
203 }
204
205 fn is_incremental(&self) -> bool {
206 true
207 }
208
209 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
210 crate::studies::PlotStyle {
211 color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
212 width: 1.0,
213 pattern: crate::model::LineStyle::Solid,
214 plot_type: match line {
215 0 => crate::studies::PlotType::Line,
216 1 => crate::studies::PlotType::TriangleUp,
217 _ => crate::studies::PlotType::TriangleDown,
218 },
219 visible: true,
220 }
221 }
222
223 fn values(&self) -> &IndicatorValues {
224 &self.values
225 }
226
227 fn colors(&self) -> Vec<Color32> {
228 self.colors.to_vec()
229 }
230
231 fn set_colors(&mut self, colors: Vec<Color32>) {
232 for (old, new) in self.colors.iter_mut().zip(colors) {
233 *old = new;
234 }
235 }
236
237 fn pane(&self) -> crate::studies::StudyPane {
238 crate::studies::StudyPane {
239 levels: vec![(0.0, palette::GRAY, "Zero")],
240 ..Default::default()
241 }
242 }
243
244 fn is_overlay(&self) -> bool {
245 false
246 }
247
248 fn is_visible(&self) -> bool {
249 self.visible
250 }
251
252 fn set_visible(&mut self, visible: bool) {
253 self.visible = visible;
254 }
255
256 fn clone_box(&self) -> Box<dyn Indicator> {
257 Box::new(self.clone())
258 }
259
260 fn line_names(&self) -> Vec<String> {
261 ["Momentum", "Bullish Divergence", "Bearish Divergence"]
262 .map(String::from)
263 .to_vec()
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::studies::InputValue;
271
272 #[test]
279 fn divergences_need_pivots_in_range_and_an_rsi_extreme() {
280 let closes = [10.0, 8.0, 9.0, 7.5, 8.5];
281 let bullish = super::super::hlc_bars(&closes.map(|c| (c, c, c)));
282 let bearish = super::super::hlc_bars(&closes.map(|c| (-c, -c, -c)));
283 let none = None;
284 let signal = |bars: &[Bar], study: &KnoxvilleDivergence, line| {
285 let mut study = study.clone();
286 study.calculate(bars);
287 (0..bars.len())
288 .map(|at| study.values().value(line, at))
289 .collect::<Vec<_>>()
290 };
291 let study = KnoxvilleDivergence::new(1, 2, 1, 1, 10);
292 super::super::assert_replay_matches_batch(&study, &bullish);
293 super::super::assert_replay_matches_batch(&study, &bearish);
294 assert_eq!(
295 signal(&bullish, &study, 1),
296 [none, none, none, none, Some(-1.5)]
297 );
298 assert_eq!(signal(&bullish, &study, 2), [none; 5]);
299 assert_eq!(
300 signal(&bearish, &study, 2),
301 [none, none, none, none, Some(1.5)]
302 );
303 assert_eq!(signal(&bearish, &study, 1), [none; 5]);
304 let limit = |id: &str, value: usize| {
306 let mut study = study.clone();
307 study.set_input(id, InputValue::Length(value)).unwrap();
308 study
309 };
310 for (study, bars, line) in [
311 (KnoxvilleDivergence::new(1, 2, 1, 1, 1), &bullish, 1),
312 (limit("oversold", 10), &bullish, 1),
313 (limit("overbought", 90), &bearish, 2),
314 ] {
315 assert_eq!(signal(bars, &study, line), [none; 5]);
316 }
317 }
318}