// Self-contained MACD indicator // Includes math, metadata, signal calculation, and base class // Signal constants (defined in each indicator file) const SIGNAL_TYPES = { BUY: 'buy', SELL: 'sell', HOLD: 'hold' }; const SIGNAL_COLORS = { buy: '#26a69a', hold: '#787b86', sell: '#ef5350' }; // Base class (inline replacement for BaseIndicator) class BaseIndicator { constructor(config) { this.id = config.id; this.type = config.type; this.name = config.name; this.params = config.params || {}; this.timeframe = config.timeframe || '1m'; this.series = []; this.visible = config.visible !== false; this.cachedResults = null; this.cachedMeta = null; this.lastSignalTimestamp = null; this.lastSignalType = null; } } // EMA calculation inline (needed for MACD) function calculateEMAInline(data, period) { const multiplier = 2 / (period + 1); const ema = []; for (let i = 0; i < data.length; i++) { if (i < period - 1) { ema.push(null); } else if (i === period - 1) { ema.push(data[i]); } else { ema.push((data[i] - ema[i - 1]) * multiplier + ema[i - 1]); } } return ema; } // Signal calculation for MACD function calculateMACDSignal(indicator, lastCandle, prevCandle, values, prevValues) { const macd = values?.macd; const signal = values?.signal; const prevMacd = prevValues?.macd; const prevSignal = prevValues?.signal; if (macd === undefined || macd === null || signal === undefined || signal === null || prevMacd === undefined || prevMacd === null || prevSignal === undefined || prevSignal === null) { return null; } // BUY: MACD crosses UP through Signal line if (prevMacd <= prevSignal && macd > signal) { return { type: SIGNAL_TYPES.BUY, strength: 80, value: macd, reasoning: `MACD crossed UP through Signal line` }; } // SELL: MACD crosses DOWN through Signal line else if (prevMacd >= prevSignal && macd < signal) { return { type: SIGNAL_TYPES.SELL, strength: 80, value: macd, reasoning: `MACD crossed DOWN through Signal line` }; } return null; } // MACD Indicator class export class MACDIndicator extends BaseIndicator { constructor(config) { super(config); this.lastSignalTimestamp = null; this.lastSignalType = null; } calculate(candles) { const fast = this.params.fast || 12; const slow = this.params.slow || 26; const signalPeriod = this.params.signal || 9; const closes = candles.map(c => c.close); // Use inline EMA calculation instead of MA.ema() const fastEMA = calculateEMAInline(closes, fast); const slowEMA = calculateEMAInline(closes, slow); const macdLine = fastEMA.map((f, i) => (f !== null && slowEMA[i] !== null) ? f - slowEMA[i] : null); let sum = 0; let ema = 0; let count = 0; const signalLine = macdLine.map(m => { if (m === null) return null; count++; if (count < signalPeriod) { sum += m; return null; } else if (count === signalPeriod) { sum += m; ema = sum / signalPeriod; return ema; } else { ema = (m - ema) * (2 / (signalPeriod + 1)) + ema; return ema; } }); return macdLine.map((m, i) => ({ macd: m, signal: signalLine[i], histogram: (m !== null && signalLine[i] !== null) ? m - signalLine[i] : null })); } getMetadata() { return { name: 'MACD', description: 'Moving Average Convergence Divergence - trend & momentum', inputs: [ { name: 'fast', label: 'Fast Period', type: 'number', default: 12 }, { name: 'slow', label: 'Slow Period', type: 'number', default: 26 }, { name: 'signal', label: 'Signal Period', type: 'number', default: 9 } ], plots: [ { id: 'macd', color: '#2196f3', title: 'MACD' }, { id: 'signal', color: '#ff5722', title: 'Signal' }, { id: 'histogram', color: '#607d8b', title: 'Histogram', type: 'histogram' } ], displayMode: 'pane' }; } } export { calculateMACDSignal };