wickra_core/indicators/
cci.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::RollingSum;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
31pub struct Cci {
32 period: usize,
33 factor: f64,
34 window: VecDeque<f64>,
35 sum: RollingSum,
36}
37
38impl Cci {
39 pub fn new(period: usize) -> Result<Self> {
44 Self::with_factor(period, 0.015)
45 }
46
47 pub fn with_factor(period: usize, factor: f64) -> Result<Self> {
54 if period == 0 {
55 return Err(Error::PeriodZero);
56 }
57 if period > crate::error::MAX_PERIOD {
58 return Err(Error::InvalidPeriod {
59 message: crate::error::PERIOD_ABOVE_MAX,
60 });
61 }
62 if !factor.is_finite() || factor <= 0.0 {
63 return Err(Error::NonPositiveMultiplier);
64 }
65 Ok(Self {
66 period,
67 factor,
68 window: VecDeque::with_capacity(period),
69 sum: RollingSum::new(),
70 })
71 }
72
73 pub const fn period(&self) -> usize {
75 self.period
76 }
77}
78
79impl Indicator for Cci {
80 type Input = Candle;
81 type Output = f64;
82
83 #[inline]
84 fn update(&mut self, candle: Candle) -> Option<f64> {
85 let tp = candle.typical_price();
86 if self.window.len() == self.period {
87 let old = self.window.pop_front().expect("non-empty");
88 self.sum.evict(old);
89 }
90 self.window.push_back(tp);
91 self.sum.push(tp);
92 if self.sum.needs_reseed(self.period) {
93 self.sum.reseed(self.window.iter().copied());
94 }
95 if self.window.len() < self.period {
96 return None;
97 }
98 let n = self.period as f64;
99 let mean = self.sum.value() / n;
100 let mad: f64 = self.window.iter().map(|v| (v - mean).abs()).sum::<f64>() / n;
101 if mad == 0.0 {
102 return Some(0.0);
103 }
104 Some((tp - mean) / (self.factor * mad))
105 }
106
107 fn reset(&mut self) {
108 self.window.clear();
109 self.sum.reset();
110 }
111
112 #[inline]
113 fn warmup_period(&self) -> usize {
114 self.period
115 }
116
117 #[inline]
118 fn is_ready(&self) -> bool {
119 self.window.len() == self.period
120 }
121
122 #[inline]
123 fn name(&self) -> &'static str {
124 "CCI"
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::traits::BatchExt;
132 use approx::assert_relative_eq;
133
134 fn c(h: f64, l: f64, cl: f64) -> Candle {
135 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
136 }
137
138 #[test]
139 fn flat_candles_yield_zero() {
140 let candles: Vec<Candle> = (0..30).map(|_| c(10.0, 10.0, 10.0)).collect();
141 let mut cci = Cci::new(20).unwrap();
142 for v in cci.batch(&candles).into_iter().flatten() {
143 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
144 }
145 }
146
147 #[test]
148 fn rejects_invalid_input() {
149 assert!(Cci::new(0).is_err());
150 assert!(Cci::with_factor(20, 0.0).is_err());
151 assert!(Cci::with_factor(20, -1.0).is_err());
152 }
153
154 #[test]
158 fn accessors_and_metadata() {
159 let cci = Cci::new(20).unwrap();
160 assert_eq!(cci.period(), 20);
161 assert_eq!(cci.warmup_period(), 20);
162 assert_eq!(cci.name(), "CCI");
163 }
164
165 #[test]
166 fn batch_equals_streaming() {
167 let candles: Vec<Candle> = (0..60)
168 .map(|i| {
169 let m = 50.0 + (f64::from(i) * 0.2).sin() * 10.0;
170 c(m + 1.0, m - 1.0, m)
171 })
172 .collect();
173 let mut a = Cci::new(20).unwrap();
174 let mut b = Cci::new(20).unwrap();
175 assert_eq!(
176 a.batch(&candles),
177 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
178 );
179 }
180
181 #[test]
182 fn reset_clears_state() {
183 let candles: Vec<Candle> = (0..30).map(|_| c(10.0, 10.0, 10.0)).collect();
184 let mut cci = Cci::new(20).unwrap();
185 cci.batch(&candles);
186 assert!(cci.is_ready());
187 cci.reset();
188 assert!(!cci.is_ready());
189 }
190}