wickra_core/indicators/
stochastic_cci.rs1use std::collections::VecDeque;
4
5use crate::error::Result;
6use crate::indicators::cci::Cci;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
44pub struct StochasticCci {
45 period: usize,
46 cci: Cci,
47 window: VecDeque<f64>,
49}
50
51impl StochasticCci {
52 pub fn new(period: usize) -> Result<Self> {
59 Ok(Self {
60 period,
61 cci: Cci::new(period)?,
62 window: VecDeque::with_capacity(period),
63 })
64 }
65
66 pub const fn period(&self) -> usize {
68 self.period
69 }
70}
71
72impl Indicator for StochasticCci {
73 type Input = Candle;
74 type Output = f64;
75
76 #[inline]
77 fn update(&mut self, candle: Candle) -> Option<f64> {
78 let cci = self.cci.update(candle)?;
79 if self.window.len() == self.period {
80 self.window.pop_front();
81 }
82 self.window.push_back(cci);
83 if self.window.len() < self.period {
84 return None;
85 }
86 let mut lo = f64::MAX;
87 let mut hi = f64::MIN;
88 for &v in &self.window {
89 if v < lo {
90 lo = v;
91 }
92 if v > hi {
93 hi = v;
94 }
95 }
96 let range = hi - lo;
97 if range == 0.0 {
98 return Some(50.0);
99 }
100 Some(100.0 * ((cci - lo) / range))
102 }
103
104 fn reset(&mut self) {
105 self.cci.reset();
106 self.window.clear();
107 }
108
109 #[inline]
110 fn warmup_period(&self) -> usize {
111 2 * self.period - 1
113 }
114
115 #[inline]
116 fn is_ready(&self) -> bool {
117 self.window.len() == self.period
118 }
119
120 #[inline]
121 fn name(&self) -> &'static str {
122 "StochasticCCI"
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::traits::BatchExt;
130 use approx::assert_relative_eq;
131
132 fn candle(high: f64, low: f64, close: f64) -> Candle {
133 Candle::new(close, high, low, close, 1.0, 0).unwrap()
134 }
135
136 #[test]
137 fn rejects_zero_period() {
138 assert!(StochasticCci::new(0).is_err());
139 }
140
141 #[test]
144 fn accessors_and_metadata() {
145 let sc = StochasticCci::new(14).unwrap();
146 assert_eq!(sc.period(), 14);
147 assert_eq!(sc.warmup_period(), 27);
148 assert_eq!(sc.name(), "StochasticCCI");
149 }
150
151 #[test]
152 fn first_emission_matches_warmup_period() {
153 let bars: Vec<Candle> = (0..40)
154 .map(|i| {
155 let base = 100.0 + (f64::from(i) * 0.4).sin() * 8.0;
156 candle(base + 1.0, base - 1.0, base)
157 })
158 .collect();
159 let mut sc = StochasticCci::new(5).unwrap();
160 let out = sc.batch(&bars);
161 let warmup = sc.warmup_period();
162 assert_eq!(warmup, 9);
163 for (i, v) in out.iter().enumerate().take(warmup - 1) {
164 assert!(v.is_none(), "index {i} must be None during warmup");
165 }
166 assert!(out[warmup - 1].is_some());
167 }
168
169 #[test]
170 fn bounded_zero_to_hundred() {
171 let bars: Vec<Candle> = (0..80)
172 .map(|i| {
173 let base = 100.0 + (f64::from(i) * 0.35).sin() * 12.0;
174 candle(base + 2.0, base - 2.0, base)
175 })
176 .collect();
177 let mut sc = StochasticCci::new(9).unwrap();
178 for v in sc.batch(&bars).into_iter().flatten() {
179 assert!((0.0..=100.0).contains(&v), "%K {v} left [0, 100]");
180 }
181 }
182
183 #[test]
184 fn flat_market_is_neutral() {
185 let mut sc = StochasticCci::new(4).unwrap();
187 let bars = vec![candle(10.0, 10.0, 10.0); 20];
188 let last = sc.batch(&bars).into_iter().flatten().last().unwrap();
189 assert_relative_eq!(last, 50.0, epsilon = 1e-12);
190 }
191
192 #[test]
193 fn highest_cci_in_window_is_hundred() {
194 let mut bars: Vec<Candle> = (0..20)
197 .map(|i| candle(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
198 .collect();
199 bars.push(candle(100.0, 98.0, 100.0));
201 let mut sc = StochasticCci::new(5).unwrap();
202 let last = sc.batch(&bars).into_iter().flatten().last().unwrap();
203 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
204 }
205
206 #[test]
207 fn reset_clears_state() {
208 let mut sc = StochasticCci::new(5).unwrap();
209 sc.batch(
210 &(0..30)
211 .map(|i| candle(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
212 .collect::<Vec<_>>(),
213 );
214 assert!(sc.is_ready());
215 sc.reset();
216 assert!(!sc.is_ready());
217 assert_eq!(sc.update(candle(2.0, 0.0, 1.0)), None);
218 }
219
220 #[test]
221 fn batch_equals_streaming() {
222 let bars: Vec<Candle> = (0..60)
223 .map(|i| {
224 let base = 50.0 + (f64::from(i) * 0.5).sin() * 10.0;
225 candle(base + 1.5, base - 1.5, base)
226 })
227 .collect();
228 let mut a = StochasticCci::new(9).unwrap();
229 let mut b = StochasticCci::new(9).unwrap();
230 assert_eq!(
231 a.batch(&bars),
232 bars.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
233 );
234 }
235}