1use crate::indicators::incremental::ma_stream::MaStream;
4use crate::indicators::incremental::rsi::RSI;
5use crate::indicators::ma_type::MaType;
6use crate::traits::Next;
7use crate::utils::RingBuffer;
8
9#[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 (&h, &l) in self.highs.iter().zip(self.lows.iter()) {
39 if h > hh {
40 hh = h;
41 }
42 if l < ll {
43 ll = l;
44 }
45 }
46 let range = hh - ll;
47 Some((hh, ll, range))
48 }
49}
50
51fn fastk_from_hlc(close: f64, ll: f64, range: f64) -> f64 {
52 if range > 0.0 {
53 100.0 * (close - ll) / range
54 } else {
55 50.0
56 }
57}
58
59#[derive(Debug, Clone)]
61#[allow(non_camel_case_types)]
62pub struct STOCH {
63 pub fastk_period: usize,
64 pub slowk_period: usize,
65 pub slowk_matype: MaType,
66 pub slowd_period: usize,
67 pub slowd_matype: MaType,
68 hl: HlWindow,
69 slowk_ma: MaStream,
70 slowd_ma: MaStream,
71 slowk_valid: Vec<f64>,
72 bar_index: usize,
73 out_start: usize,
74}
75
76impl STOCH {
77 pub fn new(
78 fastk_period: usize,
79 slowk_period: usize,
80 slowk_matype: MaType,
81 slowd_period: usize,
82 slowd_matype: MaType,
83 ) -> Self {
84 Self {
85 fastk_period,
86 slowk_period,
87 slowk_matype,
88 slowd_period,
89 slowd_matype,
90 hl: HlWindow::new(fastk_period),
91 slowk_ma: MaStream::new(slowk_period, slowk_matype),
92 slowd_ma: MaStream::new(slowd_period, slowd_matype),
93 slowk_valid: Vec::new(),
94 bar_index: 0,
95 out_start: fastk_period - 1 + slowk_period - 1 + slowd_period - 1,
96 }
97 }
98}
99
100impl Next<(f64, f64, f64)> for STOCH {
101 type Output = (f64, f64);
102
103 fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
104 let i = self.bar_index;
105 self.bar_index += 1;
106
107 let Some((_, ll, range)) = self.hl.push(high, low) else {
108 return (f64::NAN, f64::NAN);
109 };
110 let fastk = fastk_from_hlc(close, ll, range);
111 let slowk_raw = self.slowk_ma.next(fastk);
112 if !slowk_raw.is_nan() {
113 self.slowk_valid.push(slowk_raw);
114 }
115 let slowd_raw = if slowk_raw.is_nan() {
116 f64::NAN
117 } else {
118 self.slowd_ma.next(slowk_raw)
119 };
120
121 if i < self.out_start {
122 return (f64::NAN, f64::NAN);
123 }
124
125 let k_skip = self.slowd_period - 1;
126 let j = i - self.out_start;
127 let idx = k_skip + j;
128 let slowk_out = self.slowk_valid.get(idx).copied().unwrap_or(f64::NAN);
129 let slowd_out = if slowd_raw.is_nan() {
130 f64::NAN
131 } else {
132 slowd_raw
133 };
134
135 (slowk_out, slowd_out)
136 }
137}
138
139#[derive(Debug, Clone)]
141#[allow(non_camel_case_types)]
142pub struct STOCHF {
143 pub fastk_period: usize,
144 pub fastd_period: usize,
145 pub fastd_matype: MaType,
146 hl: HlWindow,
147 fastd_ma: MaStream,
148 fastk_values: Vec<f64>,
149 bar_index: usize,
150 out_start: usize,
151}
152
153impl STOCHF {
154 pub fn new(fastk_period: usize, fastd_period: usize, fastd_matype: MaType) -> Self {
155 Self {
156 fastk_period,
157 fastd_period,
158 fastd_matype,
159 hl: HlWindow::new(fastk_period),
160 fastd_ma: MaStream::new(fastd_period, fastd_matype),
161 fastk_values: Vec::new(),
162 bar_index: 0,
163 out_start: fastk_period - 1 + fastd_period - 1,
164 }
165 }
166}
167
168impl Next<(f64, f64, f64)> for STOCHF {
169 type Output = (f64, f64);
170
171 fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
172 let i = self.bar_index;
173 self.bar_index += 1;
174
175 let Some((_, ll, range)) = self.hl.push(high, low) else {
176 return (f64::NAN, f64::NAN);
177 };
178 let fastk = fastk_from_hlc(close, ll, range);
179 self.fastk_values.push(fastk);
180
181 let fastd_raw = self.fastd_ma.next(fastk);
182
183 if i < self.out_start {
184 return (f64::NAN, f64::NAN);
185 }
186
187 let k_skip = self.fastd_period - 1;
188 let j = i - self.out_start;
189 let idx = k_skip + j;
190 let fastk_out = self.fastk_values.get(idx).copied().unwrap_or(f64::NAN);
191 let fastd_out = if fastd_raw.is_nan() {
192 f64::NAN
193 } else {
194 fastd_raw
195 };
196
197 (fastk_out, fastd_out)
198 }
199}
200
201#[derive(Debug, Clone)]
203#[allow(non_camel_case_types)]
204pub struct STOCHRSI {
205 pub timeperiod: usize,
206 pub fastk_period: usize,
207 pub fastd_period: usize,
208 pub fastd_matype: MaType,
209 rsi: RSI,
210 rsi_valid: Vec<f64>,
211 fastd_ma: MaStream,
212 fastk_values: Vec<f64>,
213 bar_index: usize,
214 d_start: usize,
215}
216
217impl STOCHRSI {
218 pub fn new(
219 timeperiod: usize,
220 fastk_period: usize,
221 fastd_period: usize,
222 fastd_matype: MaType,
223 ) -> Self {
224 let d_start = timeperiod + fastk_period - 1 + fastd_period - 1;
225 Self {
226 timeperiod,
227 fastk_period,
228 fastd_period,
229 fastd_matype,
230 rsi: RSI::new(timeperiod),
231 rsi_valid: Vec::new(),
232 fastd_ma: MaStream::new(fastd_period, fastd_matype),
233 fastk_values: Vec::new(),
234 bar_index: 0,
235 d_start,
236 }
237 }
238}
239
240impl Next<f64> for STOCHRSI {
241 type Output = (f64, f64);
242
243 fn next(&mut self, input: f64) -> Self::Output {
244 let i = self.bar_index;
245 self.bar_index += 1;
246
247 let rsi_v = self.rsi.next(input);
248 if !rsi_v.is_nan() {
249 self.rsi_valid.push(rsi_v);
250 }
251
252 if self.rsi_valid.len() < self.fastk_period {
253 return (f64::NAN, f64::NAN);
254 }
255
256 let idx = self.rsi_valid.len() - 1;
257 let start = idx + 1 - self.fastk_period;
258 let mut hh = f64::NEG_INFINITY;
259 let mut ll = f64::INFINITY;
260 for j in start..=idx {
261 let v = self.rsi_valid[j];
262 if v > hh {
263 hh = v;
264 }
265 if v < ll {
266 ll = v;
267 }
268 }
269 let range = hh - ll;
270 let fastk = if range > 0.0 {
271 100.0 * (self.rsi_valid[idx] - ll) / range
272 } else {
273 50.0
274 };
275 self.fastk_values.push(fastk);
276
277 let fastd_raw = self.fastd_ma.next(fastk);
278
279 if i < self.d_start {
280 return (f64::NAN, f64::NAN);
281 }
282
283 let k_skip = self.fastd_period - 1;
284 let j = i - self.d_start;
285 let idx = k_skip + j;
286 let fastk_out = self.fastk_values.get(idx).copied().unwrap_or(f64::NAN);
287 let fastd_out = if fastd_raw.is_nan() {
288 f64::NAN
289 } else {
290 fastd_raw
291 };
292
293 (fastk_out, fastd_out)
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use proptest::prelude::*;
301
302 const STOCH_PARITY_MATYPES: [MaType; 4] =
314 [MaType::Sma, MaType::Ema, MaType::Wma, MaType::Trima];
315
316 fn hlc(
317 len: usize,
318 highs: &[f64],
319 lows: &[f64],
320 closes: &[f64],
321 ) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
322 let mut high = Vec::with_capacity(len);
323 let mut low = Vec::with_capacity(len);
324 let mut close = Vec::with_capacity(len);
325 for i in 0..len {
326 let (h, l, c): (f64, f64, f64) = (highs[i], lows[i], closes[i]);
327 high.push(h.max(l).max(c));
328 low.push(h.min(l).min(c));
329 close.push(c);
330 }
331 (high, low, close)
332 }
333
334 fn assert_pair_parity(streaming: &[(f64, f64)], b_k: &[f64], b_d: &[f64], label: &str) {
335 for (i, &(s_k, s_d)) in streaming.iter().enumerate() {
336 for (s, b, name) in [(s_k, b_k[i], "k"), (s_d, b_d[i], "d")] {
337 if s.is_nan() {
338 assert!(
339 b.is_nan(),
340 "{label} bar {i} {name}: streaming NaN, batch {b}"
341 );
342 } else {
343 assert!(
344 !b.is_nan(),
345 "{label} bar {i} {name}: streaming {s}, batch NaN"
346 );
347 approx::assert_relative_eq!(s, b, epsilon = 1e-6);
348 }
349 }
350 }
351 }
352
353 proptest! {
354 #[test]
355 fn test_stoch_parity(
356 highs in prop::collection::vec(1.0..100.0, 1..100),
357 lows in prop::collection::vec(1.0..100.0, 1..100),
358 closes in prop::collection::vec(1.0..100.0, 1..100)
359 ) {
360 let len = highs.len().min(lows.len()).min(closes.len());
361 if len < 20 { return Ok(()); }
362 let (high, low, close) = hlc(len, &highs, &lows, &closes);
363
364 let fastk = 5;
365 let slowk = 3;
366 let slowk_ma = MaType::Sma;
367 let slowd = 3;
368 let slowd_ma = MaType::Sma;
369
370 let mut stoch = STOCH::new(fastk, slowk, slowk_ma, slowd, slowd_ma);
371 let streaming: Vec<(f64, f64)> = (0..len)
372 .map(|i| stoch.next((high[i], low[i], close[i])))
373 .collect();
374 let (b_k, b_d) = talib_rs::momentum::stoch(
375 &high, &low, &close, fastk, slowk, slowk_ma.into(), slowd, slowd_ma.into(),
376 )
377 .unwrap_or_else(|_| (vec![f64::NAN; len], vec![f64::NAN; len]));
378
379 assert_pair_parity(&streaming, &b_k, &b_d, "stoch/sma");
380 }
381
382 #[test]
385 fn test_stoch_family_parity_matypes(
386 highs in prop::collection::vec(1.0..100.0, 60..120),
387 lows in prop::collection::vec(1.0..100.0, 60..120),
388 closes in prop::collection::vec(1.0..100.0, 60..120),
389 idx in 0usize..4,
390 ) {
391 let matype = STOCH_PARITY_MATYPES[idx];
392 let len = highs.len().min(lows.len()).min(closes.len());
393 let (high, low, close) = hlc(len, &highs, &lows, &closes);
394 let (fastk, slowk, slowd) = (5usize, 3usize, 3usize);
395
396 let mut stoch = STOCH::new(fastk, slowk, matype, slowd, matype);
397 let streaming: Vec<(f64, f64)> = (0..len)
398 .map(|i| stoch.next((high[i], low[i], close[i])))
399 .collect();
400 let (b_k, b_d) = talib_rs::momentum::stoch(
401 &high, &low, &close, fastk, slowk, matype.into(), slowd, matype.into(),
402 )
403 .unwrap_or_else(|_| (vec![f64::NAN; len], vec![f64::NAN; len]));
404 assert_pair_parity(&streaming, &b_k, &b_d, &format!("stoch/{matype}"));
405
406 let mut stochf = STOCHF::new(fastk, slowd, matype);
407 let streaming: Vec<(f64, f64)> = (0..len)
408 .map(|i| stochf.next((high[i], low[i], close[i])))
409 .collect();
410 let (b_k, b_d) = talib_rs::momentum::stochf(
411 &high, &low, &close, fastk, slowd, matype.into(),
412 )
413 .unwrap_or_else(|_| (vec![f64::NAN; len], vec![f64::NAN; len]));
414 assert_pair_parity(&streaming, &b_k, &b_d, &format!("stochf/{matype}"));
415 }
416 }
417
418 #[test]
420 fn stoch_matype_changes_output() {
421 let bars: Vec<(f64, f64, f64)> = (0..200)
422 .map(|i| {
423 let c = 50.0 + 10.0 * (i as f64 * 0.3).sin();
424 (c + 1.0, c - 1.0, c)
425 })
426 .collect();
427 let run = |m: MaType| {
428 let mut s = STOCH::new(5, 3, m, 3, m);
429 bars.iter().map(|&b| s.next(b)).collect::<Vec<_>>()
430 };
431 let sma = run(MaType::Sma);
432 for matype in MaType::ALL {
433 if matype == MaType::Sma {
434 continue;
435 }
436 let got = run(matype);
437 let identical = got.iter().zip(sma.iter()).all(|(a, b)| {
438 ((a.0.is_nan() && b.0.is_nan()) || (a.0 - b.0).abs() < 1e-12)
439 && ((a.1.is_nan() && b.1.is_nan()) || (a.1 - b.1).abs() < 1e-12)
440 });
441 assert!(!identical, "STOCH with {matype} still produces SMA values");
442 }
443 }
444}