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)]
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}