Commodity Channel Index (CCI)


The Commodity Channel Index (CCI) measures the current price level relative to an average price level over a given period of time. CCI is relatively high when prices are far above their average, and relatively low when prices are far below their average. It is commonly used to identify overbought and oversold conditions and potential trend reversals.

CCI

=CCI(data, period)

Example Usage

=CCI(A2:F500, 20)

Parameters

Parameter Type Description Status
data
Range
The input range of columns containing the Date, Open, High, Low, Close, and Volume data. CCI uses High, Low, and Close to calculate the Typical Price.
Required
period
Number
The lookback period. Default is 20.
Optional

Returns

A two-column array of dates and their corresponding CCI values.

CCI Formula Result in Google Sheets

Source Code

Copy the following code into your Apps Script editor (Extensions > Apps Script) to use the CCI function in your spreadsheet.

cci.js
/**
 * Calculates the Commodity Channel Index (CCI).
 * CCI = (Typical Price - SMA(Typical Price)) / (0.015 * Mean Deviation)
 * Mean Deviation = SMA(abs(Typical Price - SMA(TP)))
 *
 * @param {array} data - The input range. Must include at least 5 columns: Date, Open, High, Low, Close.
 * @param {number} [period=20] - The lookback period (default 20).
 * @returns {array} A two-column array with headers "Date" and "CCI".
 * @customfunction
 */
function CCI(data, period = 20) {
    checkPremium();

    // Argument validation
    if (arguments.length < 1 || arguments.length > 2) {
        throw new Error(`Wrong number of arguments. Expected 1 or 2, but got ${arguments.length}.`);
    }
    if (period !== undefined) {
        if (typeof period !== 'number' || period <= 0 || !Number.isInteger(period)) {
            throw new Error(`Invalid period. The period must be a positive integer. Got: ${period}`);
        }
    }

    const processedData = getData(data);

    // --- Validate Data Structure ---
    const columnCount = processedData[0].length;
    if (columnCount < 5) {
        throw new Error(`Invalid data structure. Expected at least 5 columns (Date, O, H, L, C), but got ${columnCount}.`);
    }

    const dataRows = processedData.slice(1);
    const results = [["Date", `CCI (${period})`]];

    // We need a sliding window of Typical Prices (TP)
    // TP = (H + L + C) / 3
    const tpBuffer = [];

    for (let i = 0; i < dataRows.length; i++) {
        const row = dataRows[i];
        const date = row[0];
        const high = row[2];
        const low = row[3];
        const close = row[4];

        const tp = (high + low + close) / 3;
        tpBuffer.push(tp);

        if (tpBuffer.length > period) {
            tpBuffer.shift();
        }

        if (tpBuffer.length < period) {
            results.push([date, ""]);
            continue;
        }

        // Calculate SMA of TP
        let sumTP = 0;
        for (let j = 0; j < tpBuffer.length; j++) {
            sumTP += tpBuffer[j];
        }
        const smaTP = sumTP / period;

        // Calculate Mean Deviation
        // MeanDev = Sum(Abs(TP - SMA)) / period
        let sumDev = 0;
        for (let j = 0; j < tpBuffer.length; j++) {
            sumDev += Math.abs(tpBuffer[j] - smaTP);
        }
        const meanDev = sumDev / period;

        // Calculate CCI
        // 0.015 is the standard Lambert constant.
        // Handle div by zero if meanDev is 0 (price perfectly flat)
        let cci = 0;
        if (meanDev === 0) {
            cci = 0;
        } else {
            cci = (tp - smaTP) / (0.015 * meanDev);
        }

        results.push([date, cci]);
    }

    // Trim Output
    let firstValidIndex = -1;
    for (let i = 1; i < results.length; i++) {
        if (results[i][1] !== "" && results[i][1] !== null) {
            firstValidIndex = i;
            break;
        }
    }

    if (firstValidIndex !== -1) {
        return [results[0], ...results.slice(firstValidIndex)];
    } else {
        return [results[0]];
    }
}