Skip to main content

egui_charts/studies/builtin/
klinger.rs

1use super::smoothing::Smoothing;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Klinger Volume Oscillator, as defined by Stephen J. Klinger in "Identifying Trends With Volume Analysis", Technical Analysis of Stocks & Commodities, V.15:12 (December 1997).
9///
10/// The trend is up when a bar's high + low + close is above the previous bar's sum, down when it is below, and unchanged when the sums are equal. `dm` is the bar's range and `cm` the cumulative range of the current trend: the previous `cm` plus `dm` while the trend continues, the previous bar's `dm` plus `dm` when it changes. The volume force is `volume * |2 * (dm / cm - 1)| * trend * 100`, the oscillator is its short EMA minus its long EMA, and the signal line is an EMA of the oscillator.
11///
12/// There is no trend, and so no volume force, until a bar's sum first differs from the bar before. On that first trend bar `cm` is the bar's `dm`, and a bar with `cm = 0` has a volume force of 0. Each EMA starts at the mean of its first `n` inputs.
13///
14/// Values are `None` until the oscillator exists, then `[oscillator, signal]`, with the signal NaN until it exists.
15#[derive(Clone)]
16pub struct KlingerOscillator {
17    /// Short EMA period (typically 34)
18    short_period: usize,
19    /// Long EMA period (typically 55)
20    long_period: usize,
21    /// Signal line period (typically 13)
22    signal_period: usize,
23    values: IndicatorValues,
24    stepper: Stepper<State>,
25    /// Colors: [Klinger, Signal]
26    colors: Vec<Color32>,
27    visible: bool,
28}
29
30#[derive(Clone, Default)]
31struct State {
32    previous: Option<[f64; 3]>,
33    trend: f64,
34    cm: f64,
35    ema: [Smoothing; 3],
36}
37
38impl KlingerOscillator {
39    /// Create with short EMA 34, long EMA 55 and signal EMA 13.
40    pub fn new() -> Self {
41        Self {
42            short_period: 34,
43            long_period: 55,
44            signal_period: 13,
45            values: IndicatorValues::new(),
46            stepper: Stepper::default(),
47            colors: vec![
48                palette::BLUE,       // Klinger
49                palette::DARK_GREEN, // Signal
50            ],
51            visible: true,
52        }
53    }
54
55    /// Set the short, long and signal EMA periods in bars, each at least one.
56    pub fn with_periods(mut self, short: usize, long: usize, signal: usize) -> Self {
57        self.short_period = short.max(1);
58        self.long_period = long.max(1);
59        self.signal_period = signal.max(1);
60        self
61    }
62
63    /// Set plot colors in the order returned by [`Indicator::line_names`].
64    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
65        self.colors = colors;
66        self
67    }
68}
69
70impl Default for KlingerOscillator {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl Indicator for KlingerOscillator {
77    crate::studies::inputs::study_inputs!(self;
78        short_period: length("Short period", 1, usize::MAX),
79        long_period: length("Long period", 1, usize::MAX),
80        signal_period: length("Signal period", 1, usize::MAX)
81    ; {});
82
83    fn value_format(&self) -> crate::scales::StudyFormat {
84        crate::scales::StudyFormat::Volume
85    }
86
87    fn name(&self) -> &str {
88        "Klinger"
89    }
90
91    fn desc(&self) -> &str {
92        "Klinger Volume Oscillator - Long-term money flow trend"
93    }
94
95    fn calculate(&mut self, data: &[Bar]) {
96        self.update(data, 0);
97    }
98
99    fn update(&mut self, data: &[Bar], from: usize) {
100        self.stepper
101            .run(data, from, &mut self.values, |state, bar| {
102                if ![bar.high, bar.low, bar.close, bar.volume]
103                    .iter()
104                    .all(|x| x.is_finite())
105                {
106                    return IndicatorValue::None;
107                }
108                let Some(previous) = state.previous.replace([bar.high, bar.low, bar.close]) else {
109                    return IndicatorValue::None;
110                };
111                let sum = bar.high + bar.low + bar.close;
112                let previous_sum = previous[0] + previous[1] + previous[2];
113                let trend = if sum > previous_sum {
114                    1.0
115                } else if sum < previous_sum {
116                    -1.0
117                } else {
118                    state.trend
119                };
120                if trend == 0.0 {
121                    return IndicatorValue::None;
122                }
123                let dm = bar.high - bar.low;
124                state.cm = if state.trend == 0.0 {
125                    dm
126                } else if trend == state.trend {
127                    state.cm + dm
128                } else {
129                    previous[0] - previous[1] + dm
130                };
131                state.trend = trend;
132                let force = if state.cm == 0.0 {
133                    0.0
134                } else {
135                    bar.volume * (2.0 * (dm / state.cm - 1.0)).abs() * trend * 100.0
136                };
137                let short = state.ema[0].next(
138                    force,
139                    self.short_period,
140                    2.0 / (self.short_period as f64 + 1.0),
141                );
142                let long = state.ema[1].next(
143                    force,
144                    self.long_period,
145                    2.0 / (self.long_period as f64 + 1.0),
146                );
147                let kvo = short - long;
148                let signal = state.ema[2].next(
149                    kvo,
150                    self.signal_period,
151                    2.0 / (self.signal_period as f64 + 1.0),
152                );
153                if kvo.is_nan() {
154                    IndicatorValue::None
155                } else {
156                    IndicatorValue::Multiple(vec![kvo, signal])
157                }
158            });
159    }
160
161    fn is_incremental(&self) -> bool {
162        true
163    }
164
165    fn values(&self) -> &IndicatorValues {
166        &self.values
167    }
168
169    fn colors(&self) -> Vec<Color32> {
170        self.colors.clone()
171    }
172
173    fn set_colors(&mut self, colors: Vec<Color32>) {
174        if colors.len() >= 2 {
175            self.colors = colors;
176        }
177    }
178
179    fn is_overlay(&self) -> bool {
180        false
181    }
182
183    fn is_visible(&self) -> bool {
184        self.visible
185    }
186
187    fn set_visible(&mut self, visible: bool) {
188        self.visible = visible;
189    }
190
191    fn clone_box(&self) -> Box<dyn Indicator> {
192        Box::new(self.clone())
193    }
194
195    fn line_count(&self) -> usize {
196        2
197    }
198
199    fn line_names(&self) -> Vec<String> {
200        vec![
201            format!("KVO({},{})", self.short_period, self.long_period),
202            format!("Signal({})", self.signal_period),
203        ]
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use chrono::Utc;
211
212    fn make_bar(high: f64, low: f64, close: f64, volume: f64) -> Bar {
213        Bar {
214            time: Utc::now(),
215            open: (high + low) / 2.0,
216            high,
217            low,
218            close,
219            volume,
220            colors: None,
221            payload: None,
222        }
223    }
224
225    /// Expected values follow Stephen J. Klinger, "Identifying Trends With Volume Analysis",
226    /// Technical Analysis of Stocks & Commodities, V.15:12 (December 1997), computed
227    /// independently with this file's EMA seeding (the mean of the first `n` inputs).
228    #[test]
229    fn matches_klingers_definition() {
230        let mut data: Vec<Bar> = (0..80)
231            .map(|i| {
232                let close = 100.0 + ((7 * i) % 11) as f64 * 0.5 + 0.25 * i as f64;
233                make_bar(
234                    close + 1.0 + (i % 3) as f64 * 0.5,
235                    close - 1.0 - (i % 2) as f64 * 0.5,
236                    close,
237                    1000.0 + 250.0 * (i % 4) as f64,
238                )
239            })
240            .collect();
241        // No range on bars 0 and 1 gives cm = 0 on the first trend bar; bar 40 repeats the prices of
242        // bar 39, whose trend is up, so equal sums must keep that trend.
243        for bar in &mut data[..2] {
244            bar.high = bar.close;
245            bar.low = bar.close;
246        }
247        data[40] = Bar {
248            volume: data[40].volume,
249            ..data[39].clone()
250        };
251
252        // (KVO, signal) for bars 55 to 79; the trend flips many times along the series.
253        let expected = [
254            (-7848.617481565998, f64::NAN),
255            (-4339.64602772666, f64::NAN),
256            (-6205.425115696111, f64::NAN),
257            (-1752.73323152573, f64::NAN),
258            (-4040.127175139147, f64::NAN),
259            (-6200.828960250648, f64::NAN),
260            (-2795.50613638723, f64::NAN),
261            (-5050.879964616739, f64::NAN),
262            (-9170.953041291119, f64::NAN),
263            (-5461.165195491776, f64::NAN),
264            (-6452.141418963016, f64::NAN),
265            (-9929.238900876226, f64::NAN),
266            (-5248.866838205729, -5730.471499056624),
267            (-6125.694142781424, -5786.931876731596),
268            (-1845.4547310008347, -5223.863713055774),
269            (-4086.5283414884398, -5061.387231403298),
270            (-7313.215017025366, -5383.076915063593),
271            (-3092.9035000634976, -5055.9092843492945),
272            (-4119.682994057504, -4922.162671450468),
273            (-6919.806629935592, -5207.540379805487),
274            (-1342.7450352586602, -4655.42675915594),
275            (-2503.1976252211534, -4347.965454308113),
276            (-4561.556968612778, -4378.478527780208),
277            (818.4952172891135, -3636.05370705602),
278            (-1382.6286621873223, -3314.135843503349),
279        ];
280
281        let mut klinger = KlingerOscillator::new();
282        klinger.calculate(&data);
283
284        let values = klinger.values().to_rows();
285        assert_eq!(values.len(), 80);
286        assert!(
287            values[..55]
288                .iter()
289                .all(|v| matches!(v, IndicatorValue::None))
290        );
291        let close = |x: f64, y: f64| (x.is_nan() && y.is_nan()) || (x - y).abs() <= 1e-9 * y.abs();
292        for (offset, &(kvo, signal)) in expected.iter().enumerate() {
293            let bar = 55 + offset;
294            match &values[bar] {
295                IndicatorValue::Multiple(v) => {
296                    assert_eq!(v.len(), 2, "bar {bar}");
297                    assert!(close(v[0], kvo), "bar {bar} KVO: {} != {kvo}", v[0]);
298                    assert!(
299                        close(v[1], signal),
300                        "bar {bar} signal: {} != {signal}",
301                        v[1]
302                    );
303                }
304                other => panic!("bar {bar}: expected two lines, got {other:?}"),
305            }
306        }
307        assert_eq!(klinger.line_count(), 2);
308
309        // A bar with a non-finite price has no value; the study carries on after it.
310        data[60].high = f64::NAN;
311        klinger.calculate(&data);
312        assert!(matches!(
313            klinger.values().to_rows()[60],
314            IndicatorValue::None
315        ));
316        assert!(klinger.values().to_rows()[61..].iter().all(|v| matches!(
317            v,
318            IndicatorValue::Multiple(l) if l[0].is_finite()
319        )));
320
321        // One bar short of 55 volume force values: no oscillator yet.
322        klinger.calculate(&data[..55]);
323        assert!(
324            klinger
325                .values()
326                .to_rows()
327                .iter()
328                .all(|v| matches!(v, IndicatorValue::None))
329        );
330    }
331}