Skip to main content

finance_solution/stocks/ta/
cci.rs

1//! # Commodity Channel Index (CCI)
2//!
3//! ```text
4//! TP  = (high + low + close) / 3
5//! SMA = mean(TP over N)
6//! MD  = mean( |TP − SMA| over N )
7//! CCI = (TP − SMA) / (0.015 × MD)
8//! ```
9//!
10//! When mean deviation is zero (flat typical prices), CCI is **`None`** for that bar
11//! (undefined scale), not a fake 0.
12//!
13//! Default: **period 20** ([`CciParams::period_20`]).
14//!
15//! ---
16//!
17//! ## Trading perspective
18//!
19//! | Region | Habit (classic, not a rule) |
20//! |--------|-----------------------------|
21//! | CCI \> +100 | Extended / breakout participation screen |
22//! | CCI \< −100 | Oversold / mean-reversion screen |
23//! | Zero line cross | Momentum shift screen |
24//!
25//! ## vs RSI / Stochastic / WillR
26//!
27//! | | CCI | RSI | Stoch / %R |
28//! |--|-----|-----|------------|
29//! | Centered on | Typical price vs its mean | Close momentum | Close in HH–LL range |
30//! | Bounds | Unbounded (soft ±100 zones) | 0–100 | 0–100 or −100–0 |
31//! | Best narrative | “How stretched vs recent TP?” | Overbought/oversold on closes | Where is close in the range? |
32//!
33//! Use **CCI** for channel stretch on HLC; **RSI** for pure close momentum; **Stoch/%R** for
34//! range position. They often agree at extremes but diverge in trends (CCI can stay > +100).
35//!
36//! ## Pairs well with
37//!
38//! - **Donchian / Bollinger / Keltner** — breakout when CCI already extended.
39//! - **ADX** — high ADX + CCI > +100 → trend continuation more than fade.
40//! - **ATR** — size stops in price units, not CCI points.
41//!
42//! ---
43//!
44//! ## Engineering
45//!
46//! [`CciParams`] → [`cci`] / [`CciState`] → [`cci_solution`]. Batch via [`CciState`].  
47//! Mean deviation uses the **current** window mean, so each ready bar is **O(period)** over
48//! the ring (fine for N≈20; not the same O(1) slide as Bollinger’s Σx² identity).
49//!
50//! ## Word problem
51//!
52//! > Constant H=L=C=100 for 20 bars. Is CCI defined?
53//!
54//! No: MD = 0 → `None` (zero width).
55//!
56//! ```
57//! use finance_solution::stocks::ta::{cci, CciParams};
58//! let x = vec![100.0; 25];
59//! let s = cci(&x, &x, &x, CciParams::period_20()).unwrap();
60//! assert!(s.cci[19].is_none());
61//! ```
62
63use crate::stocks::ta::common::{opt_cell, require_hlc};
64use crate::stocks::ta::ring::RingF64;
65use crate::util::error::{require_finite, FinanceError, FinanceResult};
66use crate::util::primitives::PeriodLength;
67use crate::{columns_with_strings, print_table_locale_opt};
68
69/// CCI lookback (on typical price).
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub struct CciParams {
72    pub period: usize,
73}
74
75impl CciParams {
76    pub const fn new(period: usize) -> Self {
77        Self { period }
78    }
79
80    pub const fn period_20() -> Self {
81        Self { period: 20 }
82    }
83
84    pub const fn period_14() -> Self {
85        Self { period: 14 }
86    }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90pub struct ValidatedCci {
91    params: CciParams,
92}
93
94impl ValidatedCci {
95    pub fn new(params: CciParams) -> FinanceResult<Self> {
96        PeriodLength::new(params.period)?;
97        Ok(Self { params })
98    }
99
100    pub fn params(self) -> CciParams {
101        self.params
102    }
103
104    pub fn compute(self, high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<CciSeries> {
105        cci_validated(high, low, close, self)
106    }
107}
108
109#[derive(Clone, Debug, PartialEq)]
110pub struct CciSeries {
111    pub cci: Vec<Option<f64>>,
112    pub params: CciParams,
113}
114
115impl CciSeries {
116    pub fn last(&self) -> Option<f64> {
117        self.cci.iter().rev().find_map(|x| *x)
118    }
119}
120
121/// Incremental CCI. After warm-up each push is **O(period)** (mean absolute deviation).
122#[derive(Clone, Debug)]
123pub struct CciState {
124    params: CciParams,
125    tp: RingF64,
126    scratch: Vec<f64>,
127    last: Option<f64>,
128}
129
130impl CciState {
131    pub fn new(params: CciParams) -> FinanceResult<Self> {
132        let _ = ValidatedCci::new(params)?;
133        Ok(Self {
134            params,
135            tp: RingF64::with_capacity(params.period),
136            scratch: Vec::with_capacity(params.period),
137            last: None,
138        })
139    }
140
141    pub fn from_history(
142        params: CciParams,
143        high: &[f64],
144        low: &[f64],
145        close: &[f64],
146    ) -> FinanceResult<Self> {
147        let mut s = Self::new(params)?;
148        let _ = s.push_bars(high, low, close)?;
149        Ok(s)
150    }
151
152    pub fn params(&self) -> CciParams {
153        self.params
154    }
155
156    pub fn reset(&mut self) {
157        self.tp.clear();
158        self.scratch.clear();
159        self.last = None;
160    }
161
162    pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<Option<f64>> {
163        require_finite("high", high)?;
164        require_finite("low", low)?;
165        require_finite("close", close)?;
166        if high < low {
167            return Err(FinanceError::InvalidCashflow {
168                message: "high must be >= low for each bar",
169            });
170        }
171        let tp = (high + low + close) / 3.0;
172        let _ = self.tp.push(tp);
173        if !self.tp.is_full() {
174            self.last = None;
175            return Ok(None);
176        }
177        self.tp.copy_ordered(&mut self.scratch);
178        let n = self.scratch.len() as f64;
179        let mean = self.tp.sum() / n;
180        let mut md = 0.0;
181        for &x in &self.scratch {
182            md += (x - mean).abs();
183        }
184        md /= n;
185        let out = if md <= 0.0 {
186            None
187        } else {
188            Some((tp - mean) / (0.015 * md))
189        };
190        self.last = out;
191        Ok(out)
192    }
193
194    pub fn push_bars(
195        &mut self,
196        high: &[f64],
197        low: &[f64],
198        close: &[f64],
199    ) -> FinanceResult<Vec<Option<f64>>> {
200        require_hlc(high, low, close)?;
201        let mut out = Vec::with_capacity(close.len());
202        for i in 0..close.len() {
203            out.push(self.push(high[i], low[i], close[i])?);
204        }
205        Ok(out)
206    }
207
208    pub fn last(&self) -> Option<f64> {
209        self.last
210    }
211}
212
213pub fn cci(
214    high: &[f64],
215    low: &[f64],
216    close: &[f64],
217    params: CciParams,
218) -> FinanceResult<CciSeries> {
219    ValidatedCci::new(params)?.compute(high, low, close)
220}
221
222fn cci_validated(
223    high: &[f64],
224    low: &[f64],
225    close: &[f64],
226    eng: ValidatedCci,
227) -> FinanceResult<CciSeries> {
228    let mut st = CciState::new(eng.params)?;
229    let cci = st.push_bars(high, low, close)?;
230    Ok(CciSeries {
231        cci,
232        params: eng.params,
233    })
234}
235
236#[derive(Clone, Debug)]
237pub struct CciSolution {
238    series: CciSeries,
239    close: Vec<f64>,
240    formula: String,
241}
242
243impl CciSolution {
244    pub fn series(&self) -> &CciSeries {
245        &self.series
246    }
247    pub fn formula(&self) -> &str {
248        &self.formula
249    }
250
251    pub fn print_table(&self) {
252        self.print_table_locale_opt(None, None);
253    }
254
255    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
256        self.print_table_locale_opt(Some(locale), Some(precision));
257    }
258
259    fn print_table_locale_opt(
260        &self,
261        locale: Option<&num_format::Locale>,
262        precision: Option<usize>,
263    ) {
264        let columns = columns_with_strings(&[
265            ("period", "i", true),
266            ("close", "f", true),
267            ("cci", "f", true),
268        ]);
269        let data = self
270            .close
271            .iter()
272            .enumerate()
273            .map(|(i, c)| vec![i.to_string(), c.to_string(), opt_cell(self.series.cci[i])])
274            .collect();
275        print_table_locale_opt(&columns, data, locale, precision);
276    }
277}
278
279/// # Examples
280/// ```
281/// use finance_solution::stocks::ta::{cci_solution, CciParams};
282/// let n = 30usize;
283/// let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64 * 0.1).collect();
284/// let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64 * 0.1).collect();
285/// let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64 * 0.1).collect();
286/// let sol = cci_solution(&h, &l, &c, CciParams::period_20()).unwrap();
287/// assert!(sol.formula().contains("0.015"));
288/// ```
289pub fn cci_solution(
290    high: &[f64],
291    low: &[f64],
292    close: &[f64],
293    params: CciParams,
294) -> FinanceResult<CciSolution> {
295    let series = cci(high, low, close, params)?;
296    Ok(CciSolution {
297        series,
298        close: close.to_vec(),
299        formula: format!(
300            "CCI = (TP - SMA(TP,{})) / (0.015 * MD); TP=(H+L+C)/3",
301            params.period
302        ),
303    })
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn constant_is_none() {
312        let x = vec![100.0; 25];
313        let s = cci(&x, &x, &x, CciParams::period_20()).unwrap();
314        assert!(s.cci[19].is_none());
315        assert!(s.cci[24].is_none());
316    }
317
318    #[test]
319    fn rising_path_positive() {
320        let n = 40usize;
321        let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64).collect();
322        let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64).collect();
323        let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64).collect();
324        let s = cci(&h, &l, &c, CciParams::period_20()).unwrap();
325        assert!(s.cci[39].unwrap() > 0.0);
326    }
327
328    #[test]
329    fn state_parity() {
330        let n = 50usize;
331        let h: Vec<_> = (0..n).map(|i| 12.0 + (i as f64) * 0.05).collect();
332        let l: Vec<_> = (0..n).map(|i| 10.0 + (i as f64) * 0.05).collect();
333        let c: Vec<_> = (0..n).map(|i| 11.0 + (i as f64) * 0.05).collect();
334        let p = CciParams::period_14();
335        let batch = cci(&h, &l, &c, p).unwrap();
336        let mut st = CciState::new(p).unwrap();
337        for i in 0..n {
338            let o = st.push(h[i], l[i], c[i]).unwrap();
339            match (o, batch.cci[i]) {
340                (None, None) => {}
341                (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9, "{a} vs {b}"),
342                other => panic!("{other:?}"),
343            }
344        }
345    }
346}