indexes_rs/v2/cci/
main.rs

1use crate::v2::cci::types::{CCIConfig, CCIError, CCIInput, CCIMarketCondition, CCIOutput, CCIState};
2
3/// Commodity Channel Index (CCI) Indicator
4///
5/// CCI measures how far the current price deviates from its statistical average.
6/// It's used to identify cyclical trends and overbought/oversold conditions.
7///
8/// Formula:
9/// 1. Typical Price = (High + Low + Close) / 3
10/// 2. SMA of Typical Price = Sum(TP) / Period
11/// 3. Mean Deviation = Sum(|TP - SMA|) / Period
12/// 4. CCI = (TP - SMA) / (0.015 × Mean Deviation)
13///
14/// The constant 0.015 ensures about 70-80% of CCI values fall between -100 and +100.
15///
16/// Interpretation:
17/// - Above +100: Overbought, potential sell signal
18/// - Below -100: Oversold, potential buy signal
19/// - Above +200: Extremely overbought
20/// - Below -200: Extremely oversold
21pub struct CCI {
22    state: CCIState,
23}
24
25impl CCI {
26    /// Create a new CCI calculator with default configuration (period=20)
27    pub fn new() -> Self {
28        Self::with_config(CCIConfig::default())
29    }
30
31    /// Create a new CCI calculator with custom period
32    pub fn with_period(period: usize) -> Result<Self, CCIError> {
33        if period == 0 {
34            return Err(CCIError::InvalidPeriod);
35        }
36
37        let config = CCIConfig { period, ..Default::default() };
38        Ok(Self::with_config(config))
39    }
40
41    /// Create a new CCI calculator with custom period and thresholds
42    pub fn with_thresholds(period: usize, overbought: f64, oversold: f64, extreme_overbought: f64, extreme_oversold: f64) -> Result<Self, CCIError> {
43        if period == 0 {
44            return Err(CCIError::InvalidPeriod);
45        }
46
47        if overbought <= oversold || extreme_overbought <= overbought || extreme_oversold >= oversold {
48            return Err(CCIError::InvalidThresholds);
49        }
50
51        let config = CCIConfig {
52            period,
53            overbought,
54            oversold,
55            extreme_overbought,
56            extreme_oversold,
57        };
58        Ok(Self::with_config(config))
59    }
60
61    /// Create a new CCI calculator with custom configuration
62    pub fn with_config(config: CCIConfig) -> Self {
63        Self { state: CCIState::new(config) }
64    }
65
66    /// Calculate CCI for the given input
67    pub fn calculate(&mut self, input: CCIInput) -> Result<CCIOutput, CCIError> {
68        // Validate input
69        self.validate_input(&input)?;
70        self.validate_config()?;
71
72        // Calculate typical price
73        let typical_price = self.calculate_typical_price(&input);
74
75        // Update typical price history
76        self.update_typical_price_history(typical_price);
77
78        // Calculate CCI if we have enough data
79        let (cci, sma_tp, mean_deviation) = if self.state.has_sufficient_data {
80            self.calculate_cci_value(typical_price)?
81        } else {
82            (0.0, typical_price, 0.0) // Default values when insufficient data
83        };
84
85        // Determine market condition
86        let market_condition = self.determine_market_condition(cci);
87
88        // Calculate distance from zero
89        let distance_from_zero = cci.abs();
90
91        Ok(CCIOutput {
92            cci,
93            typical_price,
94            sma_tp,
95            mean_deviation,
96            market_condition,
97            distance_from_zero,
98        })
99    }
100
101    /// Calculate CCI for a batch of inputs
102    pub fn calculate_batch(&mut self, inputs: &[CCIInput]) -> Result<Vec<CCIOutput>, CCIError> {
103        inputs.iter().map(|input| self.calculate(*input)).collect()
104    }
105
106    /// Reset the calculator state
107    pub fn reset(&mut self) {
108        self.state = CCIState::new(self.state.config);
109    }
110
111    /// Get current state (for serialization/debugging)
112    pub fn get_state(&self) -> &CCIState {
113        &self.state
114    }
115
116    /// Restore state (for deserialization)
117    pub fn set_state(&mut self, state: CCIState) {
118        self.state = state;
119    }
120
121    /// Get current market condition
122    pub fn market_condition(&self) -> CCIMarketCondition {
123        if !self.state.has_sufficient_data {
124            CCIMarketCondition::Insufficient
125        } else {
126            // Would need the last CCI value to determine this
127            CCIMarketCondition::Normal
128        }
129    }
130
131    /// Check if currently overbought
132    pub fn is_overbought(&self, cci: f64) -> bool {
133        cci >= self.state.config.overbought
134    }
135
136    /// Check if currently oversold
137    pub fn is_oversold(&self, cci: f64) -> bool {
138        cci <= self.state.config.oversold
139    }
140
141    /// Check if in extreme condition
142    pub fn is_extreme_condition(&self, cci: f64) -> bool {
143        cci >= self.state.config.extreme_overbought || cci <= self.state.config.extreme_oversold
144    }
145
146    // Private helper methods
147
148    fn validate_input(&self, input: &CCIInput) -> Result<(), CCIError> {
149        // Check for valid prices
150        if !input.high.is_finite() || !input.low.is_finite() || !input.close.is_finite() {
151            return Err(CCIError::InvalidPrice);
152        }
153
154        // Check HLC relationship
155        if input.high < input.low {
156            return Err(CCIError::InvalidHLC);
157        }
158
159        if input.close < input.low || input.close > input.high {
160            return Err(CCIError::InvalidHLC);
161        }
162
163        Ok(())
164    }
165
166    fn validate_config(&self) -> Result<(), CCIError> {
167        if self.state.config.period == 0 {
168            return Err(CCIError::InvalidPeriod);
169        }
170
171        let config = &self.state.config;
172        if config.overbought <= config.oversold || config.extreme_overbought <= config.overbought || config.extreme_oversold >= config.oversold {
173            return Err(CCIError::InvalidThresholds);
174        }
175
176        Ok(())
177    }
178
179    fn calculate_typical_price(&self, input: &CCIInput) -> f64 {
180        (input.high + input.low + input.close) / 3.0
181    }
182
183    fn update_typical_price_history(&mut self, typical_price: f64) {
184        // Remove oldest if at capacity
185        if self.state.typical_prices.len() >= self.state.config.period {
186            if let Some(oldest) = self.state.typical_prices.pop_front() {
187                self.state.tp_sum -= oldest;
188            }
189        }
190
191        // Add new typical price
192        self.state.typical_prices.push_back(typical_price);
193        self.state.tp_sum += typical_price;
194
195        // Check if we have sufficient data
196        self.state.has_sufficient_data = self.state.typical_prices.len() >= self.state.config.period;
197    }
198
199    fn calculate_cci_value(&self, current_tp: f64) -> Result<(f64, f64, f64), CCIError> {
200        if !self.state.has_sufficient_data {
201            return Ok((0.0, current_tp, 0.0));
202        }
203
204        // Calculate SMA of typical prices
205        let sma_tp = self.state.tp_sum / self.state.config.period as f64;
206
207        // Calculate mean absolute deviation
208        let mean_deviation = self.calculate_mean_deviation(sma_tp);
209
210        // Calculate CCI
211        if mean_deviation == 0.0 {
212            // If mean deviation is zero, prices are identical
213            return Ok((0.0, sma_tp, mean_deviation));
214        }
215
216        // CCI formula: (TP - SMA) / (0.015 × Mean Deviation)
217        let cci = (current_tp - sma_tp) / (0.015 * mean_deviation);
218
219        if !cci.is_finite() {
220            return Err(CCIError::DivisionByZero);
221        }
222
223        Ok((cci, sma_tp, mean_deviation))
224    }
225
226    fn calculate_mean_deviation(&self, sma_tp: f64) -> f64 {
227        let sum_deviations: f64 = self.state.typical_prices.iter().map(|&tp| (tp - sma_tp).abs()).sum();
228
229        sum_deviations / self.state.config.period as f64
230    }
231
232    fn determine_market_condition(&self, cci: f64) -> CCIMarketCondition {
233        if !self.state.has_sufficient_data {
234            CCIMarketCondition::Insufficient
235        } else if cci >= self.state.config.extreme_overbought {
236            CCIMarketCondition::ExtremeOverbought
237        } else if cci >= self.state.config.overbought {
238            CCIMarketCondition::Overbought
239        } else if cci <= self.state.config.extreme_oversold {
240            CCIMarketCondition::ExtremeOversold
241        } else if cci <= self.state.config.oversold {
242            CCIMarketCondition::Oversold
243        } else {
244            CCIMarketCondition::Normal
245        }
246    }
247}
248
249impl Default for CCI {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255/// Convenience function to calculate CCI for HLC data without maintaining state
256pub fn calculate_cci_simple(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Result<Vec<f64>, CCIError> {
257    let len = highs.len();
258    if len != lows.len() || len != closes.len() {
259        return Err(CCIError::InvalidInput("All price arrays must have same length".to_string()));
260    }
261
262    if len == 0 {
263        return Ok(Vec::new());
264    }
265
266    let mut cci_calculator = CCI::with_period(period)?;
267    let mut results = Vec::with_capacity(len);
268
269    for i in 0..len {
270        let input = CCIInput {
271            high: highs[i],
272            low: lows[i],
273            close: closes[i],
274        };
275        let output = cci_calculator.calculate(input)?;
276        results.push(output.cci);
277    }
278
279    Ok(results)
280}