McGinley Dynamic


The McGinley Dynamic looks like a moving average line but is a smoothing mechanism for prices that adjusts to shifts in market speed. This minimizes lag and avoids whipsaws, tracking the market far better than standard moving averages.

MCGINLEY

=MCGINLEY(data, period)

Example Usage

=MCGINLEY(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 lookback period. Default is 14.
Optional

Returns

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

McGinley Dynamic Formula Result in Google Sheets

Source Code

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

mcginley.js
/**
 * Calculates the McGinley Dynamic indicator.
 * A moving average that adjusts to market speed to avoid lag and whipsaws.
 * Formula: MD[i] = MD[i-1] + (Price[i] - MD[i-1]) / (k * n * (Price[i]/MD[i-1])^4)
 * Where k is a constant (usually 0.6) and n is period.
 *
 * @param {array} data - The input range. Must include at least 2 columns: Date and Close (or Value).
 * @param {number} [period=14] - The lookback period (default 14).
 * @returns {array} A two-column array with headers "Date" and "McGinley Dynamic".
 * @customfunction
 */
function MCGINLEY(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);

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

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

    // Logic
    // MD requires a previous value.
    // Initialization: Usually SMA of first N periods, or just the first Price.
    // McGinley often starts simply at Price[0] because it adapts quickly.

    let prevMD = 0;
    // Constant N (roughly 60% of period is standard for the 'N' factor in the formula denominator 
    // Wait, the formula usually cited is: MD + (Price - MD) / (k * N * (P/MD)^4)
    // Where k*N acts as the time constant.
    // Standard implementation uses N directly as the user input period.
    // Some sources say factor = N * 0.6. Let's use period directly as "N" in the standard formula context, 
    // or verify if "k" is separate.
    // TradingView's code: `md = 0.0; md := na(md[1]) ? close : md[1] + (close - md[1]) / (k * n * pow(close/md[1], 4))`
    // where k is often 0.6.

    // Constant N (usually the period itself in TradingView implementation)
    // TradingView simplifies the formula by using N as the smoothing factor directly.
    // This effectively sets k=1 relative to the Investopedia definition.
    const n = period;

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

        if (price === "" || price === null || typeof price !== 'number') {
            results.push([date, ""]);
            continue;
        }

        let md = 0;

        if (i === 0) {
            // Initialize with first price
            md = price;
        } else {
            // Formula
            // (Price / PrevMD) ^ 4
            if (prevMD === 0) prevMD = price; // Safety

            const ratio = price / prevMD;
            // Original McGinley: k * n * (ratio^4) -> k=0.6
            // TradingView: n * (ratio^4) -> k=1
            const denominator = n * Math.pow(ratio, 4);

            // Avoid division by zero or extreme values if price jumps wildly?
            // The formula handles it generally.

            md = prevMD + (price - prevMD) / denominator;
        }

        results.push([date, md]);
        prevMD = md;
    }

    // Trim Output
    // McGinley usually starts valid from index 0 if price is valid.
    // But if we encounter initial nulls (e.g. bad data), we trim.

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