/** * Technical Indicators Service * Calculates common technical analysis indicators */ import { marketService } from './market.service'; import type { Interval } from './binance.service'; // ============================================================================ // Types // ============================================================================ export interface OHLCV { time: number; open: number; high: number; low: number; close: number; volume: number; } export interface SMAResult { time: number; value: number; } export interface EMAResult { time: number; value: number; } export interface RSIResult { time: number; value: number; } export interface MACDResult { time: number; macd: number; signal: number; histogram: number; } export interface StochasticResult { time: number; k: number; d: number; } export interface BollingerBandsResult { time: number; upper: number; middle: number; lower: number; bandwidth: number; } export interface ATRResult { time: number; value: number; } export interface VWAPResult { time: number; value: number; } export interface IchimokuResult { time: number; tenkanSen: number; kijunSen: number; senkouSpanA: number; senkouSpanB: number; chikouSpan: number; } export interface IndicatorParams { symbol: string; interval: Interval; period?: number; limit?: number; } export interface MACDParams extends IndicatorParams { fastPeriod?: number; slowPeriod?: number; signalPeriod?: number; } export interface BollingerParams extends IndicatorParams { stdDev?: number; } export interface StochasticParams extends IndicatorParams { kPeriod?: number; dPeriod?: number; smoothK?: number; } // ============================================================================ // Indicators Service // ============================================================================ class IndicatorsService { /** * Convert klines to OHLCV format * CandlestickData already has numeric values */ private klinesToOHLCV(klines: unknown[]): OHLCV[] { return (klines as Record[]).map((k) => ({ time: k.time as number, open: k.open as number, high: k.high as number, low: k.low as number, close: k.close as number, volume: k.volume as number, })); } /** * Fetch OHLCV data for calculations */ private async getOHLCV(symbol: string, interval: Interval, limit: number): Promise { const klines = await marketService.getKlines(symbol, interval, { limit }); return this.klinesToOHLCV(klines); } // ========================================================================== // Moving Averages // ========================================================================== /** * Calculate Simple Moving Average (SMA) */ async getSMA(params: IndicatorParams): Promise { const { symbol, interval, period = 20, limit = 100 } = params; const ohlcv = await this.getOHLCV(symbol, interval, limit + period); return this.calculateSMA(ohlcv, period); } private calculateSMA(data: OHLCV[], period: number): SMAResult[] { const result: SMAResult[] = []; for (let i = period - 1; i < data.length; i++) { 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; } /** * Calculate Exponential Moving Average (EMA) */ async getEMA(params: IndicatorParams): Promise { const { symbol, interval, period = 20, limit = 100 } = params; const ohlcv = await this.getOHLCV(symbol, interval, limit + period); return this.calculateEMA(ohlcv, period); } private calculateEMA(data: OHLCV[], period: number): EMAResult[] { const result: EMAResult[] = []; const multiplier = 2 / (period + 1); // Start with SMA for first EMA value let sum = 0; for (let i = 0; i < period; i++) { sum += data[i].close; } let ema = sum / period; result.push({ time: data[period - 1].time, value: ema }); // Calculate EMA for remaining data for (let i = period; i < data.length; i++) { ema = (data[i].close - ema) * multiplier + ema; result.push({ time: data[i].time, value: ema }); } return result; } // ========================================================================== // Oscillators // ========================================================================== /** * Calculate Relative Strength Index (RSI) */ async getRSI(params: IndicatorParams): Promise { const { symbol, interval, period = 14, limit = 100 } = params; const ohlcv = await this.getOHLCV(symbol, interval, limit + period + 1); return this.calculateRSI(ohlcv, period); } private calculateRSI(data: OHLCV[], period: number): RSIResult[] { const result: RSIResult[] = []; const changes: number[] = []; // Calculate price changes for (let i = 1; i < data.length; i++) { changes.push(data[i].close - data[i - 1].close); } // Calculate initial average gains and losses let avgGain = 0; let avgLoss = 0; for (let i = 0; i < period; i++) { if (changes[i] > 0) { avgGain += changes[i]; } else { avgLoss += Math.abs(changes[i]); } } avgGain /= period; avgLoss /= period; // First RSI value let rs = avgLoss === 0 ? 100 : avgGain / avgLoss; let rsi = 100 - 100 / (1 + rs); result.push({ time: data[period].time, value: rsi }); // Calculate RSI for remaining data using Wilder's smoothing for (let i = period; i < changes.length; i++) { const change = changes[i]; const gain = change > 0 ? change : 0; const loss = change < 0 ? Math.abs(change) : 0; avgGain = (avgGain * (period - 1) + gain) / period; avgLoss = (avgLoss * (period - 1) + loss) / period; rs = avgLoss === 0 ? 100 : avgGain / avgLoss; rsi = 100 - 100 / (1 + rs); result.push({ time: data[i + 1].time, value: rsi }); } return result; } /** * Calculate MACD (Moving Average Convergence Divergence) */ async getMACD(params: MACDParams): Promise { const { symbol, interval, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9, limit = 100, } = params; const ohlcv = await this.getOHLCV(symbol, interval, limit + slowPeriod + signalPeriod); return this.calculateMACD(ohlcv, fastPeriod, slowPeriod, signalPeriod); } private calculateMACD( data: OHLCV[], fastPeriod: number, slowPeriod: number, signalPeriod: number ): MACDResult[] { const result: MACDResult[] = []; // Calculate EMAs const fastEMA = this.calculateEMAFromClose(data.map((d) => d.close), fastPeriod); const slowEMA = this.calculateEMAFromClose(data.map((d) => d.close), slowPeriod); // Calculate MACD line const macdLine: number[] = []; const startIndex = slowPeriod - 1; for (let i = startIndex; i < data.length; i++) { const fastIndex = i - (slowPeriod - fastPeriod); if (fastIndex >= 0 && fastIndex < fastEMA.length) { macdLine.push(fastEMA[fastIndex] - slowEMA[i - startIndex]); } } // Calculate signal line (EMA of MACD) const signalLine = this.calculateEMAFromClose(macdLine, signalPeriod); // Build result for (let i = signalPeriod - 1; i < macdLine.length; i++) { const dataIndex = startIndex + i; const macd = macdLine[i]; const signal = signalLine[i - (signalPeriod - 1)]; result.push({ time: data[dataIndex].time, macd, signal, histogram: macd - signal, }); } return result; } private calculateEMAFromClose(closes: number[], period: number): number[] { const result: number[] = []; const multiplier = 2 / (period + 1); let sum = 0; for (let i = 0; i < period; i++) { sum += closes[i]; } let ema = sum / period; result.push(ema); for (let i = period; i < closes.length; i++) { ema = (closes[i] - ema) * multiplier + ema; result.push(ema); } return result; } /** * Calculate Stochastic Oscillator */ async getStochastic(params: StochasticParams): Promise { const { symbol, interval, kPeriod = 14, dPeriod = 3, smoothK = 3, limit = 100 } = params; const ohlcv = await this.getOHLCV(symbol, interval, limit + kPeriod + smoothK + dPeriod); return this.calculateStochastic(ohlcv, kPeriod, dPeriod, smoothK); } private calculateStochastic( data: OHLCV[], kPeriod: number, dPeriod: number, smoothK: number ): StochasticResult[] { const result: StochasticResult[] = []; const rawK: number[] = []; // Calculate raw %K for (let i = kPeriod - 1; i < data.length; i++) { 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; rawK.push(k); } // Smooth %K const smoothedK = this.calculateSMAFromValues(rawK, smoothK); // Calculate %D (SMA of smoothed %K) const percentD = this.calculateSMAFromValues(smoothedK, dPeriod); // Build result const resultStart = kPeriod + smoothK + dPeriod - 3; for (let i = 0; i < percentD.length; i++) { const dataIndex = resultStart + i; result.push({ time: data[dataIndex].time, k: smoothedK[i + dPeriod - 1], d: percentD[i], }); } return result; } private calculateSMAFromValues(values: number[], period: number): number[] { const result: number[] = []; for (let i = period - 1; i < values.length; i++) { let sum = 0; for (let j = 0; j < period; j++) { sum += values[i - j]; } result.push(sum / period); } return result; } // ========================================================================== // Volatility Indicators // ========================================================================== /** * Calculate Bollinger Bands */ async getBollingerBands(params: BollingerParams): Promise { const { symbol, interval, period = 20, stdDev = 2, limit = 100 } = params; const ohlcv = await this.getOHLCV(symbol, interval, limit + period); return this.calculateBollingerBands(ohlcv, period, stdDev); } private calculateBollingerBands(data: OHLCV[], period: number, stdDev: number): BollingerBandsResult[] { const result: BollingerBandsResult[] = []; for (let i = period - 1; i < data.length; i++) { const closes: number[] = []; for (let j = 0; j < period; j++) { closes.push(data[i - j].close); } // Calculate SMA (middle band) const middle = closes.reduce((a, b) => a + b, 0) / period; // Calculate standard deviation const squaredDiffs = closes.map((c) => Math.pow(c - middle, 2)); const variance = squaredDiffs.reduce((a, b) => a + b, 0) / period; const sd = Math.sqrt(variance); // Calculate bands const upper = middle + stdDev * sd; const lower = middle - stdDev * sd; const bandwidth = ((upper - lower) / middle) * 100; result.push({ time: data[i].time, upper, middle, lower, bandwidth, }); } return result; } /** * Calculate Average True Range (ATR) */ async getATR(params: IndicatorParams): Promise { const { symbol, interval, period = 14, limit = 100 } = params; const ohlcv = await this.getOHLCV(symbol, interval, limit + period + 1); return this.calculateATR(ohlcv, period); } private calculateATR(data: OHLCV[], period: number): ATRResult[] { const result: ATRResult[] = []; const trueRanges: number[] = []; // Calculate True Range for (let i = 1; i < data.length; i++) { const high = data[i].high; const low = data[i].low; const prevClose = data[i - 1].close; const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)); trueRanges.push(tr); } // Calculate initial ATR (simple average) let atr = 0; for (let i = 0; i < period; i++) { atr += trueRanges[i]; } atr /= period; result.push({ time: data[period].time, value: atr }); // Calculate ATR using Wilder's smoothing for (let i = period; i < trueRanges.length; i++) { atr = (atr * (period - 1) + trueRanges[i]) / period; result.push({ time: data[i + 1].time, value: atr }); } return result; } // ========================================================================== // Volume Indicators // ========================================================================== /** * Calculate Volume Weighted Average Price (VWAP) */ async getVWAP(params: IndicatorParams): Promise { const { symbol, interval, limit = 100 } = params; const ohlcv = await this.getOHLCV(symbol, interval, limit); return this.calculateVWAP(ohlcv); } private calculateVWAP(data: OHLCV[]): VWAPResult[] { const result: VWAPResult[] = []; let cumulativeTPV = 0; let cumulativeVolume = 0; for (const candle of data) { const typicalPrice = (candle.high + candle.low + candle.close) / 3; cumulativeTPV += typicalPrice * candle.volume; cumulativeVolume += candle.volume; const vwap = cumulativeVolume > 0 ? cumulativeTPV / cumulativeVolume : typicalPrice; result.push({ time: candle.time, value: vwap }); } return result; } // ========================================================================== // All-in-One // ========================================================================== /** * Get multiple indicators for a symbol */ async getAllIndicators( symbol: string, interval: Interval, limit: number = 100 ): Promise<{ sma20: SMAResult[]; sma50: SMAResult[]; ema12: EMAResult[]; ema26: EMAResult[]; rsi: RSIResult[]; macd: MACDResult[]; bollinger: BollingerBandsResult[]; atr: ATRResult[]; }> { // Fetch all data needed (max period is 50 for SMA + limit) const ohlcv = await this.getOHLCV(symbol, interval, limit + 60); const [sma20, sma50, ema12, ema26, rsi, macd, bollinger, atr] = await Promise.all([ Promise.resolve(this.calculateSMA(ohlcv, 20)), Promise.resolve(this.calculateSMA(ohlcv, 50)), Promise.resolve(this.calculateEMA(ohlcv, 12)), Promise.resolve(this.calculateEMA(ohlcv, 26)), Promise.resolve(this.calculateRSI(ohlcv, 14)), Promise.resolve(this.calculateMACD(ohlcv, 12, 26, 9)), Promise.resolve(this.calculateBollingerBands(ohlcv, 20, 2)), Promise.resolve(this.calculateATR(ohlcv, 14)), ]); return { sma20, sma50, ema12, ema26, rsi, macd, bollinger, atr }; } } export const indicatorsService = new IndicatorsService();