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