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:

  1. Date
  2. ADX: The trend strength.
  3. +DI: The Positive Directional Indicator.
  4. -DI: The Negative Directional Indicator.
ADX Formula Result in Google Sheets

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