/** * Technical Indicators * Comprehensive set of technical analysis indicators */ import type { OHLCV } from '@stock-bot/types'; /** * Simple Moving Average */ export function sma(values: number[], period: number): number[] { if (period > values.length) { return []; } const result: number[] = []; for (let i = period - 1; i < values.length; i++) { const sum = values.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); result.push(sum / period); } return result; } /** * Exponential Moving Average */ export function ema(values: number[], period: number): number[] { if (period > values.length) { return []; } const result: number[] = []; const multiplier = 2 / (period + 1); // Start with SMA for first value let ema = values.slice(0, period).reduce((a, b) => a + b, 0) / period; result.push(ema); for (let i = period; i < values.length; i++) { const value = values[i]; if (value !== undefined) { ema = value * multiplier + ema * (1 - multiplier); result.push(ema); } } return result; } /** * Relative Strength Index (RSI) */ export function rsi(prices: number[], period: number = 14): number[] { if (period >= prices.length) { return []; } const gains: number[] = []; const losses: number[] = []; // Calculate gains and losses for (let i = 1; i < prices.length; i++) { const current = prices[i]; const previous = prices[i - 1]; if (current !== undefined && previous !== undefined) { const change = current - previous; gains.push(change > 0 ? change : 0); losses.push(change < 0 ? Math.abs(change) : 0); } } const result: number[] = []; // Calculate RSI for (let i = period - 1; i < gains.length; i++) { const avgGain = gains.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period; const avgLoss = losses.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period; if (avgLoss === 0) { result.push(100); } else { const rs = avgGain / avgLoss; const rsiValue = 100 - 100 / (1 + rs); result.push(rsiValue); } } return result; } /** * Moving Average Convergence Divergence (MACD) */ export function macd( prices: number[], fastPeriod: number = 12, slowPeriod: number = 26, signalPeriod: number = 9 ): { macd: number[]; signal: number[]; histogram: number[] } { const fastEMA = ema(prices, fastPeriod); const slowEMA = ema(prices, slowPeriod); const macdLine: number[] = []; const startIndex = slowPeriod - fastPeriod; for (let i = 0; i < fastEMA.length - startIndex; i++) { const fastValue = fastEMA[i + startIndex]; const slowValue = slowEMA[i]; if (fastValue !== undefined && slowValue !== undefined) { macdLine.push(fastValue - slowValue); } } const signalLine = ema(macdLine, signalPeriod); const histogram: number[] = []; const signalStartIndex = signalPeriod - 1; for (let i = 0; i < signalLine.length; i++) { const macdValue = macdLine[i + signalStartIndex]; const signalValue = signalLine[i]; if (macdValue !== undefined && signalValue !== undefined) { histogram.push(macdValue - signalValue); } } return { macd: macdLine, signal: signalLine, histogram: histogram, }; } /** * Bollinger Bands */ export function bollingerBands( prices: number[], period: number = 20, standardDeviations: number = 2 ): { upper: number[]; middle: number[]; lower: number[] } { const middle = sma(prices, period); const upper: number[] = []; const lower: number[] = []; for (let i = period - 1; i < prices.length; i++) { const slice = prices.slice(i - period + 1, i + 1); const mean = slice.reduce((a, b) => a + b, 0) / period; const variance = slice.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / period; const stdDev = Math.sqrt(variance); const middleValue = middle[i - period + 1]; if (middleValue !== undefined) { upper.push(middleValue + standardDeviations * stdDev); lower.push(middleValue - standardDeviations * stdDev); } } return { upper, middle, lower }; } /** * Average True Range (ATR) */ export function atr(ohlcv: OHLCV[], period: number = 14): number[] { if (period >= ohlcv.length) { return []; } const trueRanges: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const current = ohlcv[i]; const previous = ohlcv[i - 1]; if (current && previous) { const high = current.high; const low = current.low; const prevClose = previous.close; const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)); trueRanges.push(tr); } } return sma(trueRanges, period); } /** * On-Balance Volume (OBV) */ export function obv(ohlcv: OHLCV[]): number[] { if (ohlcv.length === 0) { return []; } const first = ohlcv[0]; if (!first) { return []; } const result: number[] = [first.volume]; for (let i = 1; i < ohlcv.length; i++) { const prev = ohlcv[i - 1]; const curr = ohlcv[i]; if (!prev || !curr) { continue; } const lastValue = result[result.length - 1]; if (lastValue === undefined) { continue; } if (curr.close > prev.close) { result.push(lastValue + curr.volume); } else if (curr.close < prev.close) { result.push(lastValue - curr.volume); } else { result.push(lastValue); } } return result; } /** * Stochastic Oscillator */ export function stochastic( ohlcv: OHLCV[], kPeriod: number = 14, dPeriod: number = 3 ): { k: number[]; d: number[] } { if (kPeriod >= ohlcv.length) { return { k: [], d: [] }; } const kValues: number[] = []; for (let i = kPeriod - 1; i < ohlcv.length; i++) { const slice = ohlcv.slice(i - kPeriod + 1, i + 1); const highest = Math.max(...slice.map(d => d.high)); const lowest = Math.min(...slice.map(d => d.low)); const current = ohlcv[i]; if (!current) { continue; } const currentClose = current.close; if (highest === lowest) { kValues.push(50); // Avoid division by zero } else { const kValue = ((currentClose - lowest) / (highest - lowest)) * 100; kValues.push(kValue); } } const dValues = sma(kValues, dPeriod); return { k: kValues, d: dValues }; } /** * Williams %R */ export function williamsR(ohlcv: OHLCV[], period: number = 14): number[] { if (period >= ohlcv.length) { return []; } const result: number[] = []; for (let i = period - 1; i < ohlcv.length; i++) { const slice = ohlcv.slice(i - period + 1, i + 1); const highest = Math.max(...slice.map(d => d.high)); const lowest = Math.min(...slice.map(d => d.low)); const current = ohlcv[i]; if (!current) { continue; } const currentClose = current.close; if (highest === lowest) { result.push(-50); // Avoid division by zero } else { const wrValue = ((highest - currentClose) / (highest - lowest)) * -100; result.push(wrValue); } } return result; } /** * Commodity Channel Index (CCI) */ export function cci(ohlcv: OHLCV[], period: number = 20): number[] { if (period >= ohlcv.length) { return []; } const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3); const smaTP = sma(typicalPrices, period); const result: number[] = []; for (let i = 0; i < smaTP.length; i++) { const slice = typicalPrices.slice(i, i + period); const mean = smaTP[i]; if (mean === undefined) { continue; } const meanDeviation = slice.reduce((sum, value) => sum + Math.abs(value - mean), 0) / period; const typicalPrice = typicalPrices[i + period - 1]; if (typicalPrice === undefined) { continue; } if (meanDeviation === 0) { result.push(0); } else { const cciValue = (typicalPrice - mean) / (0.015 * meanDeviation); result.push(cciValue); } } return result; } /** * Money Flow Index (MFI) */ export function mfi(ohlcv: OHLCV[], period: number = 14): number[] { if (period >= ohlcv.length) { return []; } const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3); const moneyFlows = ohlcv.map((d, i) => { const tp = typicalPrices[i]; return tp !== undefined ? tp * d.volume : 0; }); const result: number[] = []; for (let i = period; i < ohlcv.length; i++) { let positiveFlow = 0; let negativeFlow = 0; for (let j = i - period + 1; j <= i; j++) { if (j > 0) { const currentTP = typicalPrices[j]; const prevTP = typicalPrices[j - 1]; const currentMF = moneyFlows[j]; if (currentTP !== undefined && prevTP !== undefined && currentMF !== undefined) { if (currentTP > prevTP) { positiveFlow += currentMF; } else if (currentTP < prevTP) { negativeFlow += currentMF; } } } } if (negativeFlow === 0) { result.push(100); } else { const mfiRatio = positiveFlow / negativeFlow; const mfiValue = 100 - 100 / (1 + mfiRatio); result.push(mfiValue); } } return result; } /** * Volume Weighted Moving Average (VWMA) */ export function vwma(ohlcv: OHLCV[], period: number = 20): number[] { if (period >= ohlcv.length) { return []; } const result: number[] = []; for (let i = period - 1; i < ohlcv.length; i++) { const slice = ohlcv.slice(i - period + 1, i + 1); let totalVolumePrice = 0; let totalVolume = 0; for (const candle of slice) { const typicalPrice = (candle.high + candle.low + candle.close) / 3; totalVolumePrice += typicalPrice * candle.volume; totalVolume += candle.volume; } const vwmaValue = totalVolume > 0 ? totalVolumePrice / totalVolume : 0; result.push(vwmaValue); } return result; } /** * Momentum */ export function momentum(prices: number[], period: number = 10): number[] { if (period >= prices.length) { return []; } const result: number[] = []; for (let i = period; i < prices.length; i++) { const current = prices[i]; const previous = prices[i - period]; if (current !== undefined && previous !== undefined) { const momentum = current - previous; result.push(momentum); } } return result; } /** * Rate of Change (ROC) */ export function roc(prices: number[], period: number = 10): number[] { if (period >= prices.length) { return []; } const result: number[] = []; for (let i = period; i < prices.length; i++) { const current = prices[i]; const previous = prices[i - period]; if (current !== undefined && previous !== undefined) { if (previous === 0) { result.push(0); } else { const rocValue = ((current - previous) / previous) * 100; result.push(rocValue); } } } return result; } /** * Accumulation/Distribution Line */ export function accumulationDistribution(ohlcv: OHLCV[]): number[] { if (ohlcv.length === 0) { return []; } const result: number[] = []; let adLine = 0; for (const candle of ohlcv) { if (candle.high === candle.low) { // Avoid division by zero result.push(adLine); continue; } const moneyFlowMultiplier = (candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low); const moneyFlowVolume = moneyFlowMultiplier * candle.volume; adLine += moneyFlowVolume; result.push(adLine); } return result; } /** * Aroon Indicator */ export function aroon(ohlcv: OHLCV[], period: number = 14): { up: number[]; down: number[] } { if (period >= ohlcv.length) { return { up: [], down: [] }; } const up: number[] = []; const down: number[] = []; for (let i = period - 1; i < ohlcv.length; i++) { const slice = ohlcv.slice(i - period + 1, i + 1); // Find highest high and lowest low positions let highestIndex = 0; let lowestIndex = 0; for (let j = 1; j < slice.length; j++) { const current = slice[j]; const highest = slice[highestIndex]; const lowest = slice[lowestIndex]; if (current && highest && lowest) { if (current.high > highest.high) { highestIndex = j; } if (current.low < lowest.low) { lowestIndex = j; } } } const aroonUp = ((period - 1 - highestIndex) / (period - 1)) * 100; const aroonDown = ((period - 1 - lowestIndex) / (period - 1)) * 100; up.push(aroonUp); down.push(aroonDown); } return { up, down }; } /** * Average Directional Movement Index (ADX) and Directional Movement Indicators (DMI) */ export function adx( ohlcv: OHLCV[], period: number = 14 ): { adx: number[]; plusDI: number[]; minusDI: number[] } { if (period >= ohlcv.length) { return { adx: [], plusDI: [], minusDI: [] }; } const trueRanges: number[] = []; const plusDM: number[] = []; const minusDM: number[] = []; // Calculate True Range and Directional Movements for (let i = 1; i < ohlcv.length; i++) { const current = ohlcv[i]; const previous = ohlcv[i - 1]; if (!current || !previous) {continue;} // True Range const tr = Math.max( current.high - current.low, Math.abs(current.high - previous.close), Math.abs(current.low - previous.close) ); trueRanges.push(tr); // Directional Movements const highDiff = current.high - previous.high; const lowDiff = previous.low - current.low; const plusDMValue = highDiff > lowDiff && highDiff > 0 ? highDiff : 0; const minusDMValue = lowDiff > highDiff && lowDiff > 0 ? lowDiff : 0; plusDM.push(plusDMValue); minusDM.push(minusDMValue); } // Calculate smoothed averages const atrValues = sma(trueRanges, period); const smoothedPlusDM = sma(plusDM, period); const smoothedMinusDM = sma(minusDM, period); const plusDI: number[] = []; const minusDI: number[] = []; const dx: number[] = []; // Calculate DI+ and DI- for (let i = 0; i < atrValues.length; i++) { const atr = atrValues[i]; const plusDMSmoothed = smoothedPlusDM[i]; const minusDMSmoothed = smoothedMinusDM[i]; if (atr === undefined || plusDMSmoothed === undefined || minusDMSmoothed === undefined) {continue;} const diPlus = atr > 0 ? (plusDMSmoothed / atr) * 100 : 0; const diMinus = atr > 0 ? (minusDMSmoothed / atr) * 100 : 0; plusDI.push(diPlus); minusDI.push(diMinus); // Calculate DX const diSum = diPlus + diMinus; const dxValue = diSum > 0 ? (Math.abs(diPlus - diMinus) / diSum) * 100 : 0; dx.push(dxValue); } // Calculate ADX (smoothed DX) const adxValues = sma(dx, period); return { adx: adxValues, plusDI: plusDI.slice(period - 1), minusDI: minusDI.slice(period - 1), }; } /** * Parabolic SAR */ export function parabolicSAR( ohlcv: OHLCV[], step: number = 0.02, maxStep: number = 0.2 ): number[] { if (ohlcv.length < 2) { return []; } const first = ohlcv[0]; if (!first) {return [];} const result: number[] = []; let trend = 1; // 1 for uptrend, -1 for downtrend let acceleration = step; let extremePoint = first.high; let sar = first.low; result.push(sar); for (let i = 1; i < ohlcv.length; i++) { const curr = ohlcv[i]; const prev = ohlcv[i - 1]; if (!curr || !prev) {continue;} // Calculate new SAR sar = sar + acceleration * (extremePoint - sar); if (trend === 1) { // Uptrend if (curr.low <= sar) { // Trend reversal trend = -1; sar = extremePoint; extremePoint = curr.low; acceleration = step; } else { if (curr.high > extremePoint) { extremePoint = curr.high; acceleration = Math.min(acceleration + step, maxStep); } // Ensure SAR doesn't exceed previous lows const prevPrev = i > 1 ? ohlcv[i - 2] : null; sar = Math.min(sar, prev.low, prevPrev ? prevPrev.low : prev.low); } } else { // Downtrend if (curr.high >= sar) { // Trend reversal trend = 1; sar = extremePoint; extremePoint = curr.high; acceleration = step; } else { if (curr.low < extremePoint) { extremePoint = curr.low; acceleration = Math.min(acceleration + step, maxStep); } // Ensure SAR doesn't exceed previous highs const prevPrev = i > 1 ? ohlcv[i - 2] : null; sar = Math.max(sar, prev.high, prevPrev ? prevPrev.high : prev.high); } } result.push(sar); } return result; } /** * Chaikin Money Flow (CMF) */ export function chaikinMoneyFlow(ohlcv: OHLCV[], period: number = 20): number[] { if (period >= ohlcv.length) { return []; } const adValues: number[] = []; for (const candle of ohlcv) { if (candle.high === candle.low) { adValues.push(0); } else { const moneyFlowMultiplier = (candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low); const moneyFlowVolume = moneyFlowMultiplier * candle.volume; adValues.push(moneyFlowVolume); } } const result: number[] = []; for (let i = period - 1; i < ohlcv.length; i++) { const sumAD = adValues.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); const sumVolume = ohlcv.slice(i - period + 1, i + 1).reduce((a, b) => a + b.volume, 0); if (sumVolume === 0) { result.push(0); } else { result.push(sumAD / sumVolume); } } return result; } /** * Pivot Points (Standard) */ export function pivotPoints(ohlcv: OHLCV[]): Array<{ pivot: number; r1: number; r2: number; r3: number; s1: number; s2: number; s3: number; }> { const result: Array<{ pivot: number; r1: number; r2: number; r3: number; s1: number; s2: number; s3: number; }> = []; for (const candle of ohlcv) { const pivot = (candle.high + candle.low + candle.close) / 3; const r1 = 2 * pivot - candle.low; const s1 = 2 * pivot - candle.high; const r2 = pivot + (candle.high - candle.low); const s2 = pivot - (candle.high - candle.low); const r3 = candle.high + 2 * (pivot - candle.low); const s3 = candle.low - 2 * (candle.high - pivot); result.push({ pivot, r1, r2, r3, s1, s2, s3 }); } return result; } /** * Ichimoku Cloud components */ export function ichimoku( ohlcv: OHLCV[], tenkanPeriod: number = 9, kijunPeriod: number = 26, senkouBPeriod: number = 52, _displacement: number = 26 ): { tenkanSen: number[]; kijunSen: number[]; senkouSpanA: number[]; senkouSpanB: number[]; chikouSpan: number[]; } { const tenkanSen: number[] = []; const kijunSen: number[] = []; const senkouSpanA: number[] = []; const senkouSpanB: number[] = []; const chikouSpan: number[] = []; for (let i = 0; i < ohlcv.length; i++) { // Tenkan-sen (Conversion Line) if (i >= tenkanPeriod - 1) { const slice = ohlcv.slice(i - tenkanPeriod + 1, i + 1); const high = Math.max(...slice.map(d => d.high)); const low = Math.min(...slice.map(d => d.low)); tenkanSen.push((high + low) / 2); } // Kijun-sen (Base Line) if (i >= kijunPeriod - 1) { const slice = ohlcv.slice(i - kijunPeriod + 1, i + 1); const high = Math.max(...slice.map(d => d.high)); const low = Math.min(...slice.map(d => d.low)); kijunSen.push((high + low) / 2); } // Senkou Span A (Leading Span A) if (tenkanSen.length > 0 && kijunSen.length > 0) { const tenkanValue = tenkanSen[tenkanSen.length - 1]; const kijunValue = kijunSen[kijunSen.length - 1]; if (tenkanValue !== undefined && kijunValue !== undefined) { senkouSpanA.push((tenkanValue + kijunValue) / 2); } } // Senkou Span B (Leading Span B) if (i >= senkouBPeriod - 1) { const slice = ohlcv.slice(i - senkouBPeriod + 1, i + 1); const high = Math.max(...slice.map(d => d.high)); const low = Math.min(...slice.map(d => d.low)); senkouSpanB.push((high + low) / 2); } // Chikou Span (Lagging Span) chikouSpan.push(ohlcv[i]!.close); } return { tenkanSen, kijunSen, senkouSpanA, senkouSpanB, chikouSpan, }; } /** * Ultimate Oscillator */ export function ultimateOscillator( ohlcv: OHLCV[], period1: number = 7, period2: number = 14, period3: number = 28 ): number[] { if (ohlcv.length < Math.max(period1, period2, period3)) { return []; } const bp: number[] = []; // Buying Pressure const tr: number[] = []; // True Range // Calculate BP and TR for (let i = 0; i < ohlcv.length; i++) { const current = ohlcv[i]!; if (i === 0) { bp.push(current.close - current.low); tr.push(current.high - current.low); } else { const previous = ohlcv[i - 1]!; bp.push(current.close - Math.min(current.low, previous.close)); tr.push(Math.max( current.high - current.low, Math.abs(current.high - previous.close), Math.abs(current.low - previous.close) )); } } const result: number[] = []; for (let i = Math.max(period1, period2, period3) - 1; i < ohlcv.length; i++) { const avg1 = bp.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0) / tr.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0); const avg2 = bp.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0) / tr.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0); const avg3 = bp.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0) / tr.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0); const uo = 100 * ((4 * avg1) + (2 * avg2) + avg3) / (4 + 2 + 1); result.push(uo); } return result; } /** * Ease of Movement (EOM) */ export function easeOfMovement(ohlcv: OHLCV[], period: number = 14): number[] { if (ohlcv.length < 2) { return []; } const emv: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const current = ohlcv[i]!; const previous = ohlcv[i - 1]!; const distance = ((current.high + current.low) / 2) - ((previous.high + previous.low) / 2); const boxHeight = current.high - current.low; const volume = current.volume; if (boxHeight > 0 && volume > 0) { const emvValue = (distance * 1000000) / (volume / boxHeight); emv.push(emvValue); } else { emv.push(0); } } return sma(emv, period); } /** * Triple Exponential Moving Average (TEMA) */ export function tema(values: number[], period: number): number[] { const ema1 = ema(values, period); const ema2 = ema(ema1, period); const ema3 = ema(ema2, period); const result: number[] = []; const minLength = Math.min(ema1.length, ema2.length, ema3.length); for (let i = 0; i < minLength; i++) { const e1 = ema1[i]; const e2 = ema2[i]; const e3 = ema3[i]; if (e1 !== undefined && e2 !== undefined && e3 !== undefined) { const temaValue = 3 * e1 - 3 * e2 + e3; result.push(temaValue); } } return result; } /** * Weighted Moving Average (WMA) */ export function wma(values: number[], period: number): number[] { if (period > values.length) { return []; } const result: number[] = []; const denominator = (period * (period + 1)) / 2; for (let i = period - 1; i < values.length; i++) { let weightedSum = 0; let weight = 1; for (let j = i - period + 1; j <= i; j++) { const value = values[j]; if (value !== undefined) { weightedSum += value * weight; weight++; } } result.push(weightedSum / denominator); } return result; } /** * Keltner Channels */ export function keltnerChannels( ohlcv: OHLCV[], period: number = 20, multiplier: number = 2 ): { upper: number[]; middle: number[]; lower: number[] } { const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3); const middle = ema(typicalPrices, period); const atrValues = atr(ohlcv, period); const upper: number[] = []; const lower: number[] = []; const minLength = Math.min(middle.length, atrValues.length); for (let i = 0; i < minLength; i++) { const middleValue = middle[i]; const atrValue = atrValues[i]; if (middleValue !== undefined && atrValue !== undefined) { upper.push(middleValue + multiplier * atrValue); lower.push(middleValue - multiplier * atrValue); } } return { upper, middle, lower }; } /** * Donchian Channels */ export function donchianChannels( ohlcv: OHLCV[], period: number = 20 ): { upper: number[]; middle: number[]; lower: number[] } { if (period >= ohlcv.length) { return { upper: [], middle: [], lower: [] }; } const upper: number[] = []; const lower: number[] = []; const middle: number[] = []; for (let i = period - 1; i < ohlcv.length; i++) { const slice = ohlcv.slice(i - period + 1, i + 1); const highest = Math.max(...slice.map(d => d.high)); const lowest = Math.min(...slice.map(d => d.low)); const middleValue = (highest + lowest) / 2; upper.push(highest); lower.push(lowest); middle.push(middleValue); } return { upper, middle, lower }; } /** * Klinger Volume Oscillator */ export function klingerVolumeOscillator( ohlcv: OHLCV[], fastPeriod: number = 34, slowPeriod: number = 55, signalPeriod: number = 13 ): { kvo: number[]; signal: number[] } { if (ohlcv.length < 2) { return { kvo: [], signal: [] }; } const volumeForce: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const current = ohlcv[i]!; const previous = ohlcv[i - 1]!; const typicalPrice = (current.high + current.low + current.close) / 3; const prevTypicalPrice = (previous.high + previous.low + previous.close) / 3; const trend = typicalPrice > prevTypicalPrice ? 1 : -1; const vf = current.volume * trend * Math.abs((2 * ((current.close - current.low) - (current.high - current.close))) / (current.high - current.low)) * 100; volumeForce.push(vf); } const fastEMA = ema(volumeForce, fastPeriod); const slowEMA = ema(volumeForce, slowPeriod); const kvo: number[] = []; const minLength = Math.min(fastEMA.length, slowEMA.length); for (let i = 0; i < minLength; i++) { const fast = fastEMA[i]; const slow = slowEMA[i]; if (fast !== undefined && slow !== undefined) { kvo.push(fast - slow); } } const signal = ema(kvo, signalPeriod); return { kvo, signal }; } /** * Price Oscillator (PPO) */ export function priceOscillator( prices: number[], fastPeriod: number = 12, slowPeriod: number = 26, signalPeriod: number = 9 ): { ppo: number[]; signal: number[]; histogram: number[] } { const fastEMA = ema(prices, fastPeriod); const slowEMA = ema(prices, slowPeriod); const ppo: number[] = []; const minLength = Math.min(fastEMA.length, slowEMA.length); for (let i = 0; i < minLength; i++) { const fast = fastEMA[i]; const slow = slowEMA[i]; if (fast !== undefined && slow !== undefined && slow !== 0) { const ppoValue = ((fast - slow) / slow) * 100; ppo.push(ppoValue); } } const signal = ema(ppo, signalPeriod); const histogram: number[] = []; const histMinLength = Math.min(ppo.length, signal.length); for (let i = 0; i < histMinLength; i++) { const ppoValue = ppo[i]; const signalValue = signal[i]; if (ppoValue !== undefined && signalValue !== undefined) { histogram.push(ppoValue - signalValue); } } return { ppo, signal, histogram }; } /** * Chande Momentum Oscillator (CMO) */ export function chandeMomentumOscillator(prices: number[], period: number = 14): number[] { if (period >= prices.length) { return []; } const gains: number[] = []; const losses: number[] = []; // Calculate gains and losses for (let i = 1; i < prices.length; i++) { const current = prices[i]; const previous = prices[i - 1]; if (current !== undefined && previous !== undefined) { const change = current - previous; gains.push(change > 0 ? change : 0); losses.push(change < 0 ? Math.abs(change) : 0); } } const result: number[] = []; for (let i = period - 1; i < gains.length; i++) { const sumGains = gains.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); const sumLosses = losses.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); const cmo = ((sumGains - sumLosses) / (sumGains + sumLosses)) * 100; result.push(isNaN(cmo) ? 0 : cmo); } return result; } /** * Stochastic RSI */ export function stochasticRSI( prices: number[], rsiPeriod: number = 14, stochPeriod: number = 14, smoothK: number = 3, smoothD: number = 3 ): { k: number[]; d: number[] } { const rsiValues = rsi(prices, rsiPeriod); if (rsiValues.length < stochPeriod) { return { k: [], d: [] }; } const stochRSI: number[] = []; for (let i = stochPeriod - 1; i < rsiValues.length; i++) { const slice = rsiValues.slice(i - stochPeriod + 1, i + 1); const highest = Math.max(...slice); const lowest = Math.min(...slice); const currentRSI = rsiValues[i]!; if (highest === lowest) { stochRSI.push(0); } else { const stochValue = ((currentRSI - lowest) / (highest - lowest)) * 100; stochRSI.push(stochValue); } } const k = sma(stochRSI, smoothK); const d = sma(k, smoothD); return { k, d }; } /** * Vortex Indicator */ export function vortexIndicator( ohlcv: OHLCV[], period: number = 14 ): { vi_plus: number[]; vi_minus: number[] } { if (ohlcv.length < period + 1) { return { vi_plus: [], vi_minus: [] }; } const vm_plus: number[] = []; const vm_minus: number[] = []; const tr: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const current = ohlcv[i]!; const previous = ohlcv[i - 1]!; // Vortex Movement vm_plus.push(Math.abs(current.high - previous.low)); vm_minus.push(Math.abs(current.low - previous.high)); // True Range const trValue = Math.max( current.high - current.low, Math.abs(current.high - previous.close), Math.abs(current.low - previous.close) ); tr.push(trValue); } const vi_plus: number[] = []; const vi_minus: number[] = []; for (let i = period - 1; i < vm_plus.length; i++) { const sumVMPlus = vm_plus.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); const sumVMMinus = vm_minus.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); const sumTR = tr.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); if (sumTR > 0) { vi_plus.push(sumVMPlus / sumTR); vi_minus.push(sumVMMinus / sumTR); } else { vi_plus.push(0); vi_minus.push(0); } } return { vi_plus, vi_minus }; } /** * Balance of Power (BOP) */ export function balanceOfPower(ohlcv: OHLCV[]): number[] { const result: number[] = []; for (const candle of ohlcv) { if (candle.high === candle.low) { result.push(0); } else { const bop = (candle.close - candle.open) / (candle.high - candle.low); result.push(bop); } } return result; } /** * Trix (Triple Exponential Moving Average Oscillator) */ export function trix(prices: number[], period: number = 14): number[] { const ema1 = ema(prices, period); const ema2 = ema(ema1, period); const ema3 = ema(ema2, period); const result: number[] = []; for (let i = 1; i < ema3.length; i++) { const current = ema3[i]; const previous = ema3[i - 1]; if (current !== undefined && previous !== undefined && previous !== 0) { const trixValue = ((current - previous) / previous) * 10000; result.push(trixValue); } } return result; } /** * Mass Index */ export function massIndex(ohlcv: OHLCV[], period: number = 25): number[] { if (ohlcv.length < period) { return []; } // Calculate high-low ranges const ranges = ohlcv.map(candle => candle.high - candle.low); // Calculate 9-period EMA of ranges const ema9 = ema(ranges, 9); // Calculate 9-period EMA of the EMA (double smoothing) const emaEma9 = ema(ema9, 9); // Calculate ratio const ratios: number[] = []; const minLength = Math.min(ema9.length, emaEma9.length); for (let i = 0; i < minLength; i++) { const singleEMA = ema9[i]; const doubleEMA = emaEma9[i]; if (singleEMA !== undefined && doubleEMA !== undefined && doubleEMA !== 0) { ratios.push(singleEMA / doubleEMA); } } // Calculate 25-period sum of ratios const result: number[] = []; for (let i = period - 1; i < ratios.length; i++) { const sum = ratios.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); result.push(sum); } return result; } /** * Coppock Curve */ export function coppockCurve( prices: number[], shortROC: number = 11, longROC: number = 14, wma: number = 10 ): number[] { const roc1 = roc(prices, shortROC); const roc2 = roc(prices, longROC); const combined: number[] = []; const minLength = Math.min(roc1.length, roc2.length); for (let i = 0; i < minLength; i++) { const r1 = roc1[i]; const r2 = roc2[i]; if (r1 !== undefined && r2 !== undefined) { combined.push(r1 + r2); } } return sma(combined, wma); // Using SMA instead of WMA for simplicity }