/**
* McGinley Dynamic Indicator
* Adaptive moving average that adjusts to market volatility and reduces lag
*/
export interface McGinleyDynamicData {
// McGinley Dynamic value
mcgDyn: number
// Trend analysis
trend: 'bullish' | 'bearish' | 'neutral'
// Current price vs McGinley Dynamic
priceVsMcgDyn: number // Price vs McGinley Dynamic (%)
position: 'above' | 'below' | 'equal'
// Adaptive characteristics
adaptationRate: number // Current adaptation rate
volatilityFactor: number // Market volatility factor
// Signal strength
strength: number // Trend strength (0-100)
// Lag reduction effectiveness
lagReduction: number // How much lag is reduced vs SMA (%)
}
/**
* Calculate McGinley Dynamic
* @param prices Array of closing prices
* @param period Period for calculation (default 20)
* @returns McGinleyDynamicData object
*/
export function calculateMcGinleyDynamic(
prices: number[],
period: number = 20
): McGinleyDynamicData | null {
if (prices.length < period) {
return null
}
// Calculate volatility factor (standard deviation of recent prices)
const recentPrices = prices.slice(-period)
const mean = recentPrices.reduce((sum, price) => sum + price, 0) / period
const variance = recentPrices.reduce((sum, price) => sum + Math.pow(price - mean, 2), 0) / period
const volatility = Math.sqrt(variance)
const volatilityFactor = volatility / mean // Coefficient of variation
// Adaptive K factor based on volatility
// Higher volatility = faster adaptation (lower K)
const baseK = 0.6 // Standard K factor
const adaptiveK = baseK + (volatilityFactor * 0.4) // Adjust K based on volatility
const k = Math.max(0.1, Math.min(0.9, adaptiveK)) // Keep K between 0.1 and 0.9
// Calculate McGinley Dynamic recursively
let mcgDyn: number
if (prices.length === period) {
// First calculation - use simple average
mcgDyn = recentPrices.reduce((sum, price) => sum + price, 0) / period
} else {
// Recursive calculation
const prevPrices = prices.slice(-period - 1, -1)
const prevMcgDyn = calculateMcGinleyDynamic(prevPrices, period)
if (!prevMcgDyn) {
mcgDyn = recentPrices.reduce((sum, price) => sum + price, 0) / period
} else {
const currentPrice = prices[prices.length - 1]
mcgDyn = prevMcgDyn.mcgDyn + (currentPrice - prevMcgDyn.mcgDyn) / (k * period * Math.pow(currentPrice / prevMcgDyn.mcgDyn, 4))
}
}
// Get current price
const currentPrice = prices[prices.length - 1]
// Price vs McGinley Dynamic
const priceVsMcgDyn = ((currentPrice - mcgDyn) / mcgDyn) * 100
let position: 'above' | 'below' | 'equal' = 'equal'
if (currentPrice > mcgDyn * 1.0001) {
position = 'above'
} else if (currentPrice < mcgDyn * 0.9999) {
position = 'below'
}
// Determine trend
let trend: 'bullish' | 'bearish' | 'neutral' = 'neutral'
if (position === 'above') {
trend = 'bullish'
} else if (position === 'below') {
trend = 'bearish'
}
// Calculate trend strength based on deviation and volatility
const deviationStrength = Math.abs(priceVsMcgDyn) * 2
const volatilityStrength = volatilityFactor * 50
const strength = Math.min(100, deviationStrength + volatilityStrength)
// Calculate lag reduction effectiveness vs SMA
const sma = recentPrices.reduce((sum, price) => sum + price, 0) / period
const mcgDynLag = Math.abs(currentPrice - mcgDyn)
const smaLag = Math.abs(currentPrice - sma)
const lagReduction = smaLag > 0 ? ((smaLag - mcgDynLag) / smaLag) * 100 : 0
return {
mcgDyn,
trend,
priceVsMcgDyn,
position,
adaptationRate: k,
volatilityFactor,
strength,
lagReduction
}
}
/**
* Calculate McGinley Dynamic for multiple periods
* @param prices Array of closing prices
* @param periods Array of periods to calculate
* @returns Array of McGinleyDynamicData objects
*/
export function calculateMultipleMcGinleyDynamic(
prices: number[],
periods: number[] = [10, 20, 30]
): McGinleyDynamicData[] {
return periods
.map(period => calculateMcGinleyDynamic(prices, period))
.filter((mcg): mcg is McGinleyDynamicData => mcg !== null)
}
/**
* Get McGinley Dynamic crossover signal
* @param fastMcg Fast period McGinley Dynamic
* @param slowMcg Slow period McGinley Dynamic
* @returns Crossover signal
*/
export function getMcGinleyDynamicCrossoverSignal(
fastMcg: McGinleyDynamicData,
slowMcg: McGinleyDynamicData
): 'bullish_crossover' | 'bearish_crossover' | 'neutral' {
const fastAboveSlow = fastMcg.mcgDyn > slowMcg.mcgDyn
if (fastAboveSlow && fastMcg.position === 'above' && slowMcg.position === 'above') {
return 'bullish_crossover'
} else if (!fastAboveSlow && fastMcg.position === 'below' && slowMcg.position === 'below') {
return 'bearish_crossover'
}
return 'neutral'
}
/**
* Calculate McGinley Dynamic slope
* @param mcg McGinleyDynamicData object
* @param prices Array of prices for slope calculation
* @param slopePeriod Period to calculate slope over
* @returns Slope value
*/
export function calculateMcGinleyDynamicSlope(
mcg: McGinleyDynamicData,
prices: number[],
slopePeriod: number = 5
): number {
if (prices.length < slopePeriod) {
return 0
}
// Calculate slope using linear regression over recent McGinley values
const recentPrices = prices.slice(-slopePeriod)
let sumX = 0
let sumY = 0
let sumXY = 0
let sumXX = 0
for (let i = 0; i < slopePeriod; i++) {
const mcgValue = calculateMcGinleyDynamic(recentPrices.slice(0, i + 1), 20)?.mcgDyn || recentPrices[i]
sumX += i
sumY += mcgValue
sumXY += i * mcgValue
sumXX += i * i
}
const n = slopePeriod
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX)
return slope
}