wickra_core/indicators/
kase_permission_stochastic.rs1use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::indicators::ema::Ema;
8use crate::ohlcv::Candle;
9use crate::traits::Indicator;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct KasePermissionStochasticOutput {
14 pub fast: f64,
16 pub slow: f64,
18}
19
20#[derive(Debug, Clone)]
55pub struct KasePermissionStochastic {
56 length: usize,
57 smooth: usize,
58 window: VecDeque<(f64, f64)>,
59 fast_ema: Ema,
60 slow_ema: Ema,
61}
62
63impl KasePermissionStochastic {
64 pub fn new(length: usize, smooth: usize) -> Result<Self> {
71 if length == 0 {
72 return Err(Error::PeriodZero);
73 }
74 if length > crate::error::MAX_PERIOD {
75 return Err(Error::InvalidPeriod {
76 message: crate::error::PERIOD_ABOVE_MAX,
77 });
78 }
79 Ok(Self {
80 length,
81 smooth,
82 window: VecDeque::with_capacity(length),
83 fast_ema: Ema::new(smooth)?,
84 slow_ema: Ema::new(smooth)?,
85 })
86 }
87
88 pub fn classic() -> Self {
90 Self::new(9, 3).expect("classic Kase Permission Stochastic parameters are valid")
91 }
92
93 pub const fn periods(&self) -> (usize, usize) {
95 (self.length, self.smooth)
96 }
97}
98
99impl Indicator for KasePermissionStochastic {
100 type Input = Candle;
101 type Output = KasePermissionStochasticOutput;
102
103 #[inline]
104 fn update(&mut self, candle: Candle) -> Option<KasePermissionStochasticOutput> {
105 self.window.push_back((candle.high, candle.low));
106 if self.window.len() > self.length {
107 self.window.pop_front();
108 }
109 if self.window.len() < self.length {
110 return None;
111 }
112
113 let highest = self.window.iter().map(|w| w.0).fold(f64::MIN, f64::max);
114 let lowest = self.window.iter().map(|w| w.1).fold(f64::MAX, f64::min);
115 let raw_k = if highest > lowest {
116 100.0 * (candle.close - lowest) / (highest - lowest)
117 } else {
118 50.0
119 };
120
121 let fast = self.fast_ema.update(raw_k)?;
122 let slow = self.slow_ema.update(fast)?;
123 Some(KasePermissionStochasticOutput { fast, slow })
124 }
125
126 fn reset(&mut self) {
127 self.window.clear();
128 self.fast_ema.reset();
129 self.slow_ema.reset();
130 }
131
132 #[inline]
133 fn warmup_period(&self) -> usize {
134 self.length + 2 * self.smooth - 2
136 }
137
138 #[inline]
139 fn is_ready(&self) -> bool {
140 self.slow_ema.is_ready()
141 }
142
143 #[inline]
144 fn name(&self) -> &'static str {
145 "KasePermissionStochastic"
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::traits::BatchExt;
153 use approx::assert_relative_eq;
154
155 fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
156 Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
157 }
158
159 #[test]
160 fn rejects_zero_period() {
161 assert!(matches!(
162 KasePermissionStochastic::new(0, 3),
163 Err(Error::PeriodZero)
164 ));
165 assert!(matches!(
166 KasePermissionStochastic::new(9, 0),
167 Err(Error::PeriodZero)
168 ));
169 }
170
171 #[test]
172 fn accessors_and_metadata() {
173 let k = KasePermissionStochastic::classic();
174 assert_eq!(k.periods(), (9, 3));
175 assert_eq!(k.warmup_period(), 13);
177 assert_eq!(k.name(), "KasePermissionStochastic");
178 assert!(!k.is_ready());
179 }
180
181 #[test]
182 fn warmup_emits_at_expected_bar() {
183 let mut k = KasePermissionStochastic::new(3, 2).unwrap();
184 let candles: Vec<Candle> = (0..8).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
186 let out = k.batch(&candles);
187 assert!(out[3].is_none());
188 assert!(out[4].is_some());
189 }
190
191 #[test]
192 fn top_of_range_is_high() {
193 let mut k = KasePermissionStochastic::new(5, 3).unwrap();
196 let candles: Vec<Candle> = (0_i64..40)
197 .map(|i| {
198 let base = 100.0 + i as f64;
199 candle(base + 2.0, base - 2.0, base + 2.0, i)
200 })
201 .collect();
202 let last = k.batch(&candles).last().unwrap().unwrap();
203 assert!(last.fast > 80.0, "fast {} should be high", last.fast);
204 assert!(last.slow > 80.0, "slow {} should be high", last.slow);
205 }
206
207 #[test]
208 fn flat_window_defaults_to_neutral() {
209 let mut k = KasePermissionStochastic::new(4, 2).unwrap();
212 let candles: Vec<Candle> = (0..20).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
213 let last = k.batch(&candles).last().unwrap().unwrap();
214 assert_relative_eq!(last.fast, 50.0, epsilon = 1e-9);
215 assert_relative_eq!(last.slow, 50.0, epsilon = 1e-9);
216 }
217
218 #[test]
219 fn reset_clears_state() {
220 let mut k = KasePermissionStochastic::classic();
221 let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
222 k.batch(&candles);
223 assert!(k.is_ready());
224 k.reset();
225 assert!(!k.is_ready());
226 }
227
228 #[test]
229 fn batch_equals_streaming() {
230 let candles: Vec<Candle> = (0..80_i64)
231 .map(|i| {
232 let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
233 candle(base + 2.0, base - 2.0, base + (i as f64 * 0.3).cos(), i)
234 })
235 .collect();
236 let mut a = KasePermissionStochastic::classic();
237 let mut b = KasePermissionStochastic::classic();
238 assert_eq!(
239 a.batch(&candles),
240 candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
241 );
242 }
243}