// Technical Indicators Calculator class TechnicalIndicators { // Simple Moving Average (SMA) static SMA(data, period) { const result = []; for (let i = 0; i < data.length; i++) { if (i < period - 1) { result.push({ time: data[i].time, value: null }); } else { let sum = 0; for (let j = 0; j < period; j++) { sum += data[i - j].close; } result.push({ time: data[i].time, value: sum / period }); } } return result; } // Exponential Moving Average (EMA) static EMA(data, period) { const result = []; const multiplier = 2 / (period + 1); // First EMA uses SMA let sum = 0; for (let i = 0; i < period; i++) { sum += data[i].close; result.push({ time: data[i].time, value: null }); } result[period - 1] = { time: data[period - 1].time, value: sum / period }; // Calculate EMA for remaining data for (let i = period; i < data.length; i++) { const ema = (data[i].close - result[i - 1].value) * multiplier + result[i - 1].value; result.push({ time: data[i].time, value: ema }); } return result; } // Bollinger Bands static BollingerBands(data, period = 20, stdDev = 2) { const sma = this.SMA(data, period); const upper = []; const lower = []; for (let i = 0; i < data.length; i++) { if (i < period - 1) { upper.push({ time: data[i].time, value: null }); lower.push({ time: data[i].time, value: null }); } else { // Calculate standard deviation let sumSquares = 0; for (let j = 0; j < period; j++) { sumSquares += Math.pow(data[i - j].close - sma[i].value, 2); } const std = Math.sqrt(sumSquares / period); upper.push({ time: data[i].time, value: sma[i].value + stdDev * std }); lower.push({ time: data[i].time, value: sma[i].value - stdDev * std }); } } return { middle: sma, upper, lower }; } // MACD (Moving Average Convergence Divergence) static MACD(data, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) { const emaFast = this.EMA(data, fastPeriod); const emaSlow = this.EMA(data, slowPeriod); // MACD Line = Fast EMA - Slow EMA const macdLine = []; for (let i = 0; i < data.length; i++) { if (emaFast[i].value === null || emaSlow[i].value === null) { macdLine.push({ time: data[i].time, value: null, close: null }); } else { const value = emaFast[i].value - emaSlow[i].value; macdLine.push({ time: data[i].time, value, close: value }); } } // Signal Line = EMA of MACD Line const validMacdData = macdLine.filter(d => d.value !== null); const signalEma = this.EMA(validMacdData, signalPeriod); // Map signal back to full timeline const signalLine = []; let signalIndex = 0; for (let i = 0; i < data.length; i++) { if (macdLine[i].value === null || signalIndex >= signalEma.length) { signalLine.push({ time: data[i].time, value: null }); } else if (signalEma[signalIndex] && signalEma[signalIndex].time === data[i].time) { signalLine.push({ time: data[i].time, value: signalEma[signalIndex].value }); signalIndex++; } else { signalLine.push({ time: data[i].time, value: null }); } } // Histogram = MACD Line - Signal Line const histogram = []; for (let i = 0; i < data.length; i++) { if (macdLine[i].value === null || signalLine[i].value === null) { histogram.push({ time: data[i].time, value: 0, color: '#363a45' }); } else { const value = macdLine[i].value - signalLine[i].value; histogram.push({ time: data[i].time, value, color: value >= 0 ? '#26a69a' : '#ef5350' }); } } return { macdLine, signalLine, histogram }; } // RSI (Relative Strength Index) static RSI(data, period = 14) { const result = []; const gains = []; const losses = []; // Calculate price changes for (let i = 0; i < data.length; i++) { if (i === 0) { gains.push(0); losses.push(0); result.push({ time: data[i].time, value: null }); continue; } const change = data[i].close - data[i - 1].close; gains.push(change > 0 ? change : 0); losses.push(change < 0 ? Math.abs(change) : 0); if (i < period) { result.push({ time: data[i].time, value: null }); } else if (i === period) { // First RSI calculation uses simple average let avgGain = 0, avgLoss = 0; for (let j = 1; j <= period; j++) { avgGain += gains[j]; avgLoss += losses[j]; } avgGain /= period; avgLoss /= period; const rs = avgLoss === 0 ? 100 : avgGain / avgLoss; const rsi = 100 - (100 / (1 + rs)); result.push({ time: data[i].time, value: rsi, avgGain, avgLoss }); } else { // Subsequent RSI uses smoothed average const prevAvgGain = result[i - 1].avgGain; const prevAvgLoss = result[i - 1].avgLoss; const avgGain = (prevAvgGain * (period - 1) + gains[i]) / period; const avgLoss = (prevAvgLoss * (period - 1) + losses[i]) / period; const rs = avgLoss === 0 ? 100 : avgGain / avgLoss; const rsi = 100 - (100 / (1 + rs)); result.push({ time: data[i].time, value: rsi, avgGain, avgLoss }); } } return result.map(d => ({ time: d.time, value: d.value })); } // Stochastic Oscillator static Stochastic(data, kPeriod = 14, dPeriod = 3, smooth = 3) { const kValues = []; // Calculate %K for (let i = 0; i < data.length; i++) { if (i < kPeriod - 1) { kValues.push({ time: data[i].time, value: null, close: null }); } else { let highest = -Infinity; let lowest = Infinity; for (let j = 0; j < kPeriod; j++) { highest = Math.max(highest, data[i - j].high); lowest = Math.min(lowest, data[i - j].low); } const k = highest === lowest ? 50 : ((data[i].close - lowest) / (highest - lowest)) * 100; kValues.push({ time: data[i].time, value: k, close: k }); } } // Smooth %K (Fast %K -> Slow %K) const slowK = []; for (let i = 0; i < kValues.length; i++) { if (i < kPeriod - 1 + smooth - 1) { slowK.push({ time: kValues[i].time, value: null, close: null }); } else { let sum = 0; let count = 0; for (let j = 0; j < smooth; j++) { if (kValues[i - j].value !== null) { sum += kValues[i - j].value; count++; } } const value = count > 0 ? sum / count : null; slowK.push({ time: kValues[i].time, value, close: value }); } } // Calculate %D (SMA of Slow %K) const dValues = []; for (let i = 0; i < slowK.length; i++) { if (i < kPeriod - 1 + smooth - 1 + dPeriod - 1) { dValues.push({ time: slowK[i].time, value: null }); } else { let sum = 0; let count = 0; for (let j = 0; j < dPeriod; j++) { if (slowK[i - j].value !== null) { sum += slowK[i - j].value; count++; } } dValues.push({ time: slowK[i].time, value: count > 0 ? sum / count : null }); } } return { k: slowK.map(d => ({ time: d.time, value: d.value })), d: dValues }; } // Detect Golden Cross / Dead Cross static detectCrosses(shortMA, longMA) { const signals = []; for (let i = 1; i < shortMA.length; i++) { if (shortMA[i].value === null || longMA[i].value === null || shortMA[i - 1].value === null || longMA[i - 1].value === null) { continue; } const prevShort = shortMA[i - 1].value; const prevLong = longMA[i - 1].value; const currShort = shortMA[i].value; const currLong = longMA[i].value; // Golden Cross: Short MA crosses above Long MA if (prevShort <= prevLong && currShort > currLong) { signals.push({ time: shortMA[i].time, type: 'golden', description: 'Golden Cross' }); } // Dead Cross: Short MA crosses below Long MA if (prevShort >= prevLong && currShort < currLong) { signals.push({ time: shortMA[i].time, type: 'dead', description: 'Dead Cross' }); } } return signals; } // Detect RSI signals static detectRSISignals(rsi, oversold = 30, overbought = 70) { const signals = []; for (let i = 1; i < rsi.length; i++) { if (rsi[i].value === null || rsi[i - 1].value === null) continue; // Oversold -> Recovery (Buy signal) if (rsi[i - 1].value < oversold && rsi[i].value >= oversold) { signals.push({ time: rsi[i].time, type: 'buy', description: 'RSI Oversold Recovery' }); } // Overbought -> Decline (Sell signal) if (rsi[i - 1].value > overbought && rsi[i].value <= overbought) { signals.push({ time: rsi[i].time, type: 'sell', description: 'RSI Overbought Decline' }); } } return signals; } // Detect MACD signals static detectMACDSignals(macd) { const signals = []; const { macdLine, signalLine } = macd; for (let i = 1; i < macdLine.length; i++) { if (macdLine[i].value === null || signalLine[i].value === null || macdLine[i - 1].value === null || signalLine[i - 1].value === null) { continue; } const prevMacd = macdLine[i - 1].value; const prevSignal = signalLine[i - 1].value; const currMacd = macdLine[i].value; const currSignal = signalLine[i].value; // MACD crosses above Signal (Buy) if (prevMacd <= prevSignal && currMacd > currSignal) { signals.push({ time: macdLine[i].time, type: 'buy', description: 'MACD Bullish Cross' }); } // MACD crosses below Signal (Sell) if (prevMacd >= prevSignal && currMacd < currSignal) { signals.push({ time: macdLine[i].time, type: 'sell', description: 'MACD Bearish Cross' }); } } return signals; } // Detect Stochastic signals static detectStochasticSignals(stochastic, oversold = 20, overbought = 80) { const signals = []; const { k, d } = stochastic; for (let i = 1; i < k.length; i++) { if (k[i].value === null || d[i].value === null || k[i - 1].value === null || d[i - 1].value === null) { continue; } // %K crosses above %D in oversold zone (Buy) if (k[i - 1].value <= d[i - 1].value && k[i].value > d[i].value && k[i].value < oversold) { signals.push({ time: k[i].time, type: 'buy', description: 'Stochastic Oversold Cross' }); } // %K crosses below %D in overbought zone (Sell) if (k[i - 1].value >= d[i - 1].value && k[i].value < d[i].value && k[i].value > overbought) { signals.push({ time: k[i].time, type: 'sell', description: 'Stochastic Overbought Cross' }); } } return signals; } } // Export for use in app.js window.TechnicalIndicators = TechnicalIndicators;