Skip to main content

wickra_core/indicators/
kase_permission_stochastic.rs

1//! Kase Permission Stochastic — a double-smoothed stochastic used as a
2//! trade-permission filter.
3
4use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::indicators::ema::Ema;
8use crate::ohlcv::Candle;
9use crate::traits::Indicator;
10
11/// Kase Permission Stochastic output: a fast and a slow line.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct KasePermissionStochasticOutput {
14    /// Fast line: EMA of the raw `%K` over the smoothing period.
15    pub fast: f64,
16    /// Slow line: EMA of the fast line over the smoothing period.
17    pub slow: f64,
18}
19
20/// Cynthia Kase's Permission Stochastic: a stochastic oscillator smoothed twice,
21/// whose fast/slow relationship grants or denies "permission" to trade in the
22/// direction of a higher-timeframe signal.
23///
24/// ```text
25/// raw%K = 100 * (close - LL) / (HH - LL)     over `length` (50 when HH == LL)
26/// fast  = EMA(raw%K, smooth)
27/// slow  = EMA(fast,  smooth)
28/// ```
29///
30/// The raw stochastic is the usual `%K`, then an EMA produces the *fast* line
31/// and a second EMA of that produces the *slow* line. Kase uses the pair as a
32/// gate: a fast line above the slow line (and rising) gives permission for
33/// longs, the reverse for shorts. When the lookback window is perfectly flat
34/// (`HH == LL`), the raw stochastic is undefined and defaults to the neutral
35/// `50`.
36///
37/// Reference: Cynthia Kase, *Trading with the Odds*, 1996.
38///
39/// # Example
40///
41/// ```
42/// use wickra_core::{Candle, Indicator, KasePermissionStochastic};
43///
44/// let mut indicator = KasePermissionStochastic::new(9, 3).unwrap();
45/// let mut last = None;
46/// for i in 0..40 {
47///     let base = 100.0 + f64::from(i);
48///     let candle =
49///         Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 1.0, i64::from(i)).unwrap();
50///     last = indicator.update(candle);
51/// }
52/// assert!(last.is_some());
53/// ```
54#[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    /// Construct with the stochastic `length` and the EMA `smooth` period
65    /// applied twice.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`Error::PeriodZero`] if `length == 0` or `smooth == 0`.
70    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    /// Cynthia Kase's classic parameters: `length = 9`, `smooth = 3`.
89    pub fn classic() -> Self {
90        Self::new(9, 3).expect("classic Kase Permission Stochastic parameters are valid")
91    }
92
93    /// Configured `(length, smooth)`.
94    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        // raw%K ready after `length` bars; each EMA seeds over `smooth` values.
135        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        // 9 + 2*3 - 2 = 13.
176        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        // warmup = 3 + 2*2 - 2 = 5 -> first value at input 5 (index 4).
185        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        // Close pinned at the top of a rising range -> raw%K near 100, both
194        // smoothed lines high.
195        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        // Constant high/low/close -> HH == LL -> raw%K defaults to 50, so both
210        // EMAs converge to 50.
211        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}