Files
wickra/docs/wiki/indicators/momentum/Indicator-Cci.md
T

200 lines
6.1 KiB
Markdown
Raw Normal View History

2026-05-22 16:18:04 +02:00
# 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
| Field | Value |
|-------|-------|
| Family | Momentum |
| Sub-category | unbounded oscillator |
| Input type | `Candle` |
| Output type | `f64` |
| Output range | unbounded (typically `[200, +200]` thanks to the 0.015 factor) |
| Default parameters | `period = 20` (Python) |
| Warmup period | `period` (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 7080 % 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
| Name | Type | Default (Python) | Valid range | Description |
|------|------|------------------|-------------|-------------|
| `period` | `usize` | `20` | `>= 1` | Rolling window length for both the SMA of typical price and the MAD. |
| `factor` | `f64` | `0.015` (`Cci::new`) | `> 0`, finite | Lambert'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
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64>;
```
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
- [Indicator: Rsi](Indicator-Rsi.md) — bounded sibling for comparison.
- [Indicator: WilliamsR](Indicator-WilliamsR.md) — another candle-input
oscillator, range-based rather than deviation-based.
- [Indicator: Mfi](Indicator-Mfi.md) — volume-weighted RSI; useful as a
confirmation alongside CCI.
- [Warmup Periods](../../Warmup-Periods.md) — `period` (no off-by-one).