Average Directional Index (ADX)
The Average Directional Index (ADX) is used to quantify trend strength without regard to trend direction. It is derived from the Smoothed Moving Average of the Expanding Ranges (True Range). ADX is typically used with the Plus Directional Indicator (+DI) and Minus Directional Indicator (-DI) to identify the direction of the trend.
ADX
=ADX(data, period) Example Usage
=ADX(A2:F500, 14)
Parameters
| Parameter | Type | Description | Status |
|---|---|---|---|
data | Range | The input range of columns containing the Date, Open, High, Low, Close, and Volume data. | Required |
period | Number | The number of periods for the smoothing of the direction lines and the ADX itself. Default is 14. | Optional |
Returns
A multi-column array containing:
- Date
- ADX: The trend strength.
- +DI: The Positive Directional Indicator.
- -DI: The Negative Directional Indicator.
Source Code
Copy the following code into your Apps Script editor (Extensions > Apps Script) to use the ADX function in your spreadsheet.
adx.js
/**
* Calculates the Average Directional Index (ADX) to measure trend strength.
*
* @param {array} data - The input range. Must include at least 5 columns: Date, Open, High, Low, Close.
* @param {number} [period=14] - The smoothing period (default 14).
* @returns {array} A multi-column array with headers "Date", "ADX", "+DI", "-DI".
* @customfunction
*/
function ADX(data, period = 14) {
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}.`);
}
// --- END Validation ---
const dataRows = processedData.slice(1);
const results = [["Date", `ADX (${period})`, `+DI (${period})`, `-DI (${period})`]];
// REVISED LOGIC for Correct Alignment:
// 1. Calculate TR, +DM, -DM for all rows (0..N-1)
// 2. Smooth TR, +DM, -DM (Wilder's RMA).
// - First value at index (period).
// - Subsequent values recursive.
// 3. Calculate +DI, -DI from Smoothed values. (Valid from index `period` onwards).
// 4. Calculate DX. (Valid from index `period` onwards).
// 5. Calculate ADX (Smoothed DX).
// - First ADX value at index `period + period - 1` (Standard Wilder: 2*Period - 1).
// - Because first ADX is avg of first `period` DX values.
// - So we need `period` count of DXs. The first DX is at index `period`.
// - The `period`-th DX is at index `period + period - 1`.
// Let's implement this strictly with buffers to avoid index confusion.
const trs = [], plusDMs = [], minusDMs = [];
// Pass 1: Raw Metrics
for (let i = 0; i < dataRows.length; i++) {
const row = dataRows[i];
if (i === 0) {
trs.push(0); plusDMs.push(0); minusDMs.push(0);
continue;
}
const prevRow = dataRows[i - 1];
const h = row[2], l = row[3], c = row[4];
const ph = prevRow[2], pl = prevRow[3], pc = prevRow[4];
const tr = Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc));
const up = h - ph;
const down = pl - l;
let pdm = 0, mdm = 0;
if (up > down && up > 0) pdm = up;
if (down > up && down > 0) mdm = down;
trs.push(tr); plusDMs.push(pdm); minusDMs.push(mdm);
}
// Pass 2: Smooth TR/DM & Calculate DI/DX
const dxs = new Array(dataRows.length).fill(null);
const plusDIs = new Array(dataRows.length).fill(null);
const minusDIs = new Array(dataRows.length).fill(null);
let smTR = 0, smPDM = 0, smMDM = 0;
// First Sum (for Index = period)
// sum indices 1 to period (count = period)
// because index 0 is invalid/empty.
// Actually, standard ADX usually treats row 1 as index 0 of calculation?
// Let's assume row 0 (index 0) has no TR. row 1 (index 1) has TR.
// We need 'period' TRs. Indices 1..period.
if (dataRows.length > period) {
// Init Sums
let sTR = 0, sPDM = 0, sMDM = 0;
for (let k = 1; k <= period; k++) {
sTR += trs[k]; sPDM += plusDMs[k]; sMDM += minusDMs[k];
}
smTR = sTR; smPDM = sPDM; smMDM = sMDM;
// Calculate first DI/DX at index `period`
const calc = (t, p, m, idx) => {
const pdi = t === 0 ? 0 : (p / t) * 100;
const mdi = t === 0 ? 0 : (m / t) * 100;
const sum = pdi + mdi;
const dx = sum === 0 ? 0 : (Math.abs(pdi - mdi) / sum) * 100;
plusDIs[idx] = pdi; minusDIs[idx] = mdi; dxs[idx] = dx;
};
calc(smTR, smPDM, smMDM, period);
// Subsequent
for (let i = period + 1; i < dataRows.length; i++) {
const tr = trs[i], pdm = plusDMs[i], mdm = minusDMs[i];
// RMA: prev - (prev/n) + curr
smTR = smTR - (smTR / period) + tr;
smPDM = smPDM - (smPDM / period) + pdm;
smMDM = smMDM - (smMDM / period) + mdm;
calc(smTR, smPDM, smMDM, i);
}
}
// Pass 3: Calculate ADX from DX
// First ADX is at index `2*period - 1`?
// We need `period` valid DXs.
// First valid DX is at index `period`.
// So we sum DXs from index `period` to `period + period - 1`.
const startADX = period * 2; // Roughly
const adxs = new Array(dataRows.length).fill(null);
if (dataRows.length >= startADX) { // Or period*2
let sDX = 0;
// Sum DXs from index `period` to `2*period - 1`
// This gives us 'period' count of DXs.
for (let k = 0; k < period; k++) {
sDX += dxs[period + k];
}
let prevADX = sDX / period;
adxs[2 * period - 1] = prevADX; // Set first ADX
// Note: Some verify strict 2*period rules.
// index `period` is row `period` (0-based row index? No, dataRows index).
// dataRows[period] is the (period+1)th row. Correct.
for (let i = 2 * period; i < dataRows.length; i++) {
const dx = dxs[i];
const adx = ((prevADX * (period - 1)) + dx) / period;
adxs[i] = adx;
prevADX = adx;
}
}
// Final Alignment: Trim to the first valid ADX index
// Identify the first index where ADX is not null.
// Then slice Date, ADX, +DI, -DI from that index.
let firstValidIndex = -1;
for (let i = 0; i < adxs.length; i++) {
if (adxs[i] !== null && adxs[i] !== "") {
firstValidIndex = i;
break;
}
}
// If no valid ADX found, return empty result (just header)
results.length = 1;
if (firstValidIndex !== -1) {
for (let i = firstValidIndex; i < dataRows.length; i++) {
results.push([
dataRows[i][0],
adxs[i],
plusDIs[i],
minusDIs[i]
]);
}
}
return results;
}