CCI Indicator in Pine Script v6: Detect Cyclical Price Reversals with Precision
Every trader has faced this frustrating scenario: you enter a trend just as it exhausts itself, or you exit a position right before a powerful reversal completes. The Commodity Channel Index (CCI) was engineered specifically to solve this problem — it quantifies how far price has deviated from its statistical norm, giving you a mathematically grounded signal for when a cycle is overextended and likely to reverse. In this guide, we'll build a fully functional CCI indicator in Pine Script v6, dissect the math behind it, and wire up actionable overbought/oversold signals — all with verified, compile-ready code.
📐 The Mathematics of CCI
Developed by Donald Lambert in 1980, the CCI measures the deviation of the Typical Price from its Simple Moving Average, normalized by the Mean Absolute Deviation (MAD). The formula is:
$$\text{Typical Price} = \frac{\text{High} + \text{Low} + \text{Close}}{3}$$
$$\text{CCI} = \frac{\text{TP} - \text{SMA}(\text{TP}, n)}{0.015 \times \text{MAD}(\text{TP}, n)}$$
Where MAD is the Mean Absolute Deviation — the average of the absolute differences between each value in the window and the window's mean. In Pine Script v6, this is directly available via the built-in ta.dev() function, which computes the mean absolute deviation of a series over a given length. The constant 0.015 was chosen by Lambert so that approximately 70–80% of CCI values fall between −100 and +100 under normal market conditions.
| CCI Zone | Interpretation | Typical Action |
|---|---|---|
| Above +100 | Overbought / Strong upward deviation | Watch for bearish reversal signal |
| −100 to +100 | Normal range / No extreme deviation | Trend-following or neutral |
| Below −100 | Oversold / Strong downward deviation | Watch for bullish reversal signal |
🎯 Trading Strategy: CCI Crossback Reversal
The core strategy implemented in this script is the CCI Crossback method:
- Bullish Signal: CCI crosses above the −100 level after being oversold. This indicates the price deviation has peaked to the downside and is mean-reverting upward.
- Bearish Signal: CCI crosses below the +100 level after being overbought. This indicates the upward deviation is exhausting and price is reverting downward.
This strategy is most effective in range-bound or cyclical markets (e.g., commodities, mean-reverting ETFs, or equity indices during consolidation phases). In strongly trending markets, CCI can remain in overbought/oversold territory for extended periods — always combine with a trend filter for best results.
🧮 Step-by-Step Code Walkthrough
Step 1: Inputs and Typical Price
We start by defining user inputs and computing the Typical Price (TP), which is the foundation of all CCI calculations.
//@version=6
indicator(title = "CCI Reversal Signals", shorttitle = "CCI-Rev", overlay = false)
// --- Inputs ---
int cciLength = input.int(20, title = "CCI Length", minval = 2)
float obLevel = input.float(100.0, title = "Overbought Level", minval = 1.0)
float osLevel = input.float(-100.0, title = "Oversold Level", maxval = -1.0)
bool showSignals = input.bool(true, title = "Show Signals")
// --- Typical Price ---
// TP = (High + Low + Close) / 3
float tp = (high + low + close) / 3.0
Step 2: CCI Calculation Using ta.dev()
Pine Script v6 provides ta.dev() which computes the Mean Absolute Deviation directly. This is the exact denominator component Lambert specified. We guard against a zero MAD to prevent division-by-zero errors.
// --- SMA of Typical Price ---
float tpSma = ta.sma(tp, cciLength)
// --- Mean Absolute Deviation via ta.dev() ---
// ta.dev() computes the mean absolute deviation of a series over a length
float mad = ta.dev(tp, cciLength)
// --- CCI Formula ---
// Guard against division by zero when MAD is 0 (flat price action)
float cci = mad != 0.0 ? (tp - tpSma) / (0.015 * mad) : 0.0
Step 3: Signal Detection
We detect the crossback events using ta.crossover() and ta.crossunder(). These are computed at the global scope so their internal history buffers are always populated correctly.
// --- Signal Detection (global scope — required for ta.* history integrity) ---
// Bullish: CCI crosses ABOVE the oversold level (e.g., crosses above -100)
bool crossAboveOS = ta.crossover(cci, osLevel)
// Bearish: CCI crosses BELOW the overbought level (e.g., crosses below +100)
bool crossBelowOB = ta.crossunder(cci, obLevel)
Step 4: Plotting — Global Scope with Conditional Logic
All plot(), plotshape(), and bgcolor() calls are placed at the global scope. Conditional visibility is handled by passing na as the series value when signals are disabled — this is the correct Pine Script v6 pattern.
🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator(title = "CCI Reversal Signals", shorttitle = "CCI-Rev", overlay = false)
// ─────────────────────────────────────────────
// INPUTS
// ─────────────────────────────────────────────
int cciLength = input.int(20, title = "CCI Length", minval = 2)
float obLevel = input.float(100.0, title = "Overbought Level", minval = 1.0)
float osLevel = input.float(-100.0, title = "Oversold Level", maxval = -1.0)
bool showSignals = input.bool(true, title = "Show Signals")
// ─────────────────────────────────────────────
// TYPICAL PRICE
// ─────────────────────────────────────────────
// Lambert's Typical Price: average of High, Low, Close
float tp = (high + low + close) / 3.0
// ─────────────────────────────────────────────
// CCI CALCULATION
// ─────────────────────────────────────────────
// Step 1: SMA of Typical Price over the lookback window
float tpSma = ta.sma(tp, cciLength)
// Step 2: Mean Absolute Deviation via built-in ta.dev()
// ta.dev() returns the mean absolute deviation of the source series
float mad = ta.dev(tp, cciLength)
// Step 3: CCI = (TP - SMA) / (0.015 * MAD)
// Guard: if MAD is 0 (perfectly flat price), return 0 to avoid division by zero
float cci = mad != 0.0 ? (tp - tpSma) / (0.015 * mad) : 0.0
// ─────────────────────────────────────────────
// SIGNAL DETECTION — global scope (CRITICAL)
// ta.crossover/crossunder maintain internal history buffers;
// they MUST run on every bar to produce correct results.
// ─────────────────────────────────────────────
// Bullish crossback: CCI rises back above the oversold level
bool crossAboveOS = ta.crossover(cci, osLevel)
// Bearish crossback: CCI falls back below the overbought level
bool crossBelowOB = ta.crossunder(cci, obLevel)
// ─────────────────────────────────────────────
// ZONE DETECTION for background coloring
// ─────────────────────────────────────────────
bool inOverbought = cci >= obLevel // CCI is in overbought territory
bool inOversold = cci <= osLevel // CCI is in oversold territory
// ─────────────────────────────────────────────
// PLOTS — all at global scope
// ─────────────────────────────────────────────
// Main CCI line
plot(cci,
title = "CCI",
color = color.new(color.blue, 0),
linewidth = 2)
// Overbought reference line (constant series — safe for separate pane)
plot(obLevel,
title = "Overbought",
color = color.new(color.red, 40),
linewidth = 1)
// Oversold reference line
plot(osLevel,
title = "Oversold",
color = color.new(color.green, 40),
linewidth = 1)
// Zero line
plot(0,
title = "Zero Line",
color = color.new(color.gray, 60),
linewidth = 1)
// ─────────────────────────────────────────────
// BACKGROUND COLORING — global scope
// Conditional color passed as series argument;
// na color = no background drawn (transparent)
// ─────────────────────────────────────────────
bgcolor(inOverbought ? color.new(color.red, 88) : na,
title = "Overbought Zone")
bgcolor(inOversold ? color.new(color.green, 88) : na,
title = "Oversold Zone")
// ─────────────────────────────────────────────
// SIGNAL SHAPES — global scope
// When showSignals is false, pass na as the series value
// so plotshape draws nothing without a compile error.
// location.absolute requires a numeric (float) series, not bool.
// We pass `cci` when the signal fires, or `na` to suppress drawing.
// ─────────────────────────────────────────────
plotshape(showSignals ? (crossAboveOS ? cci : na) : na,
title = "Bullish Signal",
style = shape.triangleup,
location = location.absolute,
color = color.new(color.green, 0),
size = size.small)
plotshape(showSignals ? (crossBelowOB ? cci : na) : na,
title = "Bearish Signal",
style = shape.triangledown,
location = location.absolute,
color = color.new(color.red, 0),
size = size.small)
🔬 Deep Dive: Why 0.015 and Why MAD?
The choice of Mean Absolute Deviation over Standard Deviation is deliberate. MAD is more robust to outliers — a single extreme price spike inflates standard deviation quadratically, while MAD only inflates it linearly. This makes CCI more stable during flash crashes or gap events.
The constant 0.015 is a scaling factor. Lambert empirically determined that with this constant and a 20-period window on commodity futures, roughly 70–80% of CCI readings fall within [−100, +100]. This means readings outside that band are statistically unusual — they represent genuine deviations from the cyclical norm, not random noise.
| Parameter | Default | Effect of Increasing | Effect of Decreasing |
|---|---|---|---|
| CCI Length | 20 | Smoother, fewer signals, longer cycles | Noisier, more signals, shorter cycles |
| Overbought Level | +100 | Fewer bearish signals, higher conviction | More bearish signals, lower conviction |
| Oversold Level | −100 | More bullish signals, lower conviction | Fewer bullish signals, higher conviction |
⚙️ Key Pine Script v6 Engineering Decisions
Several architectural choices in this script reflect important Pine Script v6 best practices:
- Global scope for all ta.* calls: Functions like
ta.crossover(),ta.sma(), andta.dev()maintain internal history buffers. Placing them inside conditional blocks (e.g.,if barstate.islast) would cause those buffers to be incomplete, producingnaor incorrect results. All stateful functions run unconditionally on every bar. - plot() for reference lines instead of hline(): Since this indicator uses
overlay = false(separate pane),plot(obLevel, ...)is used for the +100/−100 reference lines. This is safe and correct for a separate-pane indicator. - location.absolute with numeric series:
plotshape()withlocation = location.absoluterequires a numeric (float) series, not a bool. We passcci(the current CCI value) when the signal fires, andnaotherwise. This places the shape precisely at the CCI line level. - Conditional visibility via na: Instead of wrapping
plotshape()in anifblock (which would break history), we passnaas the series argument whenshowSignalsis false. This is the idiomatic Pine Script v6 pattern for toggling visual elements.
✅ Conclusion
- The CCI quantifies price deviation from its cyclical mean using the formula $ \text{CCI} = \frac{\text{TP} - \text{SMA(TP)}}{0.015 \times \text{MAD(TP)}} $, where MAD is computed via Pine Script v6's built-in
ta.dev()function. - The crossback strategy (CCI crossing back inside the ±100 bands) provides statistically grounded reversal signals, most reliable in cyclical or range-bound markets.
- All stateful ta.* functions and visual output functions (
plot,plotshape,bgcolor) must reside at the global scope in Pine Script v6; conditional logic is applied throughnaseries values, not by wrapping calls inifblocks.
🚀 Ideas for Advancement
- Multi-Timeframe CCI Confluence: Use
request.security()to fetch CCI from a higher timeframe (e.g., weekly) and only trigger signals when both the current and higher-timeframe CCIs agree on direction — dramatically reducing false signals in trending markets. - CCI Divergence Detection: Compare CCI peaks/troughs against price peaks/troughs using
ta.highest()andta.lowest()over a rolling window to automatically flag bullish and bearish divergences, which are among the highest-conviction CCI signals.
Comments
Post a Comment