1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::sma::Sma;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct StochasticOutput {
13 pub k: f64,
15 pub d: f64,
17}
18
19#[derive(Debug, Clone)]
40pub struct Stochastic {
41 k_period: usize,
42 d_period: usize,
43 candles: VecDeque<Candle>,
44 hh_idx: VecDeque<usize>, ll_idx: VecDeque<usize>, count: usize,
49 d_sma: Sma,
50 last_k: Option<f64>,
51}
52
53impl Stochastic {
54 pub fn new(k_period: usize, d_period: usize) -> Result<Self> {
60 if k_period == 0 || d_period == 0 {
61 return Err(Error::PeriodZero);
62 }
63 Ok(Self {
64 k_period,
65 d_period,
66 candles: VecDeque::with_capacity(k_period),
67 hh_idx: VecDeque::with_capacity(k_period),
68 ll_idx: VecDeque::with_capacity(k_period),
69 count: 0,
70 d_sma: Sma::new(d_period)?,
71 last_k: None,
72 })
73 }
74
75 pub fn classic() -> Self {
77 Self::new(14, 3).expect("classic stochastic periods are valid")
78 }
79
80 pub const fn periods(&self) -> (usize, usize) {
82 (self.k_period, self.d_period)
83 }
84
85 fn push_window(&mut self, candle: Candle) {
86 let idx = self.count;
87 self.count += 1;
88 let oldest_keep_idx = idx.saturating_sub(self.k_period - 1);
90 while let Some(&front) = self.hh_idx.front() {
91 if front < oldest_keep_idx {
92 self.hh_idx.pop_front();
93 } else {
94 break;
95 }
96 }
97 while let Some(&front) = self.ll_idx.front() {
98 if front < oldest_keep_idx {
99 self.ll_idx.pop_front();
100 } else {
101 break;
102 }
103 }
104 while let Some(&back) = self.hh_idx.back() {
106 let back_off = back - idx.saturating_sub(self.candles.len());
107 if self.candles[back_off].high <= candle.high {
108 self.hh_idx.pop_back();
109 } else {
110 break;
111 }
112 }
113 self.hh_idx.push_back(idx);
114 while let Some(&back) = self.ll_idx.back() {
116 let back_off = back - idx.saturating_sub(self.candles.len());
117 if self.candles[back_off].low >= candle.low {
118 self.ll_idx.pop_back();
119 } else {
120 break;
121 }
122 }
123 self.ll_idx.push_back(idx);
124
125 if self.candles.len() == self.k_period {
126 self.candles.pop_front();
127 }
128 self.candles.push_back(candle);
129 }
130
131 fn current_extremes(&self) -> (f64, f64) {
132 let base = self.count - self.candles.len();
133 let hi = self.candles[self.hh_idx[0] - base].high;
134 let lo = self.candles[self.ll_idx[0] - base].low;
135 (hi, lo)
136 }
137}
138
139impl Indicator for Stochastic {
140 type Input = Candle;
141 type Output = StochasticOutput;
142
143 #[inline]
144 fn update(&mut self, candle: Candle) -> Option<StochasticOutput> {
145 self.push_window(candle);
146 if self.candles.len() < self.k_period {
147 return None;
148 }
149 let (hh, ll) = self.current_extremes();
150 let range = hh - ll;
151 let k = if range == 0.0 {
152 50.0
154 } else {
155 100.0 * (candle.close - ll) / range
156 };
157 self.last_k = Some(k);
158 let d = self.d_sma.update(k)?;
159 Some(StochasticOutput { k, d })
160 }
161
162 fn reset(&mut self) {
163 self.candles.clear();
164 self.hh_idx.clear();
165 self.ll_idx.clear();
166 self.count = 0;
167 self.d_sma.reset();
168 self.last_k = None;
169 }
170
171 #[inline]
172 fn warmup_period(&self) -> usize {
173 self.k_period + self.d_period - 1
174 }
175
176 #[inline]
177 fn is_ready(&self) -> bool {
178 self.d_sma.is_ready()
179 }
180
181 #[inline]
182 fn name(&self) -> &'static str {
183 "Stochastic"
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::traits::BatchExt;
191 use approx::assert_relative_eq;
192
193 fn c(h: f64, l: f64, cl: f64) -> Candle {
194 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
195 }
196
197 fn naive_k(candles: &[Candle], k_period: usize) -> Vec<Option<f64>> {
199 candles
200 .iter()
201 .enumerate()
202 .map(|(i, _)| {
203 if i + 1 < k_period {
204 None
205 } else {
206 let w = &candles[i + 1 - k_period..=i];
207 let hh = w.iter().map(|x| x.high).fold(f64::NEG_INFINITY, f64::max);
208 let ll = w.iter().map(|x| x.low).fold(f64::INFINITY, f64::min);
209 let range = hh - ll;
210 let cl = candles[i].close;
211 Some(if range == 0.0 {
212 50.0
213 } else {
214 100.0 * (cl - ll) / range
215 })
216 }
217 })
218 .collect()
219 }
220
221 #[test]
222 fn rejects_zero_periods() {
223 assert!(matches!(Stochastic::new(0, 3), Err(Error::PeriodZero)));
224 assert!(matches!(Stochastic::new(14, 0), Err(Error::PeriodZero)));
225 }
226
227 #[test]
233 fn classic_periods_and_metadata() {
234 let s = Stochastic::classic();
235 assert_eq!(s.periods(), (14, 3));
236 assert_eq!(s.warmup_period(), 16);
238 assert_eq!(s.name(), "Stochastic");
239 }
240
241 #[test]
242 fn close_at_high_yields_k_100() {
243 let candles = vec![
244 c(10.0, 8.0, 9.0),
245 c(11.0, 9.0, 10.0),
246 c(12.0, 10.0, 12.0), ];
248 let mut s = Stochastic::new(3, 1).unwrap();
249 let out = s.batch(&candles);
250 assert_relative_eq!(out[2].unwrap().k, 100.0, epsilon = 1e-12);
251 }
252
253 #[test]
254 fn close_at_low_yields_k_0() {
255 let candles = vec![
256 c(10.0, 8.0, 9.0),
257 c(11.0, 9.0, 10.0),
258 c(12.0, 8.0, 8.0), ];
260 let mut s = Stochastic::new(3, 1).unwrap();
261 let out = s.batch(&candles);
262 assert_relative_eq!(out[2].unwrap().k, 0.0, epsilon = 1e-12);
263 }
264
265 #[test]
266 fn flat_range_yields_k_50() {
267 let candles: Vec<Candle> = (0..20).map(|_| c(10.0, 10.0, 10.0)).collect();
268 let mut s = Stochastic::new(14, 3).unwrap();
269 for o in s.batch(&candles).into_iter().flatten() {
270 assert_relative_eq!(o.k, 50.0, epsilon = 1e-12);
271 assert_relative_eq!(o.d, 50.0, epsilon = 1e-12);
272 }
273 let ks = naive_k(&candles, 14);
277 for k in ks.into_iter().skip(13) {
278 assert_relative_eq!(k.expect("ready after 14 inputs"), 50.0, epsilon = 1e-12);
279 }
280 }
281
282 #[test]
283 fn k_matches_naive() {
284 let candles: Vec<Candle> = (0..60)
285 .map(|i| {
286 let mid = 50.0 + (f64::from(i) * 0.4).sin() * 10.0;
287 c(mid + 2.0, mid - 2.0, mid + (f64::from(i) * 0.7).cos())
288 })
289 .collect();
290 let mut s = Stochastic::new(14, 3).unwrap();
291 let out = s.batch(&candles);
292 let naive = naive_k(&candles, 14);
293 for (i, got) in out.iter().enumerate() {
294 if let Some(o) = got {
295 let n = naive[i].expect("naive ready");
296 assert_relative_eq!(o.k, n, epsilon = 1e-9);
297 }
298 }
299 }
300
301 #[test]
302 fn d_is_sma_of_k() {
303 let candles: Vec<Candle> = (0..60)
304 .map(|i| {
305 let mid = 50.0 + f64::from(i).sin() * 5.0;
306 c(mid + 1.5, mid - 1.5, mid)
307 })
308 .collect();
309 let mut s = Stochastic::new(14, 3).unwrap();
310 let out = s.batch(&candles);
311 let naive_ks = naive_k(&candles, 14);
313 let first_emit_idx = out
317 .iter()
318 .position(Option::is_some)
319 .expect("d eventually emits");
320 let first_d = out[first_emit_idx].unwrap().d;
321 let k_window = &naive_ks[first_emit_idx - 2..=first_emit_idx];
322 let want = k_window
323 .iter()
324 .map(|v| v.expect("naive K ready inside window"))
325 .sum::<f64>()
326 / 3.0;
327 assert_relative_eq!(first_d, want, epsilon = 1e-9);
328 }
329
330 #[test]
331 fn batch_equals_streaming() {
332 let candles: Vec<Candle> = (0..50)
333 .map(|i| {
334 let mid = 100.0 + f64::from(i) * 0.5;
335 c(mid + 2.0, mid - 2.0, mid)
336 })
337 .collect();
338 let mut a = Stochastic::new(14, 3).unwrap();
339 let mut b = Stochastic::new(14, 3).unwrap();
340 assert_eq!(
341 a.batch(&candles),
342 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
343 );
344 }
345
346 #[test]
347 fn reset_clears_state() {
348 let mut s = Stochastic::new(5, 3).unwrap();
349 let candles: Vec<Candle> = (0..10).map(|i| c(10.0 + f64::from(i), 5.0, 7.0)).collect();
350 s.batch(&candles);
351 assert!(s.is_ready());
352 s.reset();
353 assert!(!s.is_ready());
354 assert_eq!(s.update(candles[0]), None);
355 }
356}