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