quantwave_core/indicators/incremental/
stoch.rs1use 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#[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#[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#[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#[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}