The 33 Markdown files under docs/wiki/ were never tracked. Commit them
into the repository so the documentation is versioned alongside the
code: 8 top-level pages plus 25 per-indicator deep dives under
indicators/{momentum,trend,volatility,volume}/.
The pages are kept in-repo (not pushed to a flat GitHub Wiki), so the
relative indicators/<family>/... links in Home.md resolve correctly
when rendered on GitHub.
200 lines
6.1 KiB
Markdown
200 lines
6.1 KiB
Markdown
# 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 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
|
||
|
||
| 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).
|