Skip to main content

kestrel_chartkit/indicator/
kst.rs

1use super::smoothing::Sma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::{HashMap, VecDeque};
5
6/// Know Sure Thing with fixed windows.
7///
8/// `KST = SMA10(ROC10) + 2 SMA10(ROC15) + 3 SMA10(ROC20) + 4 SMA15(ROC30)`, each ROC in percent of
9/// its past close; `extra["signal"]` is `SMA9(KST)`, `extra["hist"]` is `KST - signal` and
10/// `extra["kst"]` repeats the value. There are no parameters.
11///
12/// First output: once the signal exists, i.e. with the 53rd bar. [`Indicator::reset`] clears all
13/// averages.
14#[derive(Debug, Clone)]
15pub struct KstEngine {
16    closes: VecDeque<f64>,
17    sma1: Sma,
18    sma2: Sma,
19    sma3: Sma,
20    sma4: Sma,
21    signal_sma: Sma,
22}
23
24impl KstEngine {
25    pub fn new() -> Self {
26        Self {
27            closes: VecDeque::with_capacity(31),
28            sma1: Sma::new(10),
29            sma2: Sma::new(10),
30            sma3: Sma::new(10),
31            sma4: Sma::new(15),
32            signal_sma: Sma::new(9),
33        }
34    }
35}
36
37impl Default for KstEngine {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl Indicator for KstEngine {
44    fn name(&self) -> &str {
45        "kst"
46    }
47
48    fn warmup_period(&self) -> usize {
49        54
50    }
51
52    fn reset(&mut self) {
53        self.closes.clear();
54        self.sma1.reset();
55        self.sma2.reset();
56        self.sma3.reset();
57        self.sma4.reset();
58        self.signal_sma.reset();
59    }
60
61    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
62        self.closes.push_back(bar.close);
63        if self.closes.len() > 31 {
64            self.closes.pop_front();
65        }
66
67        if self.closes.len() < 31 {
68            return None;
69        }
70
71        let c_curr = bar.close;
72        let c_10 = self.closes[self.closes.len() - 11];
73        let c_15 = self.closes[self.closes.len() - 16];
74        let c_20 = self.closes[self.closes.len() - 21];
75        let c_30 = self.closes[self.closes.len() - 31];
76
77        let roc10 = if c_10 > 0.0 {
78            (c_curr - c_10) / c_10 * 100.0
79        } else {
80            0.0
81        };
82        let roc15 = if c_15 > 0.0 {
83            (c_curr - c_15) / c_15 * 100.0
84        } else {
85            0.0
86        };
87        let roc20 = if c_20 > 0.0 {
88            (c_curr - c_20) / c_20 * 100.0
89        } else {
90            0.0
91        };
92        let roc30 = if c_30 > 0.0 {
93            (c_curr - c_30) / c_30 * 100.0
94        } else {
95            0.0
96        };
97
98        let rc1 = self.sma1.update(roc10);
99        let rc2 = self.sma2.update(roc15);
100        let rc3 = self.sma3.update(roc20);
101        let rc4 = self.sma4.update(roc30);
102
103        let (r1, r2, r3, r4) = match (rc1, rc2, rc3, rc4) {
104            (Some(r1), Some(r2), Some(r3), Some(r4)) => (r1, r2, r3, r4),
105            _ => return None,
106        };
107
108        let kst_val = r1 * 1.0 + r2 * 2.0 + r3 * 3.0 + r4 * 4.0;
109        let sig_val = self.signal_sma.update(kst_val)?;
110
111        let mut extra = HashMap::new();
112        extra.insert("kst".to_string(), kst_val);
113        extra.insert("signal".to_string(), sig_val);
114        extra.insert("hist".to_string(), kst_val - sig_val);
115
116        Some(IndicatorOutput::with_extra(kst_val, extra))
117    }
118
119    fn alerts(&self) -> Vec<IndicatorAlert> {
120        Vec::new()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_kst_basic() {
130        let mut kst = KstEngine::new();
131        let mut out = None;
132        for i in 0..100 {
133            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
134            out = kst.on_bar(&b);
135        }
136        assert!(out.is_some());
137    }
138}