kestrel_chartkit/indicator/
kst.rs1use super::smoothing::Sma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::{HashMap, VecDeque};
5
6#[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}