Skip to main content

egui_charts/studies/builtin/
kdj.rs

1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::Stepper;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7/// KDJ, the stochastic oscillator as used in Chinese securities analysis.
8///
9/// The raw stochastic value of each bar is `RSV = (close - lowest low) / (highest high - lowest low) * 100`
10/// over the last `period` bars, or 50 when that window has no range. K and D are smoothed recursively,
11/// both starting from 50: `K = ((k_smooth - 1) * K[-1] + RSV) / k_smooth` and
12/// `D = ((d_smooth - 1) * D[-1] + K) / d_smooth`, so the default lengths of 3 weight the new value by a
13/// third. `J = 3K - 2D`. The first value is on bar `period - 1`; earlier bars are `None`.
14/// A bar with a non-finite high, low or close has no value and is left out: the windows span usable
15/// bars only, so every other bar is that of the series without it.
16#[derive(Clone)]
17pub struct KDJ {
18    /// Period for %K calculation (typically 9)
19    period: usize,
20    /// Smoothing period for %K (typically 3)
21    k_smooth: usize,
22    /// Smoothing period for %D (typically 3)
23    d_smooth: usize,
24    values: IndicatorValues,
25    stepper: Stepper<State>,
26    /// Colors: [K, D, J]
27    colors: Vec<Color32>,
28    visible: bool,
29}
30
31#[derive(Clone, Default)]
32struct State {
33    range: std::collections::VecDeque<[f64; 2]>,
34    smoothed: Option<(f64, f64)>,
35}
36
37impl KDJ {
38    /// Create with `period`, `k_smooth`, `d_smooth` in bars, each clamped to at least one.
39    pub fn new(period: usize, k_smooth: usize, d_smooth: usize) -> Self {
40        Self {
41            period: period.max(1),
42            k_smooth: k_smooth.max(1),
43            d_smooth: d_smooth.max(1),
44            values: IndicatorValues::new(),
45            stepper: Stepper::default(),
46            colors: vec![
47                palette::BLUE,   // Blue - K
48                palette::ORANGE, // Orange - D
49                palette::PURPLE, // Purple - J
50            ],
51            visible: true,
52        }
53    }
54
55    /// Set plot colors in the order returned by [`Indicator::line_names`].
56    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
57        self.colors = colors;
58        self
59    }
60}
61
62impl Default for KDJ {
63    fn default() -> Self {
64        Self::new(9, 3, 3)
65    }
66}
67
68impl Indicator for KDJ {
69    crate::studies::inputs::study_inputs!(self;
70        period: length("Period", 1, usize::MAX),
71        k_smooth: length("K smooth", 1, usize::MAX),
72        d_smooth: length("D smooth", 1, usize::MAX)
73    ; {});
74
75    fn name(&self) -> &str {
76        "KDJ"
77    }
78
79    fn desc(&self) -> &str {
80        "KDJ - Stochastic-derived indicator with K, D, J lines"
81    }
82
83    fn calculate(&mut self, data: &[Bar]) {
84        self.update(data, 0);
85    }
86
87    fn update(&mut self, data: &[Bar], from: usize) {
88        self.stepper
89            .run(data, from, &mut self.values, |state, bar| {
90                if ![bar.high, bar.low, bar.close].iter().all(|x| x.is_finite()) {
91                    return IndicatorValue::None;
92                }
93                state.range.push_back([bar.high, bar.low]);
94                if state.range.len() > self.period {
95                    state.range.pop_front();
96                }
97                if state.range.len() < self.period {
98                    return IndicatorValue::None;
99                }
100                let high = state
101                    .range
102                    .iter()
103                    .map(|v| v[0])
104                    .fold(f64::NEG_INFINITY, f64::max);
105                let low = state
106                    .range
107                    .iter()
108                    .map(|v| v[1])
109                    .fold(f64::INFINITY, f64::min);
110                let range = high - low;
111                if !range.is_finite() {
112                    return IndicatorValue::None;
113                }
114                let rsv = if range == 0.0 {
115                    50.0
116                } else {
117                    (bar.close - low) / range * 100.0
118                };
119                let (k, d) = state.smoothed.get_or_insert((50.0, 50.0));
120                let (k_len, d_len) = (self.k_smooth as f64, self.d_smooth as f64);
121                *k = ((k_len - 1.0) * *k + rsv) / k_len;
122                *d = ((d_len - 1.0) * *d + *k) / d_len;
123                IndicatorValue::Multiple(vec![*k, *d, 3.0 * *k - 2.0 * *d])
124            });
125    }
126
127    fn is_incremental(&self) -> bool {
128        true
129    }
130
131    fn values(&self) -> &IndicatorValues {
132        &self.values
133    }
134
135    fn colors(&self) -> Vec<Color32> {
136        self.colors.clone()
137    }
138
139    fn set_colors(&mut self, colors: Vec<Color32>) {
140        if colors.len() >= 3 {
141            self.colors = colors;
142        }
143    }
144
145    fn is_overlay(&self) -> bool {
146        false
147    }
148
149    fn is_visible(&self) -> bool {
150        self.visible
151    }
152
153    fn set_visible(&mut self, visible: bool) {
154        self.visible = visible;
155    }
156
157    fn clone_box(&self) -> Box<dyn Indicator> {
158        Box::new(self.clone())
159    }
160
161    fn line_count(&self) -> usize {
162        3
163    }
164
165    fn line_names(&self) -> Vec<String> {
166        vec![
167            format!("K({})", self.period),
168            format!("D({})", self.d_smooth),
169            "J".to_string(),
170        ]
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::studies::test_support::make_range_bar as make_bar;
178
179    /// Reference values from the published KDJ formula, computed in exact rational arithmetic:
180    /// `RSV = (C - LLV(L, N)) / (HHV(H, N) - LLV(L, N)) * 100`, `K = ((M1 - 1) * K[-1] + RSV) / M1`,
181    /// `D = ((M2 - 1) * D[-1] + K) / M2`, both 50 before the first value, and `J = 3K - 2D`.
182    #[test]
183    fn kdj_matches_published_recursion() {
184        let data: Vec<Bar> = [
185            (102.0, 98.0, 100.0),
186            (104.0, 99.0, 103.0),
187            (106.0, 101.0, 104.0),
188            (105.0, 100.0, 101.0),
189            (103.0, 97.0, 98.0),
190            (104.0, 99.0, 103.0),
191            (101.0, 101.0, 101.0),
192            (101.0, 101.0, 101.0),
193            (101.0, 101.0, 101.0),
194            (108.0, 100.0, 107.0),
195            (110.0, 104.0, 105.0),
196            (109.0, 103.0, 104.0),
197            (107.0, 102.0, 106.0),
198            (112.0, 105.0, 111.0),
199            (111.0, 106.0, 107.0),
200        ]
201        .iter()
202        .map(|&(h, l, c)| make_bar(h, l, c))
203        .collect();
204        // Bar 2: RSV = (104 - 98) / (106 - 98) * 100 = 75, K = 50 * 2/3 + 75 / 3, D = 50 / 2 + K / 2.
205        // Bar 8 closes a window with no range, where RSV is 50.
206        let expected: [Option<[f64; 3]>; 15] = [
207            None,
208            None,
209            Some([58.333333333333336, 54.166666666666664, 66.66666666666667]),
210            Some([48.41269841269841, 51.28968253968254, 42.65873015873016]),
211            Some([35.97883597883598, 43.63425925925926, 20.667989417989418]),
212            Some([48.985890652557316, 46.31007495590829, 54.33752204585538]),
213            Some([51.70487948265726, 49.00747721928278, 57.09968400940623]),
214            Some([47.803252988438174, 48.405365103860476, 46.59902875759357]),
215            Some([48.53550199229212, 48.470433548076294, 48.665638880723755]),
216            Some([61.52366799486141, 54.99705077146885, 74.57690244164652]),
217            Some([57.68244532990761, 56.33974805068823, 60.36783988834636]),
218            Some([51.788296886605075, 54.06402246864665, 47.23684572252191]),
219            Some([51.19219792440338, 52.628110196525014, 48.32037338016011]),
220            Some([64.12813194960225, 58.37812107306363, 75.6281537026795]),
221            Some([59.41875463306817, 58.8984378530659, 60.459388193072705]),
222        ];
223
224        // The same bars with a NaN close on bar 9 and an infinite high on bar 13: those two bars have no
225        // value and the others are those of the series without them.
226        let mut hostile = data.clone();
227        hostile[9].close = f64::NAN;
228        hostile[13].high = f64::INFINITY;
229        let mut hostile_expected = expected;
230        hostile_expected[9] = None;
231        hostile_expected[10] = Some([47.17181614300956, 47.821124845542926, 45.87319873794282]);
232        hostile_expected[11] = Some([42.55898853978415, 45.19005669266354, 37.29685223402537]);
233        hostile_expected[12] = Some([45.03932569318943, 45.114691192926486, 44.888594693715326]);
234        hostile_expected[13] = None;
235        hostile_expected[14] = Some([48.54473564731148, 46.82971342011898, 51.97478010169646]);
236
237        // Distinct K and D lengths so that swapping their weights fails.
238        let mut kdj = KDJ::new(3, 3, 2);
239        for (case, data, expected) in [
240            ("finite", &data, expected),
241            ("non-finite", &hostile, hostile_expected),
242        ] {
243            kdj.calculate(data);
244            assert_eq!(kdj.values().len(), expected.len(), "{case}");
245            for (i, (value, want)) in kdj.values().to_rows().iter().zip(expected).enumerate() {
246                match (value, want) {
247                    (IndicatorValue::None, None) => {}
248                    (IndicatorValue::Multiple(got), Some(want)) => {
249                        assert_eq!(got.len(), kdj.line_count(), "{case} bar {i}");
250                        for (g, w) in got.iter().zip(want) {
251                            assert!(
252                                (g - w).abs() < 1e-12,
253                                "{case} bar {i}: got {got:?}, want {want:?}"
254                            );
255                        }
256                    }
257                    _ => panic!("{case} bar {i}: got {value:?}, want {want:?}"),
258                }
259            }
260        }
261    }
262}