CCI in Python — Coding It from Scratch (pandas)

Typical price, mean absolute deviation, and the 0.015 constant

CCI computed in Python
CCI computed in Python

The CCI has one part everyone gets wrong: the denominator uses mean absolute deviation, not standard deviation. Get that right and the rest is a typical price and a moving average. See the explainer for the concept.

What you’ll need

  • Python 3.9+, pandas/numpy
  • high, low, close series.

From scratch

import numpy as np


def cci(high, low, close, period: int = 20, constant: float = 0.015):
    tp = (high + low + close) / 3                      # typical price
    ma = tp.rolling(period).mean()

    # Mean ABSOLUTE deviation of TP from its moving average (NOT std dev).
    mad = tp.rolling(period).apply(
        lambda x: np.abs(x - x.mean()).mean(), raw=True)

    return (tp - ma) / (constant * mad)

tp.rolling(period).apply(lambda x: np.abs(x - x.mean()).mean()) is the mean absolute deviation — the average distance of each windowed typical price from the window’s mean. That’s what Lambert specified, and it’s the line that trips up most implementations.

Both readings, in code

Because CCI supports opposite strategies, spell out which you mean:

c = cci(df["high"], df["low"], df["close"])

# Breakout reading (Lambert's lean): enter with a push above +100.
df["breakout_long"] = (c > 100) & (c.shift(1) <= 100)

# Reversal reading: fade the extreme back toward zero.
df["reversal_long"] = (c > -100) & (c.shift(1) <= -100)

Pick one to match your regime and backtest it in AlgoGen — they behave very differently.

Gotchas

  • Mean absolute deviation, not standard deviation. x.std() gives the wrong denominator and a CCI that matches no platform. Use abs(x - x.mean()).mean().
  • The 0.015 constant. It calibrates ~70–80% of values into ±100; don’t drop it or “round” it.
  • Typical price. (H+L+C)/3, not close alone.
  • Unbounded. Don’t clip CCI to ±100; the excursions beyond are the signal.

Pair it with a regime filter

Since the reversal reading dies in trends and the breakout reading dies in ranges, gate CCI on an ADX-style regime check:

# Only fade +/-100 when NOT strongly trending; ride breakouts when trending.
a = adx(df["high"], df["low"], df["close"])["adx"]   # from the ADX post
ranging = a < 20
df["fade_short"] = ranging & (c > 100) & (c.shift(1) <= 100)
df["ride_long"]  = (~ranging) & (c > 100) & (c.shift(1) <= 100)

The same +100 cross means “fade” in a range and “ride” in a trend — the regime filter picks which. That’s the single most important thing to get right with CCI.

The lazy one-liner

import pandas_ta as ta
df["cci20"] = ta.cci(df["high"], df["low"], df["close"], length=20)

Same indicator elsewhere: MQL5, Pine Script, EasyLanguage, NinjaScript.


This post is educational, not financial advice. Indicators describe the past; they don’t predict the future. Backtest anything before you risk real money on it.

Sources and further reading

  1. Windowing operations (pandas documentation)

Historical research from the Algogen archive. Not investment advice.

Blog

Have a strategy idea?

Describe it in plain English, preview where your rules fire, then decide whether the evidence is worth a backtest.

Keep me postedHow it works