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        Ok(Self {
75            length,
76            smooth,
77            window: VecDeque::with_capacity(length),
78            fast_ema: Ema::new(smooth)?,
79            slow_ema: Ema::new(smooth)?,
80        })
81    }
82
83    /// Cynthia Kase's classic parameters: `length = 9`, `smooth = 3`.
84    pub fn classic() -> Self {
85        Self::new(9, 3).expect("classic Kase Permission Stochastic parameters are valid")
86    }
87
88    /// Configured `(length, smooth)`.
89    pub const fn periods(&self) -> (usize, usize) {
90        (self.length, self.smooth)
91    }
92}
93
94impl Indicator for KasePermissionStochastic {
95    type Input = Candle;
96    type Output = KasePermissionStochasticOutput;
97
98    fn update(&mut self, candle: Candle) -> Option<KasePermissionStochasticOutput> {
99        self.window.push_back((candle.high, candle.low));
100        if self.window.len() > self.length {
101            self.window.pop_front();
102        }
103        if self.window.len() < self.length {
104            return None;
105        }
106
107        let highest = self.window.iter().map(|w| w.0).fold(f64::MIN, f64::max);
108        let lowest = self.window.iter().map(|w| w.1).fold(f64::MAX, f64::min);
109        let raw_k = if highest > lowest {
110            100.0 * (candle.close - lowest) / (highest - lowest)
111        } else {
112            50.0
113        };
114
115        let fast = self.fast_ema.update(raw_k)?;
116        let slow = self.slow_ema.update(fast)?;
117        Some(KasePermissionStochasticOutput { fast, slow })
118    }
119
120    fn reset(&mut self) {
121        self.window.clear();
122        self.fast_ema.reset();
123        self.slow_ema.reset();
124    }
125
126    fn warmup_period(&self) -> usize {
127        // raw%K ready after `length` bars; each EMA seeds over `smooth` values.
128        self.length + 2 * self.smooth - 2
129    }
130
131    fn is_ready(&self) -> bool {
132        self.slow_ema.is_ready()
133    }
134
135    fn name(&self) -> &'static str {
136        "KasePermissionStochastic"
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::traits::BatchExt;
144    use approx::assert_relative_eq;
145
146    fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
147        Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
148    }
149
150    #[test]
151    fn rejects_zero_period() {
152        assert!(matches!(
153            KasePermissionStochastic::new(0, 3),
154            Err(Error::PeriodZero)
155        ));
156        assert!(matches!(
157            KasePermissionStochastic::new(9, 0),
158            Err(Error::PeriodZero)
159        ));
160    }
161
162    #[test]
163    fn accessors_and_metadata() {
164        let k = KasePermissionStochastic::classic();
165        assert_eq!(k.periods(), (9, 3));
166        // 9 + 2*3 - 2 = 13.
167        assert_eq!(k.warmup_period(), 13);
168        assert_eq!(k.name(), "KasePermissionStochastic");
169        assert!(!k.is_ready());
170    }
171
172    #[test]
173    fn warmup_emits_at_expected_bar() {
174        let mut k = KasePermissionStochastic::new(3, 2).unwrap();
175        // warmup = 3 + 2*2 - 2 = 5 -> first value at input 5 (index 4).
176        let candles: Vec<Candle> = (0..8).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
177        let out = k.batch(&candles);
178        assert!(out[3].is_none());
179        assert!(out[4].is_some());
180    }
181
182    #[test]
183    fn top_of_range_is_high() {
184        // Close pinned at the top of a rising range -> raw%K near 100, both
185        // smoothed lines high.
186        let mut k = KasePermissionStochastic::new(5, 3).unwrap();
187        let candles: Vec<Candle> = (0_i64..40)
188            .map(|i| {
189                let base = 100.0 + i as f64;
190                candle(base + 2.0, base - 2.0, base + 2.0, i)
191            })
192            .collect();
193        let last = k.batch(&candles).last().unwrap().unwrap();
194        assert!(last.fast > 80.0, "fast {} should be high", last.fast);
195        assert!(last.slow > 80.0, "slow {} should be high", last.slow);
196    }
197
198    #[test]
199    fn flat_window_defaults_to_neutral() {
200        // Constant high/low/close -> HH == LL -> raw%K defaults to 50, so both
201        // EMAs converge to 50.
202        let mut k = KasePermissionStochastic::new(4, 2).unwrap();
203        let candles: Vec<Candle> = (0..20).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
204        let last = k.batch(&candles).last().unwrap().unwrap();
205        assert_relative_eq!(last.fast, 50.0, epsilon = 1e-9);
206        assert_relative_eq!(last.slow, 50.0, epsilon = 1e-9);
207    }
208
209    #[test]
210    fn reset_clears_state() {
211        let mut k = KasePermissionStochastic::classic();
212        let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
213        k.batch(&candles);
214        assert!(k.is_ready());
215        k.reset();
216        assert!(!k.is_ready());
217    }
218
219    #[test]
220    fn batch_equals_streaming() {
221        let candles: Vec<Candle> = (0..80_i64)
222            .map(|i| {
223                let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
224                candle(base + 2.0, base - 2.0, base + (i as f64 * 0.3).cos(), i)
225            })
226            .collect();
227        let mut a = KasePermissionStochastic::classic();
228        let mut b = KasePermissionStochastic::classic();
229        assert_eq!(
230            a.batch(&candles),
231            candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
232        );
233    }
234}