Coppock Curve


The Coppock Curve is a long-term price momentum indicator used primarily to identify major bottoms in the stock market. It is calculated as a 10-month weighted moving average of the sum of the 14-month rate of change and the 11-month rate of change.

COPPOCK

=COPPOCK(data, longROC, shortROC, wmaPeriod)

Example Usage

=COPPOCK(A2:F500)

Parameters

Parameter Type Description Status
data
Range
The input range of columns containing the Date, Open, High, Low, Close, and Volume data.
Required
longROC
Number
The period for the long-term Rate of Change. Default is 14.
Optional
shortROC
Number
The period for the short-term Rate of Change. Default is 11.
Optional
wmaPeriod
Number
The period for the Weighted Moving Average (WMA). Default is 10.
Optional

Returns

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

Coppock Curve Formula Result in Google Sheets

Source Code

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

coppock.js
/**
 * Calculates the Coppock Curve.
 * A momentum indicator designed for long-term trend following.
 * Formula: WMA(10) of (ROC(14) + ROC(11))
 *
 * @param {array} data - The input range. Must include at least 2 columns: Date, Value (Close).
 * @param {number} [longROC=14] - Long ROC period (default 14).
 * @param {number} [shortROC=11] - Short ROC period (default 11).
 * @param {number} [wmaPeriod=10] - WMA smoothing period (default 10).
 * @returns {array} A two-column array with headers "Date" and "Coppock Curve".
 * @customfunction
 */
function COPPOCK(data, longROC = 14, shortROC = 11, wmaPeriod = 10) {
    checkPremium();

    // Argument validation
    if (arguments.length > 4) {
        throw new Error(`Wrong number of arguments. Expected up to 4.`);
    }

    const processedData = getData(data);

    let valueIndex = 1;
    if (processedData[0].length >= 5) {
        valueIndex = 4; // Close
    }

    const dataRows = processedData.slice(1);
    const results = [["Date", `Coppock Curve (${longROC}, ${shortROC}, ${wmaPeriod})`]];

    // Logic: 
    // 1. Calculate ROC(14) and ROC(11).
    // 2. Add them together.
    // 3. WMA(10) of the sum.

    // We need historical prices for ROC.
    // Max Lookback buffer needed = max(longROC, shortROC)
    const maxLookback = Math.max(longROC, shortROC);
    const priceBuffer = []; // Sliding window of prices

    // We need to store the Sum(ROC) values to feed into WMA algorithm?
    // Or can we implement WMA inline? 
    // WMA needs past values relative to its own window (wmaPeriod).
    // So we need a buffer of (ROC Sums) of size wmaPeriod.

    const rocSumBuffer = [];

    for (let i = 0; i < dataRows.length; i++) {
        const row = dataRows[i];
        const date = row[0];
        const price = row[valueIndex];

        priceBuffer.push(price);
        if (priceBuffer.length > maxLookback + 1) {
            priceBuffer.shift();
        }

        // Need enough data for ROCs
        if (priceBuffer.length <= maxLookback) {
            results.push([date, ""]);
            continue;
        }

        // Calculate ROCs
        // ROC = ((Price - PriceObs) / PriceObs) * 100
        // Buffer Index: Last is current.
        // PriceObs for LongROC is at: current (len-1) - longROC

        const currentPrice = priceBuffer[priceBuffer.length - 1];

        const priceLong = priceBuffer[priceBuffer.length - 1 - longROC];
        const priceShort = priceBuffer[priceBuffer.length - 1 - shortROC];

        const rocLong = ((currentPrice - priceLong) / priceLong) * 100;
        const rocShort = ((currentPrice - priceShort) / priceShort) * 100;

        const rocSum = rocLong + rocShort;

        // Push sum to WMA input buffer
        rocSumBuffer.push(rocSum);
        if (rocSumBuffer.length > wmaPeriod) {
            rocSumBuffer.shift();
        }

        // Calculate WMA
        if (rocSumBuffer.length < wmaPeriod) {
            results.push([date, ""]);
            continue;
        }

        // WMA Logic
        let weightSum = 0;
        let weightedValSum = 0;

        for (let j = 0; j < rocSumBuffer.length; j++) {
            const weight = j + 1;
            weightedValSum += rocSumBuffer[j] * weight;
            weightSum += weight;
        }

        const wma = weightedValSum / weightSum;
        results.push([date, wma]);
    }

    // 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]];
    }
}