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 fn update(&mut self, input: f64) -> Option<KstOutput> {
148 let r1 = self.roc1.update(input);
152 let r2 = self.roc2.update(input);
153 let r3 = self.roc3.update(input);
154 let r4 = self.roc4.update(input);
155 let rcma1 = r1.and_then(|x| self.sma1.update(x));
156 let rcma2 = r2.and_then(|x| self.sma2.update(x));
157 let rcma3 = r3.and_then(|x| self.sma3.update(x));
158 let rcma4 = r4.and_then(|x| self.sma4.update(x));
159 let (rcma1, rcma2, rcma3, rcma4) = (rcma1?, rcma2?, rcma3?, rcma4?);
160 let kst = rcma1 + 2.0 * rcma2 + 3.0 * rcma3 + 4.0 * rcma4;
161 self.last_line = Some(kst);
162 let signal = self.signal_sma.update(kst);
163 let signal = signal?;
164 self.last_signal = Some(signal);
165 Some(KstOutput { kst, signal })
166 }
167
168 fn reset(&mut self) {
169 self.roc1.reset();
170 self.roc2.reset();
171 self.roc3.reset();
172 self.roc4.reset();
173 self.sma1.reset();
174 self.sma2.reset();
175 self.sma3.reset();
176 self.sma4.reset();
177 self.signal_sma.reset();
178 self.last_line = None;
179 self.last_signal = None;
180 }
181
182 fn warmup_period(&self) -> usize {
183 let branch = |roc: usize, sma: usize| roc + sma;
188 let slowest = branch(self.roc1_period, self.sma1_period)
189 .max(branch(self.roc2_period, self.sma2_period))
190 .max(branch(self.roc3_period, self.sma3_period))
191 .max(branch(self.roc4_period, self.sma4_period));
192 slowest + self.signal_period - 1
193 }
194
195 fn is_ready(&self) -> bool {
196 self.last_signal.is_some()
197 }
198
199 fn name(&self) -> &'static str {
200 "KST"
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::traits::BatchExt;
208 use approx::assert_relative_eq;
209
210 #[test]
211 fn rejects_zero_period() {
212 assert!(matches!(
213 Kst::new(0, 15, 20, 30, 10, 10, 10, 15, 9),
214 Err(Error::PeriodZero)
215 ));
216 assert!(matches!(
217 Kst::new(10, 15, 20, 30, 10, 10, 10, 15, 0),
218 Err(Error::PeriodZero)
219 ));
220 }
221
222 #[test]
223 fn accessors_and_metadata() {
224 let kst = Kst::classic();
225 assert_eq!(kst.periods(), (10, 15, 20, 30, 10, 10, 10, 15, 9));
226 assert_eq!(kst.name(), "KST");
227 assert_eq!(kst.warmup_period(), 53);
229 }
230
231 #[test]
232 fn classic_factory_matches_pring_defaults() {
233 let kst = Kst::classic();
234 let (r1, r2, r3, r4, s1, s2, s3, s4, sig) = kst.periods();
235 assert_eq!((r1, r2, r3, r4), (10, 15, 20, 30));
236 assert_eq!((s1, s2, s3, s4), (10, 10, 10, 15));
237 assert_eq!(sig, 9);
238 }
239
240 #[test]
241 fn constant_series_yields_zero() {
242 let mut kst = Kst::classic();
245 let prices = vec![42.0_f64; 80];
246 let out = kst.batch(&prices);
247 for v in out.iter().skip(kst.warmup_period() - 1).flatten() {
248 assert_relative_eq!(v.kst, 0.0, epsilon = 1e-12);
249 assert_relative_eq!(v.signal, 0.0, epsilon = 1e-12);
250 }
251 }
252
253 #[test]
254 fn warmup_emits_first_value_at_warmup_period() {
255 let mut kst = Kst::new(2, 3, 4, 5, 2, 2, 2, 3, 2).unwrap();
256 assert_eq!(kst.warmup_period(), 9);
258 let prices: Vec<f64> = (1..=15).map(f64::from).collect();
259 let out = kst.batch(&prices);
260 for v in out.iter().take(8) {
261 assert!(v.is_none());
262 }
263 assert!(out[8].is_some());
264 }
265
266 #[test]
267 fn pure_uptrend_is_positive() {
268 let mut kst = Kst::classic();
270 let prices: Vec<f64> = (1..=120).map(|i| f64::from(i) * 2.0).collect();
271 let out = kst.batch(&prices);
272 let last = out.iter().rev().flatten().next().unwrap();
273 assert!(
274 last.kst > 0.0,
275 "KST on a clean uptrend should be positive: {}",
276 last.kst
277 );
278 assert!(last.signal > 0.0);
279 }
280
281 #[test]
282 fn batch_equals_streaming() {
283 let prices: Vec<f64> = (1..=120)
284 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0 + f64::from(i) * 0.1)
285 .collect();
286 let mut a = Kst::classic();
287 let mut b = Kst::classic();
288 assert_eq!(
289 a.batch(&prices),
290 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
291 );
292 }
293
294 #[test]
295 fn reset_clears_state() {
296 let mut kst = Kst::classic();
297 let prices: Vec<f64> = (1..=120).map(f64::from).collect();
298 kst.batch(&prices);
299 assert!(kst.is_ready());
300 kst.reset();
301 assert!(!kst.is_ready());
302 }
303}