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