1use crate::error::{Error, Result};
4use crate::indicators::roc::Roc;
5use crate::indicators::sma::Sma;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct KstOutput {
11 pub kst: f64,
13 pub signal: f64,
15}
16
17#[derive(Debug, Clone)]
45pub struct Kst {
46 roc1_period: usize,
47 roc2_period: usize,
48 roc3_period: usize,
49 roc4_period: usize,
50 sma1_period: usize,
51 sma2_period: usize,
52 sma3_period: usize,
53 sma4_period: usize,
54 signal_period: usize,
55 roc1: Roc,
56 roc2: Roc,
57 roc3: Roc,
58 roc4: Roc,
59 sma1: Sma,
60 sma2: Sma,
61 sma3: Sma,
62 sma4: Sma,
63 signal_sma: Sma,
64 last_line: Option<f64>,
65 last_signal: Option<f64>,
66}
67
68impl Kst {
69 #[allow(clippy::too_many_arguments)]
72 pub fn new(
73 roc1: usize,
74 roc2: usize,
75 roc3: usize,
76 roc4: usize,
77 sma1: usize,
78 sma2: usize,
79 sma3: usize,
80 sma4: usize,
81 signal: usize,
82 ) -> Result<Self> {
83 if [roc1, roc2, roc3, roc4, sma1, sma2, sma3, sma4, signal].contains(&0) {
84 return Err(Error::PeriodZero);
85 }
86 Ok(Self {
87 roc1_period: roc1,
88 roc2_period: roc2,
89 roc3_period: roc3,
90 roc4_period: roc4,
91 sma1_period: sma1,
92 sma2_period: sma2,
93 sma3_period: sma3,
94 sma4_period: sma4,
95 signal_period: signal,
96 roc1: Roc::new(roc1)?,
97 roc2: Roc::new(roc2)?,
98 roc3: Roc::new(roc3)?,
99 roc4: Roc::new(roc4)?,
100 sma1: Sma::new(sma1)?,
101 sma2: Sma::new(sma2)?,
102 sma3: Sma::new(sma3)?,
103 sma4: Sma::new(sma4)?,
104 signal_sma: Sma::new(signal)?,
105 last_line: None,
106 last_signal: None,
107 })
108 }
109
110 pub fn classic() -> Self {
112 Self::new(10, 15, 20, 30, 10, 10, 10, 15, 9).expect("classic KST parameters are valid")
113 }
114
115 pub const fn periods(
117 &self,
118 ) -> (
119 usize,
120 usize,
121 usize,
122 usize,
123 usize,
124 usize,
125 usize,
126 usize,
127 usize,
128 ) {
129 (
130 self.roc1_period,
131 self.roc2_period,
132 self.roc3_period,
133 self.roc4_period,
134 self.sma1_period,
135 self.sma2_period,
136 self.sma3_period,
137 self.sma4_period,
138 self.signal_period,
139 )
140 }
141}
142
143impl Indicator for Kst {
144 type Input = f64;
145 type Output = KstOutput;
146
147 #[inline]
148 fn update(&mut self, input: f64) -> Option<KstOutput> {
149 let r1 = self.roc1.update(input);
153 let r2 = self.roc2.update(input);
154 let r3 = self.roc3.update(input);
155 let r4 = self.roc4.update(input);
156 let rcma1 = r1.and_then(|x| self.sma1.update(x));
157 let rcma2 = r2.and_then(|x| self.sma2.update(x));
158 let rcma3 = r3.and_then(|x| self.sma3.update(x));
159 let rcma4 = r4.and_then(|x| self.sma4.update(x));
160 let (rcma1, rcma2, rcma3, rcma4) = (rcma1?, rcma2?, rcma3?, rcma4?);
161 let kst = rcma1 + 2.0 * rcma2 + 3.0 * rcma3 + 4.0 * rcma4;
162 self.last_line = Some(kst);
163 let signal = self.signal_sma.update(kst);
164 let signal = signal?;
165 self.last_signal = Some(signal);
166 Some(KstOutput { kst, signal })
167 }
168
169 fn reset(&mut self) {
170 self.roc1.reset();
171 self.roc2.reset();
172 self.roc3.reset();
173 self.roc4.reset();
174 self.sma1.reset();
175 self.sma2.reset();
176 self.sma3.reset();
177 self.sma4.reset();
178 self.signal_sma.reset();
179 self.last_line = None;
180 self.last_signal = None;
181 }
182
183 #[inline]
184 fn warmup_period(&self) -> usize {
185 let branch = |roc: usize, sma: usize| roc + sma;
190 let slowest = branch(self.roc1_period, self.sma1_period)
191 .max(branch(self.roc2_period, self.sma2_period))
192 .max(branch(self.roc3_period, self.sma3_period))
193 .max(branch(self.roc4_period, self.sma4_period));
194 slowest + self.signal_period - 1
195 }
196
197 #[inline]
198 fn is_ready(&self) -> bool {
199 self.last_signal.is_some()
200 }
201
202 #[inline]
203 fn name(&self) -> &'static str {
204 "KST"
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::traits::BatchExt;
212 use approx::assert_relative_eq;
213
214 #[test]
215 fn rejects_zero_period() {
216 assert!(matches!(
217 Kst::new(0, 15, 20, 30, 10, 10, 10, 15, 9),
218 Err(Error::PeriodZero)
219 ));
220 assert!(matches!(
221 Kst::new(10, 15, 20, 30, 10, 10, 10, 15, 0),
222 Err(Error::PeriodZero)
223 ));
224 }
225
226 #[test]
227 fn accessors_and_metadata() {
228 let kst = Kst::classic();
229 assert_eq!(kst.periods(), (10, 15, 20, 30, 10, 10, 10, 15, 9));
230 assert_eq!(kst.name(), "KST");
231 assert_eq!(kst.warmup_period(), 53);
233 }
234
235 #[test]
236 fn classic_factory_matches_pring_defaults() {
237 let kst = Kst::classic();
238 let (r1, r2, r3, r4, s1, s2, s3, s4, sig) = kst.periods();
239 assert_eq!((r1, r2, r3, r4), (10, 15, 20, 30));
240 assert_eq!((s1, s2, s3, s4), (10, 10, 10, 15));
241 assert_eq!(sig, 9);
242 }
243
244 #[test]
245 fn constant_series_yields_zero() {
246 let mut kst = Kst::classic();
249 let prices = vec![42.0_f64; 80];
250 let out = kst.batch(&prices);
251 for v in out.iter().skip(kst.warmup_period() - 1).flatten() {
252 assert_relative_eq!(v.kst, 0.0, epsilon = 1e-12);
253 assert_relative_eq!(v.signal, 0.0, epsilon = 1e-12);
254 }
255 }
256
257 #[test]
258 fn warmup_emits_first_value_at_warmup_period() {
259 let mut kst = Kst::new(2, 3, 4, 5, 2, 2, 2, 3, 2).unwrap();
260 assert_eq!(kst.warmup_period(), 9);
262 let prices: Vec<f64> = (1..=15).map(f64::from).collect();
263 let out = kst.batch(&prices);
264 for v in out.iter().take(8) {
265 assert!(v.is_none());
266 }
267 assert!(out[8].is_some());
268 }
269
270 #[test]
271 fn pure_uptrend_is_positive() {
272 let mut kst = Kst::classic();
274 let prices: Vec<f64> = (1..=120).map(|i| f64::from(i) * 2.0).collect();
275 let out = kst.batch(&prices);
276 let last = out.iter().rev().flatten().next().unwrap();
277 assert!(
278 last.kst > 0.0,
279 "KST on a clean uptrend should be positive: {}",
280 last.kst
281 );
282 assert!(last.signal > 0.0);
283 }
284
285 #[test]
286 fn batch_equals_streaming() {
287 let prices: Vec<f64> = (1..=120)
288 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0 + f64::from(i) * 0.1)
289 .collect();
290 let mut a = Kst::classic();
291 let mut b = Kst::classic();
292 assert_eq!(
293 a.batch(&prices),
294 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
295 );
296 }
297
298 #[test]
299 fn reset_clears_state() {
300 let mut kst = Kst::classic();
301 let prices: Vec<f64> = (1..=120).map(f64::from).collect();
302 kst.batch(&prices);
303 assert!(kst.is_ready());
304 kst.reset();
305 assert!(!kst.is_ready());
306 }
307}