/**
* McClellan Oscillator
* Market breadth indicator based on EMA of advancing minus declining stocks
*/
/**
* Calculate McClellan Oscillator
* @param advances Array of advancing stocks each period
* @param declines Array of declining stocks each period
* @returns McClellanOscillatorData object
*/
export function calculateMcClellanOscillator(advances, declines, fastPeriod = 19, slowPeriod = 39) {
if (advances.length !== declines.length || advances.length < slowPeriod) {
return null;
}
// Calculate ratio-adjusted advances: (advances - declines) / (advances + declines) * 1000
const ratioAdjustedAdvances = [];
for (let i = 0; i < advances.length; i++) {
const total = advances[i] + declines[i];
const ratio = total > 0 ? ((advances[i] - declines[i]) / total) * 1000 : 0;
ratioAdjustedAdvances.push(ratio);
}
// Calculate EMAs
const ema19 = calculateEMA(ratioAdjustedAdvances, fastPeriod);
const ema39 = calculateEMA(ratioAdjustedAdvances, slowPeriod);
if (!ema19 || !ema39) {
return null;
}
// Calculate McClellan Oscillator: 19-day EMA - 39-day EMA
const oscillator = ema19 - ema39;
// Get current ratio-adjusted advances
const currentRatio = ratioAdjustedAdvances[ratioAdjustedAdvances.length - 1];
// Determine trend based on oscillator value
let trend;
if (oscillator > 70) {
trend = 'overbought';
}
else if (oscillator > 0) {
trend = 'bullish';
}
else if (oscillator > -70) {
trend = 'neutral';
}
else {
trend = 'oversold';
}
// Calculate signal strength
const strength = Math.min(100, Math.abs(oscillator) / 2);
// Check overbought/oversold levels
const overbought = oscillator > 70;
const oversold = oscillator < -70;
// Check zero line crosses
let bullishSignal = false;
let bearishSignal = false;
if (ratioAdjustedAdvances.length >= 40) {
// Calculate previous oscillator
const prevRatioAdjusted = ratioAdjustedAdvances.slice(0, -1);
const prevEMA19 = calculateEMA(prevRatioAdjusted, 19);
const prevEMA39 = calculateEMA(prevRatioAdjusted, 39);
const prevOscillator = prevEMA19 && prevEMA39 ? prevEMA19 - prevEMA39 : 0;
if (prevOscillator !== null && prevOscillator <= 0 && oscillator > 0) {
bullishSignal = true;
}
else if (prevOscillator !== null && prevOscillator >= 0 && oscillator < 0) {
bearishSignal = true;
}
}
// Generate trading signal
let signal = 'neutral';
if (bullishSignal && !overbought) {
signal = 'buy';
}
else if (bearishSignal && !oversold) {
signal = 'sell';
}
else if (oversold) {
signal = 'buy';
}
else if (overbought) {
signal = 'sell';
}
// Determine market breadth condition
let breadthCondition;
if (oscillator > 100) {
breadthCondition = 'extremely_bullish';
}
else if (oscillator > 20) {
breadthCondition = 'bullish';
}
else if (oscillator > -20) {
breadthCondition = 'neutral';
}
else if (oscillator > -100) {
breadthCondition = 'bearish';
}
else {
breadthCondition = 'extremely_bearish';
}
return {
oscillator,
ratioAdjustedAdvances: currentRatio,
ema19,
ema39,
trend,
strength,
overbought,
oversold,
bullishSignal,
bearishSignal,
signal,
breadthCondition
};
}
/**
* Helper function to calculate EMA
*/
function calculateEMA(values, period) {
if (values.length < period) {
return 0;
}
const multiplier = 2 / (period + 1);
let ema = values[0];
for (let i = 1; i < values.length; i++) {
ema = (values[i] - ema) * multiplier + ema;
}
return ema;
}
/**
* Calculate McClellan Summation Index
* @param advances Array of advancing stocks
* @param declines Array of declining stocks
* @returns McClellan Summation Index value
*/
export function calculateMcClellanSummation(advances, declines) {
if (advances.length !== declines.length) {
return 0;
}
const oscillator = calculateMcClellanOscillator(advances, declines);
if (!oscillator) {
return 0;
}
// Summation is the cumulative sum of the oscillator
// This is a simplified version - in practice, it would accumulate over time
return oscillator.oscillator;
}
/**
* Get McClellan Oscillator interpretation
* @param mco McClellanOscillatorData object
* @returns Human-readable interpretation
*/
export function getMcClellanInterpretation(mco) {
const { oscillator, trend, breadthCondition, bullishSignal, bearishSignal } = mco;
let interpretation = `McClellan Oscillator: ${oscillator.toFixed(2)}`;
if (bullishSignal) {
interpretation += ' - Bullish zero line crossover';
}
else if (bearishSignal) {
interpretation += ' - Bearish zero line crossover';
}
else {
interpretation += ` - ${trend} (${breadthCondition.replace('_', ' ')})`;
}
return interpretation;
}
/**
* Analyze McClellan Oscillator for market timing
* @param advances Array of advancing stocks over time
* @param declines Array of declining stocks over time
* @returns Market timing analysis
*/
export function analyzeMcClellanTiming(advances, declines) {
const oscillator = calculateMcClellanOscillator(advances, declines);
if (!oscillator) {
return {
marketPhase: 'distribution',
timingSignal: 'hold',
confidence: 0,
recommendedAction: 'Insufficient data'
};
}
const { oscillator: value, breadthCondition } = oscillator;
// Determine market phase based on oscillator value
let marketPhase;
let timingSignal;
let confidence;
let recommendedAction;
if (value > 50) {
marketPhase = 'markup';
timingSignal = 'buy';
confidence = Math.min(80, value / 2);
recommendedAction = 'Strong buying pressure - favorable for longs';
}
else if (value > 0) {
marketPhase = 'accumulation';
timingSignal = 'buy';
confidence = 60;
recommendedAction = 'Moderate buying pressure - consider entry';
}
else if (value > -50) {
marketPhase = 'distribution';
timingSignal = 'hold';
confidence = 50;
recommendedAction = 'Balanced market - wait for clearer signal';
}
else {
marketPhase = 'markdown';
timingSignal = 'sell';
confidence = Math.min(80, Math.abs(value) / 2);
recommendedAction = 'Strong selling pressure - consider shorts';
}
return {
marketPhase,
timingSignal,
confidence,
recommendedAction
};
}
/**
* Calculate McClellan Oscillator for multiple periods
* @param advances Array of advancing stocks
* @param declines Array of declining stocks
* @returns Array of McClellanOscillatorData objects
*/
export function calculateMultipleMcClellan(advances, declines) {
const oscillator = calculateMcClellanOscillator(advances, declines);
return oscillator ? [oscillator] : [];
}