1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::Stepper;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7#[derive(Clone)]
17pub struct KDJ {
18 period: usize,
20 k_smooth: usize,
22 d_smooth: usize,
24 values: IndicatorValues,
25 stepper: Stepper<State>,
26 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 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, palette::ORANGE, palette::PURPLE, ],
51 visible: true,
52 }
53 }
54
55 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 #[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 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 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 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}