/** * Technical Indicators * Comprehensive set of technical analysis indicators */ import { OHLCVData } from './index'; /** * 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++) { ema = values[i] * 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 change = prices[i] - prices[i - 1]; 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++) { macdLine.push(fastEMA[i + startIndex] - slowEMA[i]); } const signalLine = ema(macdLine, signalPeriod); const histogram: number[] = []; const signalStartIndex = signalPeriod - 1; for (let i = 0; i < signalLine.length; i++) { histogram.push(macdLine[i + signalStartIndex] - signalLine[i]); } 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]; upper.push(middleValue + standardDeviations * stdDev); lower.push(middleValue - standardDeviations * stdDev); } return { upper, middle, lower }; } /** * Average True Range (ATR) */ export function atr(ohlcv: OHLCVData[], period: number = 14): number[] { if (period >= ohlcv.length) { return []; } const trueRanges: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const high = ohlcv[i].high; const low = ohlcv[i].low; const prevClose = ohlcv[i - 1].close; const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)); trueRanges.push(tr); } return sma(trueRanges, period); } /** * Stochastic Oscillator */ export function stochastic( ohlcv: OHLCVData[], 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 currentClose = ohlcv[i].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: OHLCVData[], 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 currentClose = ohlcv[i].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: OHLCVData[], 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]; const meanDeviation = slice.reduce((sum, value) => sum + Math.abs(value - mean), 0) / period; if (meanDeviation === 0) { result.push(0); } else { const cciValue = (typicalPrices[i + period - 1] - mean) / (0.015 * meanDeviation); result.push(cciValue); } } 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 momentum = prices[i] - prices[i - period]; 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++) { if (prices[i - period] === 0) { result.push(0); } else { const rocValue = ((prices[i] - prices[i - period]) / prices[i - period]) * 100; result.push(rocValue); } } return result; } /** * Money Flow Index (MFI) */ export function mfi(ohlcv: OHLCVData[], 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) => typicalPrices[i] * d.volume); 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) { if (typicalPrices[j] > typicalPrices[j - 1]) { positiveFlow += moneyFlows[j]; } else if (typicalPrices[j] < typicalPrices[j - 1]) { negativeFlow += moneyFlows[j]; } } } if (negativeFlow === 0) { result.push(100); } else { const mfiRatio = positiveFlow / negativeFlow; const mfiValue = 100 - 100 / (1 + mfiRatio); result.push(mfiValue); } } return result; } /** * On-Balance Volume (OBV) */ export function obv(ohlcv: OHLCVData[]): number[] { if (ohlcv.length === 0) { return []; } const result: number[] = [ohlcv[0].volume]; for (let i = 1; i < ohlcv.length; i++) { const prev = ohlcv[i - 1]; const curr = ohlcv[i]; if (curr.close > prev.close) { result.push(result[result.length - 1] + curr.volume); } else if (curr.close < prev.close) { result.push(result[result.length - 1] - curr.volume); } else { result.push(result[result.length - 1]); } } return result; } /** * Accumulation/Distribution Line */ export function accumulationDistribution(ohlcv: OHLCVData[]): 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; } /** * Chaikin Money Flow (CMF) */ export function chaikinMoneyFlow(ohlcv: OHLCVData[], 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; } /** * Parabolic SAR */ export function parabolicSAR( ohlcv: OHLCVData[], step: number = 0.02, maxStep: number = 0.2 ): number[] { if (ohlcv.length < 2) { return []; } const result: number[] = []; let trend = 1; // 1 for uptrend, -1 for downtrend let acceleration = step; let extremePoint = ohlcv[0].high; let sar = ohlcv[0].low; result.push(sar); for (let i = 1; i < ohlcv.length; i++) { const curr = ohlcv[i]; const prev = ohlcv[i - 1]; // 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 sar = Math.min(sar, prev.low, i > 1 ? ohlcv[i - 2].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 sar = Math.max(sar, prev.high, i > 1 ? ohlcv[i - 2].high : prev.high); } } result.push(sar); } return result; } /** * Aroon Indicator */ export function aroon(ohlcv: OHLCVData[], 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++) { if (slice[j].high > slice[highestIndex].high) { highestIndex = j; } if (slice[j].low < slice[lowestIndex].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: OHLCVData[], 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]; // 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 diPlus = atrValues[i] > 0 ? (smoothedPlusDM[i] / atrValues[i]) * 100 : 0; const diMinus = atrValues[i] > 0 ? (smoothedMinusDM[i] / atrValues[i]) * 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), }; } /** * Volume Weighted Moving Average (VWMA) */ export function vwma(ohlcv: OHLCVData[], 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; } /** * Pivot Points (Standard) */ export function pivotPoints(ohlcv: OHLCVData[]): Array<{ pivot: number; resistance1: number; resistance2: number; resistance3: number; support1: number; support2: number; support3: number; }> { if (ohlcv.length === 0) { return []; } const result: Array<{ pivot: number; resistance1: number; resistance2: number; resistance3: number; support1: number; support2: number; support3: number; }> = []; for (let i = 0; i < ohlcv.length; i++) { const candle = ohlcv[i]; // Calculate pivot point const pivot = (candle.high + candle.low + candle.close) / 3; // Calculate resistance and support levels const resistance1 = 2 * pivot - candle.low; const support1 = 2 * pivot - candle.high; const resistance2 = pivot + (candle.high - candle.low); const support2 = pivot - (candle.high - candle.low); const resistance3 = candle.high + 2 * (pivot - candle.low); const support3 = candle.low - 2 * (candle.high - pivot); result.push({ pivot, resistance1, resistance2, resistance3, support1, support2, support3, }); } return result; } /** * Ichimoku Cloud */ export function ichimokuCloud( ohlcv: OHLCVData[], tenkanSenPeriod: number = 9, kijunSenPeriod: number = 26, senkouSpanBPeriod: number = 52 ): { tenkanSen: number[]; kijunSen: number[]; senkouSpanA: number[]; senkouSpanB: number[]; chikouSpan: number[]; } { const { high, low, close } = { high: ohlcv.map(item => item.high), low: ohlcv.map(item => item.low), close: ohlcv.map(item => item.close), }; const tenkanSen = calculateTenkanSen(high, low, tenkanSenPeriod); const kijunSen = calculateKijunSen(high, low, kijunSenPeriod); const senkouSpanA = calculateSenkouSpanA(tenkanSen, kijunSen); const senkouSpanB = calculateSenkouSpanB(high, low, senkouSpanBPeriod); const chikouSpan = calculateChikouSpan(close, kijunSenPeriod); return { tenkanSen, kijunSen, senkouSpanA, senkouSpanB, chikouSpan, }; function calculateTenkanSen(high: number[], low: number[], period: number): number[] { const tenkanSen: number[] = []; for (let i = period - 1; i < high.length; i++) { const sliceHigh = high.slice(i - period + 1, i + 1); const sliceLow = low.slice(i - period + 1, i + 1); const highestHigh = Math.max(...sliceHigh); const lowestLow = Math.min(...sliceLow); tenkanSen.push((highestHigh + lowestLow) / 2); } return tenkanSen; } function calculateKijunSen(high: number[], low: number[], period: number): number[] { const kijunSen: number[] = []; for (let i = period - 1; i < high.length; i++) { const sliceHigh = high.slice(i - period + 1, i + 1); const sliceLow = low.slice(i - period + 1, i + 1); const highestHigh = Math.max(...sliceHigh); const lowestLow = Math.min(...sliceLow); kijunSen.push((highestHigh + lowestLow) / 2); } return kijunSen; } function calculateSenkouSpanA(tenkanSen: number[], kijunSen: number[]): number[] { const senkouSpanA: number[] = []; for (let i = 0; i < tenkanSen.length; i++) { senkouSpanA.push((tenkanSen[i] + kijunSen[i]) / 2); } return senkouSpanA; } function calculateSenkouSpanB(high: number[], low: number[], period: number): number[] { const senkouSpanB: number[] = []; for (let i = period - 1; i < high.length; i++) { const sliceHigh = high.slice(i - period + 1, i + 1); const sliceLow = low.slice(i - period + 1, i + 1); const highestHigh = Math.max(...sliceHigh); const lowestLow = Math.min(...sliceLow); senkouSpanB.push((highestHigh + lowestLow) / 2); } return senkouSpanB; } function calculateChikouSpan(close: number[], period: number): number[] { const chikouSpan: number[] = []; for (let i = 0; i < close.length - period; i++) { chikouSpan.push(close[i]); } return chikouSpan; } } /** * Keltner Channels */ export function keltnerChannels( ohlcv: OHLCVData[], period: number = 20, multiplier: number = 2 ): { upper: number[]; middle: number[]; lower: number[]; } { const atrValues = atr(ohlcv, period); const middle = sma( ohlcv.map(item => (item.high + item.low + item.close) / 3), period ); const upper: number[] = []; const lower: number[] = []; for (let i = 0; i < middle.length; i++) { upper.push(middle[i] + multiplier * atrValues[i]); lower.push(middle[i] - multiplier * atrValues[i]); } return { upper, middle, lower, }; } /** * Donchian Channels */ export function donchianChannels( ohlcv: OHLCVData[], period: number = 20 ): { upper: number[]; middle: number[]; lower: number[]; } { 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 highestHigh = Math.max(...slice.map(item => item.high)); const lowestLow = Math.min(...slice.map(item => item.low)); upper.push(highestHigh); lower.push(lowestLow); middle.push((highestHigh + lowestLow) / 2); } return { upper, middle, lower, }; } /** * Elder-Ray Index */ export function elderRay( ohlcv: OHLCVData[], period: number = 13 ): { bullPower: number[]; bearPower: number[]; } { const closePrices = ohlcv.map(item => item.close); const emaValues = ema(closePrices, period); const bullPower: number[] = []; const bearPower: number[] = []; // Adjust the indexing to ensure we're matching the correct EMA value with each candle for (let i = period - 1; i < ohlcv.length; i++) { // Using the proper index for the EMA values which are aligned with closePrices // Since ema() returns values starting from the period-th element const emaIndex = i - (period - 1); if (emaIndex >= 0 && emaIndex < emaValues.length) { bullPower.push(ohlcv[i].high - emaValues[emaIndex]); bearPower.push(ohlcv[i].low - emaValues[emaIndex]); } } return { bullPower, bearPower, }; } /** * Force Index */ export function forceIndex(ohlcv: OHLCVData[], period: number = 13): number[] { const forceIndexValues: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const change = ohlcv[i].close - ohlcv[i - 1].close; const volume = ohlcv[i].volume; forceIndexValues.push(change * volume); } const smaValues = sma(forceIndexValues, period); return smaValues; } /** * Moving Average Envelope */ export function movingAverageEnvelope( prices: number[], period: number = 20, percentage: number = 0.05 ): { upper: number[]; lower: number[]; middle: number[]; } { const middle = sma(prices, period); const upper: number[] = middle.map(value => value * (1 + percentage)); const lower: number[] = middle.map(value => value * (1 - percentage)); return { upper, lower, middle, }; } /** * High-Low Index */ export function highLowIndex(ohlcv: OHLCVData[], period: number = 14): number[] { const highLowIndexValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { let newHighs = 0; let newLows = 0; for (let j = i - period; j <= i; j++) { if (ohlcv[j].close === Math.max(...ohlcv.slice(i - period, i + 1).map(item => item.close))) { newHighs++; } if (ohlcv[j].close === Math.min(...ohlcv.slice(i - period, i + 1).map(item => item.close))) { newLows++; } } highLowIndexValues.push(((newHighs - newLows) / (newHighs + newLows)) * 100); } return highLowIndexValues; } /** * Coppock Curve */ export function coppockCurve( prices: number[], longPeriod: number = 14, shortPeriod: number = 11, weightedMovingAveragePeriod: number = 10 ): number[] { const rocLong = roc(prices, longPeriod); const rocShort = roc(prices, shortPeriod); const sumROC: number[] = rocLong.map((value, index) => value + rocShort[index]); return sma(sumROC, weightedMovingAveragePeriod); } /** * Ease of Movement (EMV) */ export function easeOfMovement(ohlcv: OHLCVData[], period: number = 14): number[] { const emv: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const distance = (ohlcv[i].high + ohlcv[i].low) / 2 - (ohlcv[i - 1].high + ohlcv[i - 1].low) / 2; const boxRatio = ohlcv[i].volume / 100000000 / (ohlcv[i].high - ohlcv[i].low); // Scale volume to avoid very small numbers emv.push(distance / boxRatio); } return sma(emv, period); } /** * Mass Index */ export function massIndex( ohlcv: OHLCVData[], period: number = 9, emaPeriod: number = 25 ): number[] { const singleEma: number[] = ema( ohlcv.map(item => item.high - item.low), emaPeriod ); const doubleEma: number[] = ema(singleEma, emaPeriod); const massIndexValues: number[] = []; for (let i = period; i < doubleEma.length; i++) { let sum = 0; for (let j = i - period; j < i; j++) { sum += singleEma[j] / doubleEma[j]; } massIndexValues.push(sum); } return massIndexValues; } /** * Ultimate Oscillator */ export function ultimateOscillator( ohlcv: OHLCVData[], shortPeriod: number = 7, mediumPeriod: number = 14, longPeriod: number = 28 ): number[] { const ultimateOscillatorValues: number[] = []; for (let i = longPeriod; i < ohlcv.length; i++) { let trueRangeSum = 0; let buyingPressureSum = 0; for (let j = i; j > 0 && j >= i - longPeriod; j--) { const trueRange = Math.max( ohlcv[j].high - ohlcv[j].low, Math.abs(ohlcv[j].high - ohlcv[j - 1].close), Math.abs(ohlcv[j].low - ohlcv[j - 1].close) ); const buyingPressure = ohlcv[j].close - Math.min(ohlcv[j].low, ohlcv[j - 1].close); trueRangeSum += trueRange; buyingPressureSum += buyingPressure; } const ultimateOscillatorValue = (100 * ((4 * buyingPressureSum) / trueRangeSum + (2 * buyingPressureSum) / trueRangeSum + buyingPressureSum / trueRangeSum)) / 7; ultimateOscillatorValues.push(ultimateOscillatorValue); } return ultimateOscillatorValues; } /** * Schaff Trend Cycle (STC) */ export function schaffTrendCycle( prices: number[], period: number = 10, fastMAPeriod: number = 23, slowMAPeriod: number = 50 ): number[] { const macdValues = macd(prices, fastMAPeriod, slowMAPeriod); const maxValue = Math.max(...macdValues.macd); const minValue = Math.min(...macdValues.macd); const kValues: number[] = macdValues.macd.map( value => ((value - minValue) / (maxValue - minValue)) * 100 ); const dValues: number[] = sma(kValues, period); return dValues; } /** * Hilbert Transform - Instantaneous Trendline */ export function hilbertTransformInstantaneousTrendline(prices: number[]): number[] { // This is a placeholder. A full Hilbert Transform implementation is complex. // Requires significantly more code and signal processing knowledge. // Returning a simple moving average as a substitute. return sma(prices, 20); } /** * Relative Volatility Index (RVI) */ export function relativeVolatilityIndex(ohlcv: OHLCVData[], period: number = 14): number[] { const rviValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { let highCloseSum = 0; let lowCloseSum = 0; for (let j = i; j > 0 && j >= i - period; j--) { highCloseSum += Math.pow(ohlcv[j].high - ohlcv[j].close, 2); lowCloseSum += Math.pow(ohlcv[j].low - ohlcv[j].close, 2); } const highCloseStdDev = Math.sqrt(highCloseSum / period); const lowCloseStdDev = Math.sqrt(lowCloseSum / period); const rviValue = (100 * highCloseStdDev) / (highCloseStdDev + lowCloseStdDev); rviValues.push(rviValue); } return rviValues; } /** * Chande Momentum Oscillator (CMO) */ export function chandeMomentumOscillator(prices: number[], period: number = 14): number[] { const cmoValues: number[] = []; for (let i = period; i < prices.length; i++) { let sumOfGains = 0; let sumOfLosses = 0; for (let j = i; j > 0 && j >= i - period; j--) { const change = prices[j] - prices[j - 1]; if (change > 0) { sumOfGains += change; } else { sumOfLosses += Math.abs(change); } } const cmoValue = (100 * (sumOfGains - sumOfLosses)) / (sumOfGains + sumOfLosses); cmoValues.push(cmoValue); } return cmoValues; } /** * Detrended Price Oscillator (DPO) */ export function detrendedPriceOscillator(prices: number[], period: number = 20): number[] { const dpoValues: number[] = []; const smaValues = sma(prices, period); for (let i = period; i < prices.length; i++) { const dpoValue = prices[i - Math.floor(period / 2) - 1] - smaValues[i - period]; dpoValues.push(dpoValue); } return dpoValues; } /** * Fractal Chaos Bands */ export function fractalChaosBands( ohlcv: OHLCVData[], period: number = 20 ): { upper: number[]; lower: number[] } { const upper: number[] = []; const lower: number[] = []; for (let i = period; i < ohlcv.length; i++) { const slice = ohlcv.slice(i - period + 1, i + 1); const highestHigh = Math.max(...slice.map(item => item.high)); const lowestLow = Math.min(...slice.map(item => item.low)); upper.push(highestHigh); lower.push(lowestLow); } return { upper, lower, }; } /** * Know Sure Thing (KST) Oscillator */ export function knowSureThing( prices: number[], rocPeriod1: number = 10, rocPeriod2: number = 15, rocPeriod3: number = 20, rocPeriod4: number = 30, smaPeriod1: number = 10, smaPeriod2: number = 10, smaPeriod3: number = 10, smaPeriod4: number = 15 ): number[] { const roc1 = roc(prices, rocPeriod1); const roc2 = roc(prices, rocPeriod2); const roc3 = roc(prices, rocPeriod3); const roc4 = roc(prices, rocPeriod4); const sma1 = sma(roc1, smaPeriod1); const sma2 = sma(roc2, smaPeriod2); const sma3 = sma(roc3, smaPeriod3); const sma4 = sma(roc4, smaPeriod4); const kstValues: number[] = []; for (let i = 0; i < sma1.length; i++) { const kstValue = sma1[i] + sma2[i] + sma3[i] + sma4[i]; kstValues.push(kstValue); } return kstValues; } /** * Percentage Price Oscillator (PPO) */ export function percentagePriceOscillator( prices: number[], fastPeriod: number = 12, slowPeriod: number = 26 ): number[] { const fastEMA = ema(prices, fastPeriod); const slowEMA = ema(prices, slowPeriod); const ppoValues: number[] = []; for (let i = 0; i < fastEMA.length; i++) { const ppoValue = ((fastEMA[i] - slowEMA[i]) / slowEMA[i]) * 100; ppoValues.push(ppoValue); } return ppoValues; } /** * Price Volume Trend (PVT) */ export function priceVolumeTrend(ohlcv: OHLCVData[]): number[] { const pvtValues: number[] = [0]; // Initialize with 0 for (let i = 1; i < ohlcv.length; i++) { const change = (ohlcv[i].close - ohlcv[i - 1].close) / ohlcv[i - 1].close; const pvtValue = pvtValues[i - 1] + change * ohlcv[i].volume; pvtValues.push(pvtValue); } return pvtValues; } /** * Q Stick */ export function qStick(ohlcv: OHLCVData[], period: number = 10): number[] { const qStickValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { let sum = 0; for (let j = i; j > 0 && j >= i - period; j--) { sum += ohlcv[j].close - ohlcv[j].open; } qStickValues.push(sum / period); } return qStickValues; } /** * TRIX (Triple Exponentially Smoothed Average) */ export function trix(prices: number[], period: number = 18): number[] { const ema1 = ema(prices, period); const ema2 = ema(ema1, period); const ema3 = ema(ema2, period); const trixValues: number[] = []; for (let i = 1; i < ema3.length; i++) { const trixValue = ((ema3[i] - ema3[i - 1]) / ema3[i - 1]) * 100; trixValues.push(trixValue); } return trixValues; } /** * Vertical Horizontal Filter (VHF) */ export function verticalHorizontalFilter(ohlcv: OHLCVData[], period: number = 28): number[] { const vhfValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { const slice = ohlcv.slice(i - period + 1, i + 1); const highestHigh = Math.max(...slice.map(item => item.high)); const lowestLow = Math.min(...slice.map(item => item.low)); const closeChanges: number[] = []; for (let j = 1; j < slice.length; j++) { closeChanges.push(Math.abs(slice[j].close - slice[j - 1].close)); } const sumOfCloseChanges = closeChanges.reduce((a, b) => a + b, 0); const vhfValue = (highestHigh - lowestLow) / sumOfCloseChanges; vhfValues.push(vhfValue); } return vhfValues; } /** * Volume Rate of Change (VROC) */ export function volumeRateOfChange(ohlcv: OHLCVData[], period: number = 10): number[] { const vrocValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { if (ohlcv[i - period].volume === 0) { vrocValues.push(0); // Avoid division by zero } else { const vrocValue = ((ohlcv[i].volume - ohlcv[i - period].volume) / ohlcv[i - period].volume) * 100; vrocValues.push(vrocValue); } } return vrocValues; } /** * Average True Range Trailing Stops * Calculates trailing stop levels based on ATR */ export function atrTrailingStops( ohlcv: OHLCVData[], period: number = 14, multiplier: number = 3 ): { longStop: number[]; shortStop: number[]; } { const atrValues = atr(ohlcv, period); const longStop: number[] = []; const shortStop: number[] = []; for (let i = period; i < ohlcv.length; i++) { longStop.push(ohlcv[i].low - multiplier * atrValues[i - period]); shortStop.push(ohlcv[i].high + multiplier * atrValues[i - period]); } return { longStop, shortStop, }; } /** * Elder's Force Index * Measures the strength of a trend by combining price and volume */ export function eldersForceIndex(ohlcv: OHLCVData[], period: number = 13): number[] { const forceIndexValues: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const change = ohlcv[i].close - ohlcv[i - 1].close; const volume = ohlcv[i].volume; forceIndexValues.push(change * volume); } return ema(forceIndexValues, period); } /** * Ultimate Oscillator */ export function trueStrengthIndex( prices: number[], longPeriod: number = 25, shortPeriod: number = 13, signalPeriod: number = 9 ): number[] { const priceChanges: number[] = []; for (let i = 1; i < prices.length; i++) { priceChanges.push(prices[i] - prices[i - 1]); } const smoothedMomentum = ema(priceChanges, shortPeriod); const doubleSmoothedMomentum = ema(smoothedMomentum, longPeriod); const absoluteMomentum = priceChanges.map(Math.abs); const smoothedAbsoluteMomentum = ema(absoluteMomentum, shortPeriod); const doubleSmoothedAbsoluteMomentum = ema(smoothedAbsoluteMomentum, longPeriod); const tsiValues: number[] = []; for (let i = longPeriod; i < prices.length - 1; i++) { tsiValues.push( (doubleSmoothedMomentum[i - longPeriod] / doubleSmoothedAbsoluteMomentum[i - longPeriod]) * 100 ); } return tsiValues; } /** * Money Flow Multiplier * Calculates the Money Flow Multiplier */ export function moneyFlowMultiplier(ohlcv: OHLCVData[]): number[] { return ohlcv.map( candle => (candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low) ); } /** * Positive Volume Index (PVI) */ export function positiveVolumeIndex(ohlcv: OHLCVData[], initialValue: number = 1000): number[] { const pviValues: number[] = [initialValue]; for (let i = 1; i < ohlcv.length; i++) { if (ohlcv[i].volume > ohlcv[i - 1].volume) { const change = (ohlcv[i].close - ohlcv[i - 1].close) / ohlcv[i - 1].close; pviValues.push(pviValues[i - 1] + pviValues[i - 1] * change); } else { pviValues.push(pviValues[i - 1]); } } return pviValues; } /** * Negative Volume Index (NVI) */ export function negativeVolumeIndex(ohlcv: OHLCVData[], initialValue: number = 1000): number[] { const nviValues: number[] = [initialValue]; for (let i = 1; i < ohlcv.length; i++) { if (ohlcv[i].volume < ohlcv[i - 1].volume) { const change = (ohlcv[i].close - ohlcv[i - 1].close) / ohlcv[i - 1].close; nviValues.push(nviValues[i - 1] + nviValues[i - 1] * change); } else { nviValues.push(nviValues[i - 1]); } } return nviValues; } /** * Typical Price * Calculates the typical price for each period */ export function typicalPrice(ohlcv: OHLCVData[]): number[] { return ohlcv.map(candle => (candle.high + candle.low + candle.close) / 3); } /** * Median Price * Calculates the median price for each period */ export function medianPrice(ohlcv: OHLCVData[]): number[] { return ohlcv.map(candle => (candle.high + candle.low) / 2); } /** * On Balance Volume Mean (OBV Mean) * Calculates the mean of the On Balance Volume (OBV) values. */ export function onBalanceVolumeMean(ohlcv: OHLCVData[], period: number = 14): number[] { const obvValues = obv(ohlcv); return sma(obvValues, period); } /** * Kaufman's Adaptive Moving Average (KAMA) */ export function kama( prices: number[], period: number = 10, fastPeriod: number = 2, slowPeriod: number = 30 ): number[] { const kamaValues: number[] = []; if (prices.length <= period) { return kamaValues; } // Calculate the initial KAMA using SMA const firstSMA = prices.slice(0, period).reduce((sum, price) => sum + price, 0) / period; let kama = firstSMA; kamaValues.push(kama); // Constants for the calculation const fastConst = 2 / (fastPeriod + 1); const slowConst = 2 / (slowPeriod + 1); for (let i = period; i < prices.length; i++) { // Calculate direction - the numerator of the efficiency ratio const direction = Math.abs(prices[i] - prices[i - period]); // Calculate volatility - the denominator of the efficiency ratio let volatility = 0; for (let j = i - period + 1; j <= i; j++) { volatility += Math.abs(prices[j] - prices[j - 1]); } // Calculate efficiency ratio (ER) // Handle the case where volatility is zero to avoid division by zero const er = volatility === 0 ? 1 : Math.min(direction / volatility, 1); // Calculate smoothing constant (SC) const sc = Math.pow(er * (fastConst - slowConst) + slowConst, 2); // Calculate KAMA kama = kama + sc * (prices[i] - kama); kamaValues.push(kama); } return kamaValues; } /** * DeMarker */ export function deMarker(ohlcv: OHLCVData[], period: number = 14): number[] { const deMax: number[] = []; const deMin: number[] = []; for (let i = 1; i < ohlcv.length; i++) { deMax.push(ohlcv[i].high > ohlcv[i - 1].high ? ohlcv[i].high - ohlcv[i - 1].high : 0); deMin.push(ohlcv[i].low < ohlcv[i - 1].low ? ohlcv[i - 1].low - ohlcv[i].low : 0); } const sumDeMax = sma(deMax, period); const sumDeMin = sma(deMin, period); const deMarkerValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { deMarkerValues.push(sumDeMax[i - period] / (sumDeMax[i - period] + sumDeMin[i - period])); } return deMarkerValues; } /** * Elder's SafeZone Stops */ export function eldersSafeZoneStops( ohlcv: OHLCVData[], atrPeriod: number = 20, percentageRisk: number = 2 ): { longStop: number[]; shortStop: number[] } { const atrValues = atr(ohlcv, atrPeriod); const longStop: number[] = []; const shortStop: number[] = []; for (let i = atrPeriod; i < ohlcv.length; i++) { longStop.push(ohlcv[i].low - atrValues[i - atrPeriod] * (percentageRisk / 100)); shortStop.push(ohlcv[i].high + atrValues[i - atrPeriod] * (percentageRisk / 100)); } return { longStop, shortStop, }; } /** * Projection Oscillator */ export function projectionOscillator(ohlcv: OHLCVData[], period: number = 14): number[] { const projectionOscillatorValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { let highestHigh = ohlcv[i - period].high; let lowestLow = ohlcv[i - period].low; for (let j = i - period; j < i; j++) { if (ohlcv[j].high > highestHigh) { highestHigh = ohlcv[j].high; } if (ohlcv[j].low < lowestLow) { lowestLow = ohlcv[j].low; } } const projectionOscillatorValue = ((ohlcv[i].close - lowestLow) / (highestHigh - lowestLow)) * 100; projectionOscillatorValues.push(projectionOscillatorValue); } return projectionOscillatorValues; } /** * Twiggs Money Flow */ export function twiggsMoneyFlow(ohlcv: OHLCVData[]): number[] { const twiggsMoneyFlowValues: number[] = []; for (let i = 0; i < ohlcv.length; i++) { const moneyFlowVolume = ohlcv[i].volume * ((ohlcv[i].close - ohlcv[i].low - (ohlcv[i].high - ohlcv[i].close)) / (ohlcv[i].high - ohlcv[i].low)); twiggsMoneyFlowValues.push(moneyFlowVolume); } return twiggsMoneyFlowValues; } /** * Relative Strength * Compares the performance of one asset to another */ export function relativeStrength( prices1: number[], prices2: number[], period: number = 14 ): number[] { const rsValues: number[] = []; const sma1 = sma(prices1, period); const sma2 = sma(prices2, period); for (let i = 0; i < sma1.length; i++) { rsValues.push(sma1[i] / sma2[i]); } return rsValues; } /** * Correlation Coefficient * Measures the statistical relationship between two assets */ export function correlationCoefficient( prices1: number[], prices2: number[], period: number = 14 ): number[] { const correlationValues: number[] = []; for (let i = period; i < prices1.length; i++) { const slice1 = prices1.slice(i - period, i); const slice2 = prices2.slice(i - period, i); const mean1 = slice1.reduce((a, b) => a + b, 0) / period; const mean2 = slice2.reduce((a, b) => a + b, 0) / period; let sumXY = 0; let sumX2 = 0; let sumY2 = 0; for (let j = 0; j < period; j++) { sumXY += (slice1[j] - mean1) * (slice2[j] - mean2); sumX2 += Math.pow(slice1[j] - mean1, 2); sumY2 += Math.pow(slice2[j] - mean2, 2); } const correlation = sumXY / (Math.sqrt(sumX2) * Math.sqrt(sumY2)); correlationValues.push(correlation); } return correlationValues; } /** * Coppock Range * Calculates the range between high and low Coppock values */ export function coppockRange( prices: number[], longPeriod: number = 14, shortPeriod: number = 11, wmaPeriod: number = 10 ): { high: number[]; low: number[] } { const coppockValues = coppockCurve(prices, longPeriod, shortPeriod, wmaPeriod); const highValues: number[] = []; const lowValues: number[] = []; for (let i = 1; i < coppockValues.length; i++) { highValues.push(Math.max(coppockValues[i], coppockValues[i - 1])); lowValues.push(Math.min(coppockValues[i], coppockValues[i - 1])); } return { high: highValues, low: lowValues, }; } /** * Chaikin Oscillator * Calculates the difference between two moving averages of the Accumulation/Distribution Line */ export function chaikinOscillator( ohlcv: OHLCVData[], fastPeriod: number = 3, slowPeriod: number = 10 ): number[] { const adlValues = accumulationDistribution(ohlcv); const fastMA = ema(adlValues, fastPeriod); const slowMA = ema(adlValues, slowPeriod); const chaikinOscillatorValues: number[] = []; for (let i = 0; i < fastMA.length; i++) { chaikinOscillatorValues.push(fastMA[i] - slowMA[i]); } return chaikinOscillatorValues; } /** * Prime Number Oscillator * Uses prime numbers to create an oscillator */ export function primeNumberOscillator(prices: number[], period: number = 14): number[] { const primeNumbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43]; // First 14 prime numbers const pnoValues: number[] = []; for (let i = period; i < prices.length; i++) { let sum = 0; for (let j = 0; j < period; j++) { sum += prices[i - j] * primeNumbers[j]; } pnoValues.push(sum); } return pnoValues; } /** * Fractal Efficiency * Measures the efficiency of price movement based on fractal dimension */ export function fractalEfficiency(ohlcv: OHLCVData[], period: number = 20): number[] { const fractalEfficiencyValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { let netDistance = 0; for (let j = i; j > i - period; j--) { netDistance += Math.sqrt(Math.pow(ohlcv[j].close - ohlcv[j - 1].close, 2)); } const straightLineDistance = Math.sqrt(Math.pow(ohlcv[i].close - ohlcv[i - period].close, 2)); const fractalEfficiencyValue = straightLineDistance / netDistance; fractalEfficiencyValues.push(fractalEfficiencyValue); } return fractalEfficiencyValues; } /** * Market Facilitation Index (MFI) */ export function marketFacilitationIndex(ohlcv: OHLCVData[]): number[] { const mfiValues: number[] = []; for (let i = 0; i < ohlcv.length; i++) { const range = ohlcv[i].high - ohlcv[i].low; const mfiValue = range / ohlcv[i].volume; mfiValues.push(mfiValue); } return mfiValues; } /** * Elder-Disk * Combination of Elder-Ray and Force Index */ export function elderDisk(ohlcv: OHLCVData[], period: number = 13): number[] { const { bullPower, bearPower } = elderRay(ohlcv, period); const forceIndexValues = forceIndex(ohlcv, period); const elderDiskValues: number[] = []; for (let i = 0; i < bullPower.length; i++) { elderDiskValues.push(bullPower[i] + bearPower[i] + forceIndexValues[i]); } return elderDiskValues; } /** * Relative Vigor Index (RVI) */ export function relativeVigorIndex(ohlcv: OHLCVData[], period: number = 10): number[] { const rviValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { let sumNumerator = 0; let sumDenominator = 0; for (let j = i; j > i - period; j--) { sumNumerator += (ohlcv[j].close - ohlcv[j].open) * (ohlcv[j].high - ohlcv[j].low); sumDenominator += (ohlcv[j].high - ohlcv[j].low) * (ohlcv[j].high - ohlcv[j].low); } const rviValue = sumDenominator !== 0 ? sumNumerator / sumDenominator : 0; rviValues.push(rviValue); } return rviValues; } /** * Balance of Power (BOP) */ export function balanceOfPower(ohlcv: OHLCVData[]): number[] { const bopValues: number[] = []; for (let i = 0; i < ohlcv.length; i++) { const range = ohlcv[i].high - ohlcv[i].low; const bopValue = range !== 0 ? (ohlcv[i].close - ohlcv[i].open) / range : 0; bopValues.push(bopValue); } return bopValues; } /** * Stochastic RSI * Combines Stochastic Oscillator and RSI to provide overbought/oversold signals */ export function stochasticRSI( prices: number[], rsiPeriod: number = 14, stochasticPeriod: number = 14, smoothPeriod: number = 3 ): { k: number[]; d: number[] } { const rsiValues = rsi(prices, rsiPeriod); return stochastic( rsiValues.map(rsi => ({ high: rsi, low: rsi, close: rsi, open: rsi, volume: 0 }) as OHLCVData), stochasticPeriod, smoothPeriod ); } /** * StochRSI Fast */ export function stochRSIFast( prices: number[], rsiPeriod: number = 14, stochasticPeriod: number = 14 ): { k: number[]; d: number[] } { const rsiValues = rsi(prices, rsiPeriod); return stochastic( rsiValues.map(rsi => ({ high: rsi, low: rsi, close: rsi, open: rsi, volume: 0 }) as OHLCVData), stochasticPeriod, 1 ); } /** * StochRSI Full */ export function stochRSIFull( prices: number[], rsiPeriod: number = 14, stochasticPeriod: number = 14, kSmoothPeriod: number = 3, dSmoothPeriod: number = 3 ): { k: number[]; d: number[] } { const rsiValues = rsi(prices, rsiPeriod); const { k } = stochastic( rsiValues.map(rsi => ({ high: rsi, low: rsi, close: rsi, open: rsi, volume: 0 }) as OHLCVData), stochasticPeriod, kSmoothPeriod ); const d = sma(k, dSmoothPeriod); return { k, d }; } /** * Normalized Average True Range (NATR) */ export function normalizedAverageTrueRange(ohlcv: OHLCVData[], period: number = 14): number[] { const atrValues = atr(ohlcv, period); const natrValues: number[] = []; for (let i = 0; i < atrValues.length; i++) { natrValues.push((atrValues[i] / ohlcv[i].close) * 100); } return natrValues; } /** * Pretty Good Oscillator (PGO) */ export function prettyGoodOscillator(ohlcv: OHLCVData[], period: number = 14): number[] { const pgoValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { let sumHighLow = 0; let sumCloseOpen = 0; for (let j = i; j > i - period; j--) { sumHighLow += ohlcv[j].high - ohlcv[j].low; sumCloseOpen += ohlcv[j].close - ohlcv[j].open; } const pgoValue = sumHighLow !== 0 ? sumCloseOpen / sumHighLow : 0; pgoValues.push(pgoValue); } return pgoValues; } /** * Intraday Intensity Index (III) */ export function intradayIntensityIndex(ohlcv: OHLCVData[]): number[] { const iiiValues: number[] = []; for (let i = 0; i < ohlcv.length; i++) { const volume = ohlcv[i].volume; const range = ohlcv[i].high - ohlcv[i].low; const iiiValue = range !== 0 ? ((2 * ohlcv[i].close - ohlcv[i].high - ohlcv[i].low) / range) * volume : 0; iiiValues.push(iiiValue); } return iiiValues; } /** * Money Flow Chaikin A/D Oscillator * Uses the Chaikin A/D line to create an oscillator */ export function moneyFlowChaikinOscillator( ohlcv: OHLCVData[], fastPeriod: number = 3, slowPeriod: number = 10 ): number[] { const adlValues = accumulationDistribution(ohlcv); const fastMA = ema(adlValues, fastPeriod); const slowMA = ema(adlValues, slowPeriod); const moneyFlowChaikinOscillatorValues: number[] = []; for (let i = 0; i < fastMA.length; i++) { moneyFlowChaikinOscillatorValues.push(fastMA[i] - slowMA[i]); } return moneyFlowChaikinOscillatorValues; } /** * Elder's Thermometer * Uses high and low prices to gauge market temperature */ export function eldersThermometer(ohlcv: OHLCVData[], period: number = 20): number[] { const eldersThermometerValues: number[] = []; for (let i = period; i < ohlcv.length; i++) { let sumOfHighs = 0; let sumOfLows = 0; for (let j = i; j > i - period; j--) { sumOfHighs += ohlcv[j].high; sumOfLows += ohlcv[j].low; } const averageHigh = sumOfHighs / period; const averageLow = sumOfLows / period; const thermometerValue = averageHigh - averageLow; eldersThermometerValues.push(thermometerValue); } return eldersThermometerValues; } /** * High-Low Range * Calculates the range between high and low prices */ export function highLowRange(ohlcv: OHLCVData[]): number[] { return ohlcv.map(candle => candle.high - candle.low); } /** * Typical Price Range * Calculates the range of typical prices */ export function typicalPriceRange(ohlcv: OHLCVData[]): number[] { const typicalPrices = typicalPrice(ohlcv); const typicalPriceRangeValues: number[] = []; for (let i = 1; i < typicalPrices.length; i++) { typicalPriceRangeValues.push(typicalPrices[i] - typicalPrices[i - 1]); } return typicalPriceRangeValues; } /** * Median Price Range * Calculates the range of median prices */ export function medianPriceRange(ohlcv: OHLCVData[]): number[] { const medianPrices = medianPrice(ohlcv); const medianPriceRangeValues: number[] = []; for (let i = 1; i < medianPrices.length; i++) { medianPriceRangeValues.push(medianPrices[i] - medianPrices[i - 1]); } return medianPriceRangeValues; } /** * Center of Gravity */ export function centerOfGravity(prices: number[], period: number = 10): number[] { const cogValues: number[] = []; for (let i = period; i < prices.length; i++) { let weightedSum = 0; let sumOfWeights = 0; for (let j = 1; j <= period; j++) { weightedSum += j * prices[i - period + j]; sumOfWeights += j; } const cogValue = weightedSum / sumOfWeights; cogValues.push(cogValue); } return cogValues; } /** * Linear Regression Indicator */ export function linearRegressionIndicator(prices: number[], period: number = 14): number[] { const lriValues: number[] = []; if (prices.length < period) { return lriValues; } for (let i = period; i < prices.length; i++) { const slice = prices.slice(i - period, i); // Calculate means for normalization (increases numerical stability) const meanX = (period + 1) / 2; // Mean of 1,2,3,...,period let meanY = 0; for (let j = 0; j < period; j++) { meanY += slice[j]; } meanY /= period; // Calculate covariance and variance with normalized data let covariance = 0; let variance = 0; for (let j = 0; j < period; j++) { const xDiff = j + 1 - meanX; const yDiff = slice[j] - meanY; covariance += xDiff * yDiff; variance += xDiff * xDiff; } // Avoid division by zero const slope = variance !== 0 ? covariance / variance : 0; const intercept = meanY - slope * meanX; // Calculate the predicted value at the end of the period const lriValue = slope * period + intercept; lriValues.push(lriValue); } return lriValues; } /** * Standard Deviation * Calculates the standard deviation of a set of values */ export function standardDeviation(prices: number[], period: number = 20): number[] { const stdDevValues: number[] = []; const smaValues = sma(prices, period); for (let i = period - 1; i < prices.length; i++) { const slice = prices.slice(i - period + 1, i + 1); const mean = smaValues[i - period + 1]; let sumOfSquaredDifferences = 0; for (const price of slice) { sumOfSquaredDifferences += Math.pow(price - mean, 2); } const variance = sumOfSquaredDifferences / period; const stdDevValue = Math.sqrt(variance); stdDevValues.push(stdDevValue); } return stdDevValues; } /** * Chaikin A/D Range * Calculates the range of the Chaikin A/D line */ export function chaikinADRange(ohlcv: OHLCVData[]): number[] { const adValues = accumulationDistribution(ohlcv); const adRangeValues: number[] = []; for (let i = 1; i < adValues.length; i++) { adRangeValues.push(adValues[i] - adValues[i - 1]); } return adRangeValues; } /** * Volume Oscillator * Compares two moving averages of volume */ export function volumeOscillator( ohlcv: OHLCVData[], fastPeriod: number = 5, slowPeriod: number = 10 ): number[] { const volumes = ohlcv.map(candle => candle.volume); const fastMA = sma(volumes, fastPeriod); const slowMA = sma(volumes, slowPeriod); const volumeOscillatorValues: number[] = []; for (let i = 0; i < fastMA.length; i++) { volumeOscillatorValues.push(((fastMA[i] - slowMA[i]) / slowMA[i]) * 100); } return volumeOscillatorValues; } /** * Money Flow Index Range * Calculates the range of the Money Flow Index */ export function moneyFlowIndexRange(ohlcv: OHLCVData[], period: number = 14): number[] { const mfiValues = mfi(ohlcv, period); const mfiRangeValues: number[] = []; for (let i = 1; i < mfiValues.length; i++) { mfiRangeValues.push(mfiValues[i] - mfiValues[i - 1]); } return mfiRangeValues; } /** * On Balance Volume Oscillator * Calculates the oscillator of the On Balance Volume */ export function onBalanceVolumeOscillator( ohlcv: OHLCVData[], fastPeriod: number = 5, slowPeriod: number = 10 ): number[] { const obvValues = obv(ohlcv); const fastMA = sma(obvValues, fastPeriod); const slowMA = sma(obvValues, slowPeriod); const obvOscillatorValues: number[] = []; for (let i = 0; i < fastMA.length; i++) { obvOscillatorValues.push(((fastMA[i] - slowMA[i]) / slowMA[i]) * 100); } return obvOscillatorValues; } /** * Klinger Oscillator */ export function klingerOscillator( ohlcv: OHLCVData[], fastPeriod: number = 34, slowPeriod: number = 55 ): number[] { if (ohlcv.length < 2) { return []; } // Calculate volume force const volumeForce: number[] = []; for (let i = 1; i < ohlcv.length; i++) { const current = ohlcv[i]; const previous = ohlcv[i - 1]; // Calculate typical prices const typicalPriceCurrent = (current.high + current.low + current.close) / 3; const typicalPricePrevious = (previous.high + previous.low + previous.close) / 3; // Determine trend const trend = typicalPriceCurrent > typicalPricePrevious ? 1 : -1; // Calculate volume force const force = trend * ohlcv[i].volume * Math.abs(typicalPriceCurrent - typicalPricePrevious); volumeForce.push(force); } // Calculate fast and slow EMAs of the volume force const fastEMA = ema(volumeForce, fastPeriod); const slowEMA = ema(volumeForce, slowPeriod); // Calculate Klinger Oscillator const klingerOscillatorValues: number[] = []; // Both EMAs should have the same starting point const startIndex = Math.abs(fastEMA.length - slowEMA.length); const shorterEMA = fastEMA.length < slowEMA.length ? fastEMA : slowEMA; const longerEMA = fastEMA.length < slowEMA.length ? slowEMA : fastEMA; for (let i = 0; i < shorterEMA.length; i++) { if (fastEMA.length < slowEMA.length) { klingerOscillatorValues.push(shorterEMA[i] - longerEMA[i + startIndex]); } else { klingerOscillatorValues.push(longerEMA[i + startIndex] - shorterEMA[i]); } } return klingerOscillatorValues; } /** * Directional Movement Index (DMI) */ export function directionalMovementIndex( ohlcv: OHLCVData[], period: number = 14 ): { plusDI: number[]; minusDI: number[] } { const { plusDI, minusDI } = adx(ohlcv, period); return { plusDI, minusDI }; } /** * Elder's Cloud */ export function eldersCloud( ohlcv: OHLCVData[], period: number = 20 ): { upper: number[]; lower: number[] } { const emaValues = ema( ohlcv.map(item => item.close), period ); const atrValues = atr(ohlcv, period); const upper: number[] = []; const lower: number[] = []; for (let i = 0; i < emaValues.length; i++) { upper.push(emaValues[i] + atrValues[i]); lower.push(emaValues[i] - atrValues[i]); } return { upper, lower, }; } /** * Ultimate Moving Average (UMA) */ export function ultimateMovingAverage( prices: number[], fastPeriod: number = 7, mediumPeriod: number = 14, slowPeriod: number = 28 ): number[] { const fastMA = sma(prices, fastPeriod); const mediumMA = sma(prices, mediumPeriod); const slowMA = sma(prices, slowPeriod); const umaValues: number[] = []; for (let i = 0; i < fastMA.length; i++) { umaValues.push((fastMA[i] + mediumMA[i] + slowMA[i]) / 3); } return umaValues; } /** * Rainbow Oscillator */ export function rainbowOscillator( prices: number[], numberOfMAs: number = 7, periodIncrement: number = 5 ): number[] { const maValues: number[][] = []; for (let i = 1; i <= numberOfMAs; i++) { maValues.push(sma(prices, i * periodIncrement)); } const rainbowOscillatorValues: number[] = []; for (let i = 0; i < maValues[0].length; i++) { let sum = 0; for (let j = 0; j < numberOfMAs; j++) { sum += maValues[j][i]; } rainbowOscillatorValues.push(sum / numberOfMAs); } return rainbowOscillatorValues; } /** * Guppy Multiple Moving Average (GMMA) */ export function guppyMultipleMovingAverage( prices: number[], shortTermPeriods: number[] = [3, 5, 8, 10, 12, 15], longTermPeriods: number[] = [30, 35, 40, 45, 50, 60] ): { shortTermMAs: number[][]; longTermMAs: number[][] } { const shortTermMAs: number[][] = []; const longTermMAs: number[][] = []; for (const period of shortTermPeriods) { shortTermMAs.push(sma(prices, period)); } for (const period of longTermPeriods) { longTermMAs.push(sma(prices, period)); } return { shortTermMAs, longTermMAs }; } /** * Historical Volatility */ export function historicalVolatility(prices: number[], period: number = 20): number[] { const logReturns: number[] = []; for (let i = 1; i < prices.length; i++) { logReturns.push(Math.log(prices[i] / prices[i - 1])); } const stdDevs = standardDeviation(logReturns, period); const historicalVolatilityValues: number[] = []; for (const stdDev of stdDevs) { historicalVolatilityValues.push(stdDev * Math.sqrt(252)); // Annualize } return historicalVolatilityValues; } /** * Donchian Width */ export function donchianWidth(ohlcv: OHLCVData[], period: number = 20): number[] { const { upper, lower } = donchianChannels(ohlcv, period); const donchianWidthValues: number[] = []; for (let i = 0; i < upper.length; i++) { donchianWidthValues.push(upper[i] - lower[i]); } return donchianWidthValues; } /** * Chandelier Exit */ export function chandelierExit( ohlcv: OHLCVData[], period: number = 22, multiplier: number = 3 ): { long: number[]; short: number[] } { const atrValues = atr(ohlcv, period); const long: number[] = []; const short: number[] = []; for (let i = period; i < ohlcv.length; i++) { const slice = ohlcv.slice(i - period, i); const highestHigh = Math.max(...slice.map(item => item.high)); const lowestLow = Math.min(...slice.map(item => item.low)); long.push(highestHigh - multiplier * atrValues[i - period]); short.push(lowestLow + multiplier * atrValues[i - period]); } return { long, short }; } /** * Projection Bands */ export function projectionBands( ohlcv: OHLCVData[], period: number = 14, stdDevMultiplier: number = 2 ): { upper: number[]; lower: number[] } { const projectionOscillatorValues = projectionOscillator(ohlcv, period); const stdDevValues = standardDeviation(projectionOscillatorValues, period); const upper: number[] = []; const lower: number[] = []; for (let i = 0; i < projectionOscillatorValues.length; i++) { upper.push(projectionOscillatorValues[i] + stdDevMultiplier * stdDevValues[i]); lower.push(projectionOscillatorValues[i] - stdDevMultiplier * stdDevValues[i]); } return { upper, lower }; } /** * Range Action Verification Index (RAVI) */ export function rangeActionVerificationIndex( prices: number[], longPeriod: number = 65, shortPeriod: number = 10 ): number[] { const longMA = sma(prices, longPeriod); const shortMA = sma(prices, shortPeriod); const raviValues: number[] = []; for (let i = 0; i < longMA.length; i++) { raviValues.push(((shortMA[i] - longMA[i]) / longMA[i]) * 100); } return raviValues; } /** * Momentum from Current Price * Calculates momentum using the current price and a previous price. Reduces lag compared to using moving averages. */ export function momentumFromCurrentPrice(prices: number[], period: number = 10): number[] { const result: number[] = []; for (let i = period; i < prices.length; i++) { const momentum = prices[i] - prices[i - period]; result.push(momentum); } return result; } /** * Rate of Change from Current Price (ROC) * Calculates ROC using the current price. */ export function rocFromCurrentPrice(prices: number[], period: number = 10): number[] { const result: number[] = []; for (let i = period; i < prices.length; i++) { if (prices[i - period] === 0) { result.push(0); } else { const rocValue = ((prices[i] - prices[i - period]) / prices[i - period]) * 100; result.push(rocValue); } } return result; }