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