mantis_ta/indicators/momentum/
stochastic.rs1use crate::indicators::Indicator;
2use crate::types::{Candle, StochasticOutput};
3use crate::utils::ringbuf::RingBuf;
4
5#[derive(Debug, Clone)]
36pub struct Stochastic {
37 k_period: usize,
38 d_period: usize,
39 window: RingBuf<(f64, f64)>,
40 k_values: RingBuf<f64>,
41}
42
43impl Stochastic {
44 pub fn new(k_period: usize, d_period: usize) -> Self {
45 assert!(k_period > 0, "k_period must be > 0");
46 assert!(d_period > 0, "d_period must be > 0");
47 Self {
48 k_period,
49 d_period,
50 window: RingBuf::new(k_period, (0.0, 0.0)),
51 k_values: RingBuf::new(d_period, 0.0),
52 }
53 }
54
55 fn range_high_low(&self) -> Option<(f64, f64)> {
56 if self.window.len() < self.k_period {
57 return None;
58 }
59 let mut highest = f64::MIN;
60 let mut lowest = f64::MAX;
61 for (high, low) in self.window.iter() {
62 if *high > highest {
63 highest = *high;
64 }
65 if *low < lowest {
66 lowest = *low;
67 }
68 }
69 Some((highest, lowest))
70 }
71}
72
73impl Indicator for Stochastic {
74 type Output = StochasticOutput;
75
76 fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
77 self.window.push((candle.high, candle.low));
78
79 let (highest, lowest) = self.range_high_low()?;
80
81 let denom = highest - lowest;
82 let k = if denom == 0.0 {
83 50.0
84 } else {
85 ((candle.close - lowest) / denom) * 100.0
86 };
87 self.k_values.push(k);
88
89 if self.k_values.len() < self.d_period {
90 return None;
91 }
92
93 let sum_d: f64 = self.k_values.iter().copied().sum();
94 let d = sum_d / self.d_period as f64;
95
96 Some(StochasticOutput { k, d })
97 }
98
99 fn reset(&mut self) {
100 self.window = RingBuf::new(self.k_period, (0.0, 0.0));
101 self.k_values = RingBuf::new(self.d_period, 0.0);
102 }
103
104 fn warmup_period(&self) -> usize {
105 self.k_period + self.d_period - 1
106 }
107
108 fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
109 Box::new(self.clone())
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn stochastic_emits_after_warmup() {
119 let mut stoch = Stochastic::new(3, 3);
120 let candles: Vec<Candle> = [
121 (1.0, 0.5, 0.8),
122 (2.0, 0.5, 1.5),
123 (3.0, 1.0, 2.5),
124 (3.5, 1.5, 3.0),
125 (4.0, 2.0, 3.5),
126 ]
127 .iter()
128 .map(|(h, l, c)| Candle {
129 timestamp: 0,
130 open: *c,
131 high: *h,
132 low: *l,
133 close: *c,
134 volume: 0.0,
135 })
136 .collect();
137
138 let outputs: Vec<_> = candles.iter().map(|c| stoch.next(c)).collect();
139 assert!(
140 outputs
141 .iter()
142 .take(stoch.warmup_period() - 1)
143 .all(|o| o.is_none())
144 );
145 assert!(
146 outputs
147 .iter()
148 .skip(stoch.warmup_period() - 1)
149 .any(|o| o.is_some())
150 );
151 }
152}