diff --git a/libs/utils/src/calculations/technical-indicators.ts b/libs/utils/src/calculations/technical-indicators.ts index bdccb06..5a70ceb 100644 --- a/libs/utils/src/calculations/technical-indicators.ts +++ b/libs/utils/src/calculations/technical-indicators.ts @@ -805,13 +805,20 @@ export function elderRay( bullPower: number[]; bearPower: number[]; } { - const emaValues = ema(ohlcv.map(item => item.close), period); + 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++) { - bullPower.push(ohlcv[i].high - emaValues[i - period + 1]); - bearPower.push(ohlcv[i].low - emaValues[i - period + 1]); + // 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 { @@ -837,4 +844,1507 @@ export function forceIndex( 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; +} + +/** + * Ulcer Index + * Measures downside risk + */ +export function ulcerIndex(prices: number[], period: number = 14): number[] { + const ulcerIndexValues: number[] = []; + + for (let i = period; i < prices.length; i++) { + let sumOfSquaredPercentDrawdowns = 0; + for (let j = i - period + 1; j <= i; j++) { + let highestPrice = prices[i - period]; + for (let k = i - period + 1; k <= j; k++) { + if (prices[k] > highestPrice) { + highestPrice = prices[k]; + } + } + const percentDrawdown = (prices[j] - highestPrice) / highestPrice * 100; + sumOfSquaredPercentDrawdowns += Math.pow(percentDrawdown, 2); + } + const ulcerIndexValue = Math.sqrt(sumOfSquaredPercentDrawdowns / period); + ulcerIndexValues.push(ulcerIndexValue); + } + + return ulcerIndexValues; +} + +/** + * 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; } \ No newline at end of file