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