wickra_core/indicators/
adaptive_cci.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
48pub struct AdaptiveCci {
49 period: usize,
50 window: VecDeque<f64>,
51 mean: Option<f64>,
52 last: Option<f64>,
53}
54
55impl AdaptiveCci {
56 pub fn new(period: usize) -> Result<Self> {
64 if period == 0 {
65 return Err(Error::PeriodZero);
66 }
67 if period > crate::error::MAX_PERIOD {
68 return Err(Error::InvalidPeriod {
69 message: crate::error::PERIOD_ABOVE_MAX,
70 });
71 }
72 if period < 2 {
73 return Err(Error::InvalidPeriod {
74 message: "adaptive CCI needs period >= 2",
75 });
76 }
77 Ok(Self {
78 period,
79 window: VecDeque::with_capacity(period),
80 mean: None,
81 last: None,
82 })
83 }
84
85 pub const fn period(&self) -> usize {
87 self.period
88 }
89
90 pub const fn value(&self) -> Option<f64> {
92 self.last
93 }
94}
95
96impl Indicator for AdaptiveCci {
97 type Input = Candle;
98 type Output = f64;
99
100 fn update(&mut self, candle: Candle) -> Option<f64> {
101 let tp = candle.typical_price();
102 if self.window.len() == self.period {
103 self.window.pop_front();
104 }
105 self.window.push_back(tp);
106 if self.window.len() < self.period {
107 return None;
108 }
109 let n = self.period as f64;
110
111 let oldest = self.window[0];
113 let direction = (tp - oldest).abs();
114 let mut path = 0.0;
115 for pair in self.window.iter().collect::<Vec<_>>().windows(2) {
116 path += (pair[1] - pair[0]).abs();
117 }
118 let er = if path > 0.0 {
119 (direction / path).clamp(0.0, 1.0)
120 } else {
121 0.0
122 };
123 let fast = 2.0 / 3.0;
124 let slow = 2.0 / 31.0;
125 let sc = (er * (fast - slow) + slow).powi(2);
126
127 let mean = match self.mean {
128 None => self.window.iter().sum::<f64>() / n,
129 Some(prev) => prev + sc * (tp - prev),
130 };
131 self.mean = Some(mean);
132
133 let md = self.window.iter().map(|&v| (v - mean).abs()).sum::<f64>() / n;
134 let cci = if md > 0.0 {
135 (tp - mean) / (0.015 * md)
136 } else {
137 0.0
138 };
139 self.last = Some(cci);
140 Some(cci)
141 }
142
143 fn reset(&mut self) {
144 self.window.clear();
145 self.mean = None;
146 self.last = None;
147 }
148
149 #[inline]
150 fn warmup_period(&self) -> usize {
151 self.period
152 }
153
154 #[inline]
155 fn is_ready(&self) -> bool {
156 self.last.is_some()
157 }
158
159 #[inline]
160 fn name(&self) -> &'static str {
161 "AdaptiveCci"
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::traits::BatchExt;
169 use approx::assert_relative_eq;
170
171 fn candle(tp: f64) -> Candle {
172 Candle::new_unchecked(tp, tp, tp, tp, 1_000.0, 0)
174 }
175
176 #[test]
177 fn rejects_invalid_period() {
178 assert!(matches!(AdaptiveCci::new(0), Err(Error::PeriodZero)));
179 assert!(matches!(
180 AdaptiveCci::new(1),
181 Err(Error::InvalidPeriod { .. })
182 ));
183 }
184
185 #[test]
186 fn accessors_and_metadata() {
187 let c = AdaptiveCci::new(20).unwrap();
188 assert_eq!(c.period(), 20);
189 assert_eq!(c.warmup_period(), 20);
190 assert_eq!(c.name(), "AdaptiveCci");
191 assert!(!c.is_ready());
192 assert_eq!(c.value(), None);
193 }
194
195 #[test]
196 fn first_emission_at_warmup_period() {
197 let mut c = AdaptiveCci::new(4).unwrap();
198 let candles: Vec<Candle> = (0..6).map(|i| candle(100.0 + f64::from(i))).collect();
199 let out = c.batch(&candles);
200 for v in out.iter().take(3) {
201 assert!(v.is_none());
202 }
203 assert!(out[3].is_some());
204 }
205
206 #[test]
207 fn uptrend_is_positive() {
208 let mut c = AdaptiveCci::new(10).unwrap();
209 let candles: Vec<Candle> = (0..40).map(|i| candle(100.0 + f64::from(i))).collect();
210 let last = c.batch(&candles).into_iter().flatten().last().unwrap();
211 assert!(last > 0.0, "uptrend should give positive CCI, got {last}");
212 }
213
214 #[test]
215 fn downtrend_is_negative() {
216 let mut c = AdaptiveCci::new(10).unwrap();
217 let candles: Vec<Candle> = (0..40).map(|i| candle(200.0 - f64::from(i))).collect();
218 let last = c.batch(&candles).into_iter().flatten().last().unwrap();
219 assert!(last < 0.0, "downtrend should give negative CCI, got {last}");
220 }
221
222 #[test]
223 fn flat_window_is_zero() {
224 let mut c = AdaptiveCci::new(5).unwrap();
225 let candles: Vec<Candle> = (0..10).map(|_| candle(100.0)).collect();
226 for v in c.batch(&candles).into_iter().flatten() {
227 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
228 }
229 }
230
231 #[test]
232 fn reset_clears_state() {
233 let mut c = AdaptiveCci::new(5).unwrap();
234 let candles: Vec<Candle> = (0..20).map(|i| candle(100.0 + f64::from(i))).collect();
235 c.batch(&candles);
236 assert!(c.is_ready());
237 c.reset();
238 assert!(!c.is_ready());
239 assert_eq!(c.value(), None);
240 assert_eq!(c.update(candle(100.0)), None);
241 }
242
243 #[test]
244 fn batch_equals_streaming() {
245 let candles: Vec<Candle> = (0..120)
246 .map(|i| candle(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
247 .collect();
248 let batch = AdaptiveCci::new(20).unwrap().batch(&candles);
249 let mut b = AdaptiveCci::new(20).unwrap();
250 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
251 assert_eq!(batch, streamed);
252 }
253}