Skip to content

CCI

Commodity Channel Index — measures how far the current typical price deviates from its rolling mean, in units of mean absolute deviation scaled by Lambert's constant.

Quick reference

FieldValue
FamilyMomentum Oscillators
Input typeCandle
Output typef64
Output rangeunbounded (typically [−200, +200] thanks to the 0.015 factor)
Default parametersperiod = 20 (Python)
Warmup periodperiod (20 for period = 20)
Interpretation> +100 overbought, < −100 oversold (Lambert)

Formula

For each candle, compute the typical price TP = (high + low + close) / 3, then over the rolling period-bar window:

SMA_TP_t = (TP_{t-period+1} + … + TP_t) / period
MAD_t    = (1 / period) · Σ |TP_i − SMA_TP_t|    for i = t-period+1 … t

CCI_t    = (TP_t − SMA_TP_t) / (factor · MAD_t)

The default factor is Lambert's 0.015, chosen empirically so that roughly 70–80 % of values fall inside [−100, +100]. The implementation exposes the factor through Cci::with_factor(period, factor) if you want to retune it for an asset with very different volatility characteristics.

When MAD == 0 (a perfectly flat window), the implementation returns 0 rather than dividing by zero.

Parameters

NameTypeDefault (Python)Valid rangeDescription
periodusize20>= 1Rolling window length for both the SMA of typical price and the MAD.
factorf640.015 (Cci::new)> 0, finiteLambert's scaling constant; configurable via Cci::with_factor.

Cci::new(0) returns Error::PeriodZero. Cci::with_factor(_, factor) returns Error::NonPositiveMultiplier when factor <= 0 or non-finite.

Inputs / Outputs

From impl Indicator for Cci:

rust
use wickra::{Indicator, Cci, Candle};
// Cci: Input = Candle, Output = f64
const _: fn(&mut Cci, Candle) -> Option<f64> = <Cci as Indicator>::update;

Python's CCI.batch(high, low, close) returns a 1-D float64 np.ndarray with NaN during warmup. Node's CCI.batch(high, low, close) returns a flat number[] (also NaN during warmup); the Node binding does not expose a streaming update() (bindings/node/index.d.ts lists only constructor and batch).

Warmup

warmup_period() returns exactly period. CCI does not consume diffs — it only needs period typical-price samples to populate its rolling window before it can compute an SMA and MAD. In streaming terms, calls 1..period return None; the period-th call returns the first value.

Edge cases

  • Flat input. Every TP is the SMA, so MAD == 0 and the implementation returns 0.0 (test flat_candles_yield_zero). This avoids the divide-by-zero that would otherwise produce NaN / ±∞.
  • Custom factor. Cci::with_factor(period, factor) lets you replace Lambert's 0.015. Picking a smaller factor widens the typical range of CCI values; picking a larger one compresses them.
  • Reset. reset() clears the rolling window and the running sum, returning the indicator to the freshly-constructed state.

Examples

Rust

rust
use wickra::{BatchExt, Candle, Cci, Indicator};

let candles: Vec<Candle> = (0..25)
    .map(|i| {
        let m = 50.0 + i as f64;
        Candle::new(m, m + 1.0, m - 1.0, m, 1.0, 0).unwrap()
    })
    .collect();
let mut cci = Cci::new(20)?;
let out = cci.batch(&candles);
println!("row 19 = {}", out[19].unwrap());
println!("row 24 = {}", out[24].unwrap());
# Ok::<(), wickra::Error>(())

Verified output:

row 19 = 126.66666666666667
row 24 = 126.66666666666667

Python

python
import numpy as np
import wickra as ta

i = np.arange(25, dtype=float)
m = 50.0 + i
high  = m + 1.0
low   = m - 1.0
close = m
cci = ta.CCI(20)
out = cci.batch(high, low, close)
print('row 19:', out[19])
print('row 24:', out[24])

Verified output:

row 19: 126.66666666666667
row 24: 126.66666666666667

Node

javascript
const wickra = require('wickra');

const n = 25;
const high = [], low = [], close = [];
for (let i = 0; i < n; i++) {
  const m = 50 + i;
  high.push(m + 1);
  low.push(m - 1);
  close.push(m);
}
const cci = new wickra.CCI(20);
const out = cci.batch(high, low, close);
console.log('row 19:', out[19]);
console.log('row 24:', out[24]);

Verified output:

row 19: 126.66666666666667
row 24: 126.66666666666667

Interpretation

  • ±100 threshold. Lambert's published convention is to treat values above +100 as overbought and below −100 as oversold. The choice of 0.015 for the divisor is what makes the threshold meaningful; changing the factor changes the threshold.
  • Zero-line cross. CCI crossing zero says the typical price has moved through its period-bar mean — sometimes used as a trend-direction filter.
  • Divergence. As with RSI/Stochastic, a price making a new high while CCI makes a lower high is a classic bearish divergence.

Common pitfalls

  • CCI is unbounded. Unlike RSI or Stochastic, CCI can spike well outside ±100 in volatile markets. Threshold-based rules should be paired with a maximum-absolute-value guard, or you will mis-classify legitimate breakouts as "extreme overbought".
  • The 0.015 factor is empirical, not derived. It was chosen by Lambert in 1980 for commodity futures markets. Modern equities and crypto have wider distributions; if your |CCI| distribution sits almost entirely outside ±100, retune via Cci::with_factor rather than rewriting downstream thresholds.

References

  • Donald Lambert, "Commodity Channel Index: Tools for Trading Cyclical Trends", Commodities Magazine, October 1980 — the original publication, including the empirical choice of 0.015.

See also