Skip to main content

quantwave_core/indicators/incremental/
stoch.rs

1//! Native O(1) Stochastic family — TA-Lib parity (STOCH, STOCHF, STOCHRSI).
2
3use crate::indicators::incremental::ma_stream::MaStream;
4use crate::indicators::incremental::rsi::RSI;
5use crate::traits::Next;
6use crate::utils::RingBuffer;
7use talib_rs::MaType;
8
9/// Rolling highest high / lowest low over `period` bars.
10#[derive(Debug, Clone)]
11struct HlWindow {
12    highs: RingBuffer<f64>,
13    lows: RingBuffer<f64>,
14    period: usize,
15}
16
17impl HlWindow {
18    fn new(period: usize) -> Self {
19        Self {
20            highs: RingBuffer::with_capacity(period),
21            lows: RingBuffer::with_capacity(period),
22            period,
23        }
24    }
25
26    fn push(&mut self, high: f64, low: f64) -> Option<(f64, f64, f64)> {
27        if self.highs.len() >= self.period {
28            let _ = self.highs.pop_front();
29            let _ = self.lows.pop_front();
30        }
31        self.highs.push_back(high);
32        self.lows.push_back(low);
33        if self.highs.len() < self.period {
34            return None;
35        }
36        let mut hh = f64::NEG_INFINITY;
37        let mut ll = f64::INFINITY;
38        for i in 0..self.highs.len() {
39            let h = *self.highs.get(i).unwrap();
40            let l = *self.lows.get(i).unwrap();
41            if h > hh {
42                hh = h;
43            }
44            if l < ll {
45                ll = l;
46            }
47        }
48        let range = hh - ll;
49        Some((hh, ll, range))
50    }
51}
52
53fn fastk_from_hlc(close: f64, ll: f64, range: f64) -> f64 {
54    if range > 0.0 {
55        100.0 * (close - ll) / range
56    } else {
57        50.0
58    }
59}
60
61/// Stochastic Oscillator (STOCH) — default SMA smoothing on %K and %D.
62#[derive(Debug, Clone)]
63#[allow(non_camel_case_types)]
64pub struct STOCH {
65    pub fastk_period: usize,
66    pub slowk_period: usize,
67    pub slowk_matype: MaType,
68    pub slowd_period: usize,
69    pub slowd_matype: MaType,
70    hl: HlWindow,
71    slowk_ma: MaStream,
72    slowd_ma: MaStream,
73    slowk_valid: Vec<f64>,
74    bar_index: usize,
75    out_start: usize,
76}
77
78impl STOCH {
79    pub fn new(
80        fastk_period: usize,
81        slowk_period: usize,
82        slowk_matype: MaType,
83        slowd_period: usize,
84        slowd_matype: MaType,
85    ) -> Self {
86        Self {
87            fastk_period,
88            slowk_period,
89            slowk_matype,
90            slowd_period,
91            slowd_matype,
92            hl: HlWindow::new(fastk_period),
93            slowk_ma: MaStream::new(slowk_period, slowk_matype),
94            slowd_ma: MaStream::new(slowd_period, slowd_matype),
95            slowk_valid: Vec::new(),
96            bar_index: 0,
97            out_start: fastk_period - 1 + slowk_period - 1 + slowd_period - 1,
98        }
99    }
100}
101
102impl Next<(f64, f64, f64)> for STOCH {
103    type Output = (f64, f64);
104
105    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
106        let i = self.bar_index;
107        self.bar_index += 1;
108
109        let Some((_, ll, range)) = self.hl.push(high, low) else {
110            return (f64::NAN, f64::NAN);
111        };
112        let fastk = fastk_from_hlc(close, ll, range);
113        let slowk_raw = self.slowk_ma.next(fastk);
114        if !slowk_raw.is_nan() {
115            self.slowk_valid.push(slowk_raw);
116        }
117        let slowd_raw = if slowk_raw.is_nan() {
118            f64::NAN
119        } else {
120            self.slowd_ma.next(slowk_raw)
121        };
122
123        if i < self.out_start {
124            return (f64::NAN, f64::NAN);
125        }
126
127        let k_skip = self.slowd_period - 1;
128        let j = i - self.out_start;
129        let idx = k_skip + j;
130        let slowk_out = self.slowk_valid.get(idx).copied().unwrap_or(f64::NAN);
131        let slowd_out = if slowd_raw.is_nan() { f64::NAN } else { slowd_raw };
132
133        (slowk_out, slowd_out)
134    }
135}
136
137/// Fast Stochastic (STOCHF).
138#[derive(Debug, Clone)]
139#[allow(non_camel_case_types)]
140pub struct STOCHF {
141    pub fastk_period: usize,
142    pub fastd_period: usize,
143    pub fastd_matype: MaType,
144    hl: HlWindow,
145    fastd_ma: MaStream,
146    fastk_values: Vec<f64>,
147    bar_index: usize,
148    out_start: usize,
149}
150
151impl STOCHF {
152    pub fn new(fastk_period: usize, fastd_period: usize, fastd_matype: MaType) -> Self {
153        Self {
154            fastk_period,
155            fastd_period,
156            fastd_matype,
157            hl: HlWindow::new(fastk_period),
158            fastd_ma: MaStream::new(fastd_period, fastd_matype),
159            fastk_values: Vec::new(),
160            bar_index: 0,
161            out_start: fastk_period - 1 + fastd_period - 1,
162        }
163    }
164}
165
166impl Next<(f64, f64, f64)> for STOCHF {
167    type Output = (f64, f64);
168
169    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
170        let i = self.bar_index;
171        self.bar_index += 1;
172
173        let Some((_, ll, range)) = self.hl.push(high, low) else {
174            return (f64::NAN, f64::NAN);
175        };
176        let fastk = fastk_from_hlc(close, ll, range);
177        self.fastk_values.push(fastk);
178
179        let fastd_raw = self.fastd_ma.next(fastk);
180
181        if i < self.out_start {
182            return (f64::NAN, f64::NAN);
183        }
184
185        let k_skip = self.fastd_period - 1;
186        let j = i - self.out_start;
187        let idx = k_skip + j;
188        let fastk_out = self.fastk_values.get(idx).copied().unwrap_or(f64::NAN);
189        let fastd_out = if fastd_raw.is_nan() { f64::NAN } else { fastd_raw };
190
191        (fastk_out, fastd_out)
192    }
193}
194
195/// Stochastic RSI (STOCHRSI).
196#[derive(Debug, Clone)]
197#[allow(non_camel_case_types)]
198pub struct STOCHRSI {
199    pub timeperiod: usize,
200    pub fastk_period: usize,
201    pub fastd_period: usize,
202    pub fastd_matype: MaType,
203    rsi: RSI,
204    rsi_valid: Vec<f64>,
205    fastd_ma: MaStream,
206    fastk_values: Vec<f64>,
207    bar_index: usize,
208    d_start: usize,
209}
210
211impl STOCHRSI {
212    pub fn new(
213        timeperiod: usize,
214        fastk_period: usize,
215        fastd_period: usize,
216        fastd_matype: MaType,
217    ) -> Self {
218        let d_start = timeperiod + fastk_period - 1 + fastd_period - 1;
219        Self {
220            timeperiod,
221            fastk_period,
222            fastd_period,
223            fastd_matype,
224            rsi: RSI::new(timeperiod),
225            rsi_valid: Vec::new(),
226            fastd_ma: MaStream::new(fastd_period, fastd_matype),
227            fastk_values: Vec::new(),
228            bar_index: 0,
229            d_start,
230        }
231    }
232}
233
234impl Next<f64> for STOCHRSI {
235    type Output = (f64, f64);
236
237    fn next(&mut self, input: f64) -> Self::Output {
238        let i = self.bar_index;
239        self.bar_index += 1;
240
241        let rsi_v = self.rsi.next(input);
242        if !rsi_v.is_nan() {
243            self.rsi_valid.push(rsi_v);
244        }
245
246        if self.rsi_valid.len() < self.fastk_period {
247            return (f64::NAN, f64::NAN);
248        }
249
250        let idx = self.rsi_valid.len() - 1;
251        let start = idx + 1 - self.fastk_period;
252        let mut hh = f64::NEG_INFINITY;
253        let mut ll = f64::INFINITY;
254        for j in start..=idx {
255            let v = self.rsi_valid[j];
256            if v > hh {
257                hh = v;
258            }
259            if v < ll {
260                ll = v;
261            }
262        }
263        let range = hh - ll;
264        let fastk = if range > 0.0 {
265            100.0 * (self.rsi_valid[idx] - ll) / range
266        } else {
267            50.0
268        };
269        self.fastk_values.push(fastk);
270
271        let fastd_raw = self.fastd_ma.next(fastk);
272
273        if i < self.d_start {
274            return (f64::NAN, f64::NAN);
275        }
276
277        let k_skip = self.fastd_period - 1;
278        let j = i - self.d_start;
279        let idx = k_skip + j;
280        let fastk_out = self.fastk_values.get(idx).copied().unwrap_or(f64::NAN);
281        let fastd_out = if fastd_raw.is_nan() { f64::NAN } else { fastd_raw };
282
283        (fastk_out, fastd_out)
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use proptest::prelude::*;
291
292    proptest! {
293        #[test]
294        fn test_stoch_parity(
295            highs in prop::collection::vec(1.0..100.0, 1..100),
296            lows in prop::collection::vec(1.0..100.0, 1..100),
297            closes in prop::collection::vec(1.0..100.0, 1..100)
298        ) {
299            let len = highs.len().min(lows.len()).min(closes.len());
300            if len < 20 { return Ok(()); }
301            let mut high = Vec::with_capacity(len);
302            let mut low = Vec::with_capacity(len);
303            let mut close = Vec::with_capacity(len);
304            for i in 0..len {
305                let val_h: f64 = highs[i];
306                let val_l: f64 = lows[i];
307                let val_c: f64 = closes[i];
308                high.push(val_h.max(val_l).max(val_c));
309                low.push(val_h.min(val_l).min(val_c));
310                close.push(val_c);
311            }
312
313            let fastk = 5;
314            let slowk = 3;
315            let slowk_ma = MaType::Sma;
316            let slowd = 3;
317            let slowd_ma = MaType::Sma;
318
319            let mut stoch = STOCH::new(fastk, slowk, slowk_ma, slowd, slowd_ma);
320            let streaming: Vec<(f64, f64)> = (0..len)
321                .map(|i| stoch.next((high[i], low[i], close[i])))
322                .collect();
323            let (b_k, b_d) = talib_rs::momentum::stoch(
324                &high, &low, &close, fastk, slowk, slowk_ma, slowd, slowd_ma,
325            )
326            .unwrap_or_else(|_| (vec![f64::NAN; len], vec![f64::NAN; len]));
327
328            for (i, (s_k, s_d)) in streaming.into_iter().enumerate() {
329                if s_k.is_nan() { assert!(b_k[i].is_nan()); }
330                else { approx::assert_relative_eq!(s_k, b_k[i], epsilon = 1e-6); }
331                if s_d.is_nan() { assert!(b_d[i].is_nan()); }
332                else { approx::assert_relative_eq!(s_d, b_d[i], epsilon = 1e-6); }
333            }
334        }
335    }
336}