From 0397796cfb0a5ee42467b610972940c1ab2b0e6f Mon Sep 17 00:00:00 2001 From: Boki Date: Thu, 19 Jun 2025 10:35:38 -0400 Subject: [PATCH] reworked calcs lib --- ...is.ts => correlation-analysis.ts.disabled} | 38 +- libs/utils/src/calculations/index.ts | 116 +- ...stics.ts => market-statistics.ts.disabled} | 0 ...pricing.ts => options-pricing.ts.disabled} | 0 ...ics.ts => performance-metrics.ts.disabled} | 0 ...ics.ts => portfolio-analytics.ts.disabled} | 0 ...-sizing.ts => position-sizing.ts.disabled} | 0 ...sk-metrics.ts => risk-metrics.ts.disabled} | 0 .../src/calculations/technical-indicators.ts | 2345 +-------------- .../technical-indicators.ts.disabled | 2508 +++++++++++++++++ ...odels.ts => volatility-models.ts.disabled} | 0 11 files changed, 2648 insertions(+), 2359 deletions(-) rename libs/utils/src/calculations/{correlation-analysis.ts => correlation-analysis.ts.disabled} (97%) rename libs/utils/src/calculations/{market-statistics.ts => market-statistics.ts.disabled} (100%) rename libs/utils/src/calculations/{options-pricing.ts => options-pricing.ts.disabled} (100%) rename libs/utils/src/calculations/{performance-metrics.ts => performance-metrics.ts.disabled} (100%) rename libs/utils/src/calculations/{portfolio-analytics.ts => portfolio-analytics.ts.disabled} (100%) rename libs/utils/src/calculations/{position-sizing.ts => position-sizing.ts.disabled} (100%) rename libs/utils/src/calculations/{risk-metrics.ts => risk-metrics.ts.disabled} (100%) create mode 100644 libs/utils/src/calculations/technical-indicators.ts.disabled rename libs/utils/src/calculations/{volatility-models.ts => volatility-models.ts.disabled} (100%) diff --git a/libs/utils/src/calculations/correlation-analysis.ts b/libs/utils/src/calculations/correlation-analysis.ts.disabled similarity index 97% rename from libs/utils/src/calculations/correlation-analysis.ts rename to libs/utils/src/calculations/correlation-analysis.ts.disabled index 8c2f457..5d4eadc 100644 --- a/libs/utils/src/calculations/correlation-analysis.ts +++ b/libs/utils/src/calculations/correlation-analysis.ts.disabled @@ -184,20 +184,24 @@ export function correlationMatrix( for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i === j) { - matrix[i][j] = 1; + matrix[i]![j] = 1; } else { - let corrResult: CorrelationResult; - switch (method) { - case 'spearman': - corrResult = spearmanCorrelation(data[i], data[j]); - break; - case 'kendall': - corrResult = kendallTau(data[i], data[j]); - break; - default: - corrResult = pearsonCorrelation(data[i], data[j]); + const dataI = data[i]; + const dataJ = data[j]; + if (dataI && dataJ) { + let corrResult: CorrelationResult; + switch (method) { + case 'spearman': + corrResult = spearmanCorrelation(dataI, dataJ); + break; + case 'kendall': + corrResult = kendallTau(dataI, dataJ); + break; + default: + corrResult = pearsonCorrelation(dataI, dataJ); + } + matrix[i]![j] = corrResult.correlation; } - matrix[i][j] = corrResult.correlation; } } } @@ -650,8 +654,14 @@ export function distanceCorrelation(x: number[], y: number[]): CorrelationResult for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { - a[i][j] = Math.abs(x[i] - x[j]); - b[i][j] = Math.abs(y[i] - y[j]); + const xi = x[i]; + const xj = x[j]; + const yi = y[i]; + const yj = y[j]; + if (xi !== undefined && xj !== undefined && yi !== undefined && yj !== undefined) { + a[i]![j] = Math.abs(xi - xj); + b[i]![j] = Math.abs(yi - yj); + } } } diff --git a/libs/utils/src/calculations/index.ts b/libs/utils/src/calculations/index.ts index c88cb5a..90b9094 100644 --- a/libs/utils/src/calculations/index.ts +++ b/libs/utils/src/calculations/index.ts @@ -1,28 +1,8 @@ -// Import standardized types -import type { OHLCV, OHLCVWithMetadata } from '@stock-bot/types'; - -// Import specific functions for convenience functions -import { calculateStrategyMetrics } from './performance-metrics'; -import { calculateRiskMetrics } from './risk-metrics'; -import { - atr, - bollingerBands, - cci, - ema, - macd, - momentum, - roc, - rsi, - sma, - stochastic, - williamsR, -} from './technical-indicators'; - /** - * Comprehensive Financial Calculations Library + * Financial Calculations Library * - * This module provides a complete set of financial calculations for trading and investment analysis. - * Organized into logical categories for easy use and maintenance. + * This module provides a core set of financial calculations for trading and investment analysis. + * Focuses on basic calculations and standardized type exports. */ // Re-export all standardized types from @stock-bot/types @@ -60,65 +40,39 @@ export type { HasTimestamp } from '@stock-bot/types'; -// Export all calculation functions +// Export working calculation functions export * from './basic-calculations'; -export * from './technical-indicators'; -export * from './risk-metrics'; -export * from './portfolio-analytics'; -export * from './options-pricing'; -export * from './position-sizing'; -export * from './performance-metrics'; -export * from './market-statistics'; -export * from './volatility-models'; -export * from './correlation-analysis'; -// Convenience function to calculate all technical indicators at once -export function calculateAllTechnicalIndicators( - ohlcv: OHLCV[], - periods: { sma?: number; ema?: number; rsi?: number; atr?: number } = {} -): TechnicalIndicators { - const { - sma: smaPeriod = 20, - ema: emaPeriod = 20, - rsi: rsiPeriod = 14, - atr: atrPeriod = 14, - } = periods; +// Export working technical indicators (building one by one) +export { sma, ema, rsi, macd, bollingerBands, atr, obv, stochastic } from './technical-indicators'; +// export * from './risk-metrics'; +// export * from './portfolio-analytics'; +// export * from './options-pricing'; +// export * from './position-sizing'; +// export * from './performance-metrics'; +// export * from './market-statistics'; +// export * from './volatility-models'; +// export * from './correlation-analysis'; - const closes = ohlcv.map(d => d.close); +// TODO: Re-enable when performance-metrics and risk-metrics are fixed +// // Convenience function for comprehensive portfolio analysis +// export function analyzePortfolio( +// returns: number[], +// equityCurve: Array<{ value: number; date: Date }>, +// benchmarkReturns?: number[], +// riskFreeRate: number = 0.02 +// ): { +// performance: PortfolioAnalysis; +// risk: RiskMetrics; +// trades?: any; +// drawdown?: any; +// } { +// const performance = calculateStrategyMetrics(equityCurve, benchmarkReturns, riskFreeRate); +// const equityValues = equityCurve.map(point => point.value); +// const risk = calculateRiskMetrics(returns, equityValues, benchmarkReturns, riskFreeRate); - return { - sma: sma(closes, smaPeriod), - ema: ema(closes, emaPeriod), - rsi: rsi(closes, rsiPeriod), - macd: macd(closes), - bollinger: bollingerBands(closes), - atr: atr(ohlcv, atrPeriod), - stochastic: stochastic(ohlcv), - williams_r: williamsR(ohlcv), - cci: cci(ohlcv), - momentum: momentum(closes), - roc: roc(closes), - }; -} - -// Convenience function for comprehensive portfolio analysis -export function analyzePortfolio( - returns: number[], - equityCurve: Array<{ value: number; date: Date }>, - benchmarkReturns?: number[], - riskFreeRate: number = 0.02 -): { - performance: PortfolioAnalysis; - risk: RiskMetrics; - trades?: any; - drawdown?: any; -} { - const performance = calculateStrategyMetrics(equityCurve, benchmarkReturns, riskFreeRate); - const equityValues = equityCurve.map(point => point.value); - const risk = calculateRiskMetrics(returns, equityValues, benchmarkReturns, riskFreeRate); - - return { - performance, - risk, - }; -} +// return { +// performance, +// risk, +// }; +// } diff --git a/libs/utils/src/calculations/market-statistics.ts b/libs/utils/src/calculations/market-statistics.ts.disabled similarity index 100% rename from libs/utils/src/calculations/market-statistics.ts rename to libs/utils/src/calculations/market-statistics.ts.disabled diff --git a/libs/utils/src/calculations/options-pricing.ts b/libs/utils/src/calculations/options-pricing.ts.disabled similarity index 100% rename from libs/utils/src/calculations/options-pricing.ts rename to libs/utils/src/calculations/options-pricing.ts.disabled diff --git a/libs/utils/src/calculations/performance-metrics.ts b/libs/utils/src/calculations/performance-metrics.ts.disabled similarity index 100% rename from libs/utils/src/calculations/performance-metrics.ts rename to libs/utils/src/calculations/performance-metrics.ts.disabled diff --git a/libs/utils/src/calculations/portfolio-analytics.ts b/libs/utils/src/calculations/portfolio-analytics.ts.disabled similarity index 100% rename from libs/utils/src/calculations/portfolio-analytics.ts rename to libs/utils/src/calculations/portfolio-analytics.ts.disabled diff --git a/libs/utils/src/calculations/position-sizing.ts b/libs/utils/src/calculations/position-sizing.ts.disabled similarity index 100% rename from libs/utils/src/calculations/position-sizing.ts rename to libs/utils/src/calculations/position-sizing.ts.disabled diff --git a/libs/utils/src/calculations/risk-metrics.ts b/libs/utils/src/calculations/risk-metrics.ts.disabled similarity index 100% rename from libs/utils/src/calculations/risk-metrics.ts rename to libs/utils/src/calculations/risk-metrics.ts.disabled diff --git a/libs/utils/src/calculations/technical-indicators.ts b/libs/utils/src/calculations/technical-indicators.ts index 23f8e68..92e7b83 100644 --- a/libs/utils/src/calculations/technical-indicators.ts +++ b/libs/utils/src/calculations/technical-indicators.ts @@ -3,7 +3,7 @@ * Comprehensive set of technical analysis indicators */ -import { OHLCVData } from './index'; +import type { OHLCV } from '@stock-bot/types'; /** * Simple Moving Average @@ -39,8 +39,11 @@ export function ema(values: number[], period: number): number[] { result.push(ema); for (let i = period; i < values.length; i++) { - ema = values[i] * multiplier + ema * (1 - multiplier); - result.push(ema); + const value = values[i]; + if (value !== undefined) { + ema = value * multiplier + ema * (1 - multiplier); + result.push(ema); + } } return result; @@ -59,9 +62,13 @@ export function rsi(prices: number[], period: number = 14): 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 current = prices[i]; + const previous = prices[i - 1]; + if (current !== undefined && previous !== undefined) { + const change = current - previous; + gains.push(change > 0 ? change : 0); + losses.push(change < 0 ? Math.abs(change) : 0); + } } const result: number[] = []; @@ -99,7 +106,11 @@ export function macd( const startIndex = slowPeriod - fastPeriod; for (let i = 0; i < fastEMA.length - startIndex; i++) { - macdLine.push(fastEMA[i + startIndex] - slowEMA[i]); + const fastValue = fastEMA[i + startIndex]; + const slowValue = slowEMA[i]; + if (fastValue !== undefined && slowValue !== undefined) { + macdLine.push(fastValue - slowValue); + } } const signalLine = ema(macdLine, signalPeriod); @@ -107,7 +118,11 @@ export function macd( const signalStartIndex = signalPeriod - 1; for (let i = 0; i < signalLine.length; i++) { - histogram.push(macdLine[i + signalStartIndex] - signalLine[i]); + const macdValue = macdLine[i + signalStartIndex]; + const signalValue = signalLine[i]; + if (macdValue !== undefined && signalValue !== undefined) { + histogram.push(macdValue - signalValue); + } } return { @@ -136,8 +151,10 @@ export function bollingerBands( const stdDev = Math.sqrt(variance); const middleValue = middle[i - period + 1]; - upper.push(middleValue + standardDeviations * stdDev); - lower.push(middleValue - standardDeviations * stdDev); + if (middleValue !== undefined) { + upper.push(middleValue + standardDeviations * stdDev); + lower.push(middleValue - standardDeviations * stdDev); + } } return { upper, middle, lower }; @@ -146,7 +163,7 @@ export function bollingerBands( /** * Average True Range (ATR) */ -export function atr(ohlcv: OHLCVData[], period: number = 14): number[] { +export function atr(ohlcv: OHLCV[], period: number = 14): number[] { if (period >= ohlcv.length) { return []; } @@ -154,23 +171,64 @@ export function atr(ohlcv: OHLCVData[], period: number = 14): number[] { 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 current = ohlcv[i]; + const previous = ohlcv[i - 1]; + if (current && previous) { + const high = current.high; + const low = current.low; + const prevClose = previous.close; - const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)); - - trueRanges.push(tr); + const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)); + trueRanges.push(tr); + } } return sma(trueRanges, period); } +/** + * On-Balance Volume (OBV) + */ +export function obv(ohlcv: OHLCV[]): number[] { + if (ohlcv.length === 0) { + return []; + } + + const first = ohlcv[0]; + if (!first) { + return []; + } + const result: number[] = [first.volume]; + + for (let i = 1; i < ohlcv.length; i++) { + const prev = ohlcv[i - 1]; + const curr = ohlcv[i]; + if (!prev || !curr) { + continue; + } + + const lastValue = result[result.length - 1]; + if (lastValue === undefined) { + continue; + } + + if (curr.close > prev.close) { + result.push(lastValue + curr.volume); + } else if (curr.close < prev.close) { + result.push(lastValue - curr.volume); + } else { + result.push(lastValue); + } + } + + return result; +} + /** * Stochastic Oscillator */ export function stochastic( - ohlcv: OHLCVData[], + ohlcv: OHLCV[], kPeriod: number = 14, dPeriod: number = 3 ): { k: number[]; d: number[] } { @@ -184,7 +242,11 @@ export function stochastic( 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; + const current = ohlcv[i]; + if (!current) { + continue; + } + const currentClose = current.close; if (highest === lowest) { kValues.push(50); // Avoid division by zero @@ -198,2248 +260,3 @@ export function stochastic( 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; -} diff --git a/libs/utils/src/calculations/technical-indicators.ts.disabled b/libs/utils/src/calculations/technical-indicators.ts.disabled new file mode 100644 index 0000000..1811e41 --- /dev/null +++ b/libs/utils/src/calculations/technical-indicators.ts.disabled @@ -0,0 +1,2508 @@ +/** + * Technical Indicators + * Comprehensive set of technical analysis indicators + */ + +import type { OHLCV, HasOHLC } from '@stock-bot/types'; + +/** + * Simple Moving Average + */ +export function sma(values: number[], period: number): number[] { + if (period > values.length) { + return []; + } + + const result: number[] = []; + + for (let i = period - 1; i < values.length; i++) { + const sum = values.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); + result.push(sum / period); + } + + return result; +} + +/** + * Exponential Moving Average + */ +export function ema(values: number[], period: number): number[] { + if (period > values.length) { + return []; + } + + const result: number[] = []; + const multiplier = 2 / (period + 1); + + // Start with SMA for first value + let ema = values.slice(0, period).reduce((a, b) => a + b, 0) / period; + result.push(ema); + + for (let i = period; i < values.length; i++) { + const value = values[i]; + if (value !== undefined) { + ema = value * multiplier + ema * (1 - multiplier); + result.push(ema); + } + } + + return result; +} + +/** + * Relative Strength Index (RSI) + */ +export function rsi(prices: number[], period: number = 14): number[] { + if (period >= prices.length) { + return []; + } + + const gains: number[] = []; + const losses: number[] = []; + + // Calculate gains and losses + for (let i = 1; i < prices.length; i++) { + const current = prices[i]; + const previous = prices[i - 1]; + if (current !== undefined && previous !== undefined) { + const change = current - previous; + gains.push(change > 0 ? change : 0); + losses.push(change < 0 ? Math.abs(change) : 0); + } + } + + const result: number[] = []; + + // Calculate RSI + for (let i = period - 1; i < gains.length; i++) { + const avgGain = gains.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period; + const avgLoss = losses.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period; + + if (avgLoss === 0) { + result.push(100); + } else { + const rs = avgGain / avgLoss; + const rsiValue = 100 - 100 / (1 + rs); + result.push(rsiValue); + } + } + + return result; +} + +/** + * Moving Average Convergence Divergence (MACD) + */ +export function macd( + prices: number[], + fastPeriod: number = 12, + slowPeriod: number = 26, + signalPeriod: number = 9 +): { macd: number[]; signal: number[]; histogram: number[] } { + const fastEMA = ema(prices, fastPeriod); + const slowEMA = ema(prices, slowPeriod); + + const macdLine: number[] = []; + const startIndex = slowPeriod - fastPeriod; + + for (let i = 0; i < fastEMA.length - startIndex; i++) { + const fastValue = fastEMA[i + startIndex]; + const slowValue = slowEMA[i]; + if (fastValue !== undefined && slowValue !== undefined) { + macdLine.push(fastValue - slowValue); + } + } + + const signalLine = ema(macdLine, signalPeriod); + const histogram: number[] = []; + + const signalStartIndex = signalPeriod - 1; + for (let i = 0; i < signalLine.length; i++) { + const macdValue = macdLine[i + signalStartIndex]; + const signalValue = signalLine[i]; + if (macdValue !== undefined && signalValue !== undefined) { + histogram.push(macdValue - signalValue); + } + } + + return { + macd: macdLine, + signal: signalLine, + histogram: histogram, + }; +} + +/** + * Bollinger Bands + */ +export function bollingerBands( + prices: number[], + period: number = 20, + standardDeviations: number = 2 +): { upper: number[]; middle: number[]; lower: number[] } { + const middle = sma(prices, period); + const upper: number[] = []; + const lower: number[] = []; + + for (let i = period - 1; i < prices.length; i++) { + const slice = prices.slice(i - period + 1, i + 1); + const mean = slice.reduce((a, b) => a + b, 0) / period; + const variance = slice.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / period; + const stdDev = Math.sqrt(variance); + + const middleValue = middle[i - period + 1]; + if (middleValue !== undefined) { + upper.push(middleValue + standardDeviations * stdDev); + lower.push(middleValue - standardDeviations * stdDev); + } + } + + return { upper, middle, lower }; +} + +/** + * Average True Range (ATR) + */ +export function atr(ohlcv: OHLCV[], period: number = 14): number[] { + if (period >= ohlcv.length) { + return []; + } + + const trueRanges: number[] = []; + + for (let i = 1; i < ohlcv.length; i++) { + const current = ohlcv[i]; + const previous = ohlcv[i - 1]; + if (current && previous) { + const high = current.high; + const low = current.low; + const prevClose = previous.close; + + const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)); + trueRanges.push(tr); + } + } + + return sma(trueRanges, period); +} + +/** + * Stochastic Oscillator + */ +export function stochastic( + ohlcv: OHLCV[], + kPeriod: number = 14, + dPeriod: number = 3 +): { k: number[]; d: number[] } { + if (kPeriod >= ohlcv.length) { + return { k: [], d: [] }; + } + + const kValues: number[] = []; + + for (let i = kPeriod - 1; i < ohlcv.length; i++) { + const slice = ohlcv.slice(i - kPeriod + 1, i + 1); + const highest = Math.max(...slice.map(d => d.high)); + const lowest = Math.min(...slice.map(d => d.low)); + const current = ohlcv[i]; + if (!current) continue; + const currentClose = current.close; + + if (highest === lowest) { + kValues.push(50); // Avoid division by zero + } else { + const kValue = ((currentClose - lowest) / (highest - lowest)) * 100; + kValues.push(kValue); + } + } + + const dValues = sma(kValues, dPeriod); + + return { k: kValues, d: dValues }; +} + +/** + * Williams %R + */ +export function williamsR(ohlcv: T[], 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 currentBar = ohlcv[i]; + if (!currentBar) continue; + const currentClose = currentBar.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: T[], period: number = 20): number[] { + if (period >= ohlcv.length) { + return []; + } + + const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3); + const smaTP = sma(typicalPrices, period); + const result: number[] = []; + + for (let i = 0; i < smaTP.length; i++) { + const slice = typicalPrices.slice(i, i + period); + const mean = smaTP[i]; + if (mean === undefined) continue; + const meanDeviation = slice.reduce((sum, value) => sum + Math.abs(value - mean), 0) / period; + const typicalPrice = typicalPrices[i + period - 1]; + if (typicalPrice === undefined) continue; + + if (meanDeviation === 0) { + result.push(0); + } else { + const cciValue = (typicalPrice - mean) / (0.015 * meanDeviation); + result.push(cciValue); + } + } + + return result; +} + +/** + * Momentum + */ +export function momentum(prices: number[], period: number = 10): number[] { + if (period >= prices.length) { + return []; + } + + const result: number[] = []; + + for (let i = period; i < prices.length; i++) { + const current = prices[i]; + const previous = prices[i - period]; + if (current !== undefined && previous !== undefined) { + const momentum = current - previous; + result.push(momentum); + } + } + + return result; +} + +/** + * Rate of Change (ROC) + */ +export function roc(prices: number[], period: number = 10): number[] { + if (period >= prices.length) { + return []; + } + + const result: number[] = []; + + for (let i = period; i < prices.length; i++) { + const current = prices[i]; + const previous = prices[i - period]; + if (current !== undefined && previous !== undefined) { + if (previous === 0) { + result.push(0); + } else { + const rocValue = ((current - previous) / previous) * 100; + result.push(rocValue); + } + } + } + + return result; +} + +/** + * Money Flow Index (MFI) + */ +export function mfi(ohlcv: OHLCV[], period: number = 14): number[] { + if (period >= ohlcv.length) { + return []; + } + + const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3); + const moneyFlows = ohlcv.map((d, i) => 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) { + const currentTP = typicalPrices[j]; + const prevTP = typicalPrices[j - 1]; + const currentMF = moneyFlows[j]; + if (currentTP !== undefined && prevTP !== undefined && currentMF !== undefined) { + if (currentTP > prevTP) { + positiveFlow += currentMF; + } else if (currentTP < prevTP) { + negativeFlow += currentMF; + } + } + } + } + + if (negativeFlow === 0) { + result.push(100); + } else { + const mfiRatio = positiveFlow / negativeFlow; + const mfiValue = 100 - 100 / (1 + mfiRatio); + result.push(mfiValue); + } + } + + return result; +} + +/** + * On-Balance Volume (OBV) + */ +export function obv(ohlcv: OHLCV[]): number[] { + if (ohlcv.length === 0) { + return []; + } + + const first = ohlcv[0]; + if (!first) return []; + const result: number[] = [first.volume]; + + for (let i = 1; i < ohlcv.length; i++) { + const prev = ohlcv[i - 1]; + const curr = ohlcv[i]; + if (!prev || !curr) continue; + + const lastValue = result[result.length - 1]; + if (lastValue === undefined) continue; + + if (curr.close > prev.close) { + result.push(lastValue + curr.volume); + } else if (curr.close < prev.close) { + result.push(lastValue - curr.volume); + } else { + result.push(lastValue); + } + } + + return result; +} + +/** + * Accumulation/Distribution Line + */ +export function accumulationDistribution(ohlcv: OHLCV[]): number[] { + if (ohlcv.length === 0) { + return []; + } + + const result: number[] = []; + let adLine = 0; + + for (const candle of ohlcv) { + if (candle.high === candle.low) { + // Avoid division by zero + result.push(adLine); + continue; + } + + const moneyFlowMultiplier = + (candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low); + const moneyFlowVolume = moneyFlowMultiplier * candle.volume; + adLine += moneyFlowVolume; + result.push(adLine); + } + + return result; +} + +/** + * Chaikin Money Flow (CMF) + */ +export function chaikinMoneyFlow(ohlcv: OHLCV[], period: number = 20): number[] { + if (period >= ohlcv.length) { + return []; + } + + const adValues: number[] = []; + + for (const candle of ohlcv) { + if (candle.high === candle.low) { + adValues.push(0); + } else { + const moneyFlowMultiplier = + (candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low); + const moneyFlowVolume = moneyFlowMultiplier * candle.volume; + adValues.push(moneyFlowVolume); + } + } + + const result: number[] = []; + + for (let i = period - 1; i < ohlcv.length; i++) { + const sumAD = adValues.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); + const sumVolume = ohlcv.slice(i - period + 1, i + 1).reduce((a, b) => a + b.volume, 0); + + if (sumVolume === 0) { + result.push(0); + } else { + result.push(sumAD / sumVolume); + } + } + + return result; +} + +/** + * Parabolic SAR + */ +export function parabolicSAR( + ohlcv: OHLCV[], + step: number = 0.02, + maxStep: number = 0.2 +): number[] { + if (ohlcv.length < 2) { + return []; + } + + const first = ohlcv[0]; + if (!first) return []; + + const result: number[] = []; + let trend = 1; // 1 for uptrend, -1 for downtrend + let acceleration = step; + let extremePoint = first.high; + let sar = first.low; + + result.push(sar); + + for (let i = 1; i < ohlcv.length; i++) { + const curr = ohlcv[i]; + const prev = ohlcv[i - 1]; + if (!curr || !prev) continue; + + // Calculate new SAR + sar = sar + acceleration * (extremePoint - sar); + + if (trend === 1) { + // Uptrend + if (curr.low <= sar) { + // Trend reversal + trend = -1; + sar = extremePoint; + extremePoint = curr.low; + acceleration = step; + } else { + if (curr.high > extremePoint) { + extremePoint = curr.high; + acceleration = Math.min(acceleration + step, maxStep); + } + // Ensure SAR doesn't exceed previous lows + const prevPrev = i > 1 ? ohlcv[i - 2] : null; + sar = Math.min(sar, prev.low, prevPrev ? prevPrev.low : prev.low); + } + } else { + // Downtrend + if (curr.high >= sar) { + // Trend reversal + trend = 1; + sar = extremePoint; + extremePoint = curr.high; + acceleration = step; + } else { + if (curr.low < extremePoint) { + extremePoint = curr.low; + acceleration = Math.min(acceleration + step, maxStep); + } + // Ensure SAR doesn't exceed previous highs + const prevPrev = i > 1 ? ohlcv[i - 2] : null; + sar = Math.max(sar, prev.high, prevPrev ? prevPrev.high : prev.high); + } + } + + result.push(sar); + } + + return result; +} + +/** + * Aroon Indicator + */ +export function aroon(ohlcv: OHLCV[], period: number = 14): { up: number[]; down: number[] } { + if (period >= ohlcv.length) { + return { up: [], down: [] }; + } + + const up: number[] = []; + const down: number[] = []; + + for (let i = period - 1; i < ohlcv.length; i++) { + const slice = ohlcv.slice(i - period + 1, i + 1); + + // Find highest high and lowest low positions + let highestIndex = 0; + let lowestIndex = 0; + + for (let j = 1; j < slice.length; j++) { + const current = slice[j]; + const highest = slice[highestIndex]; + const lowest = slice[lowestIndex]; + if (current && highest && lowest) { + if (current.high > highest.high) { + highestIndex = j; + } + if (current.low < lowest.low) { + lowestIndex = j; + } + } + } + + const aroonUp = ((period - 1 - highestIndex) / (period - 1)) * 100; + const aroonDown = ((period - 1 - lowestIndex) / (period - 1)) * 100; + + up.push(aroonUp); + down.push(aroonDown); + } + + return { up, down }; +} + +/** + * Average Directional Movement Index (ADX) and Directional Movement Indicators (DMI) + */ +export function adx( + ohlcv: OHLCV[], + period: number = 14 +): { adx: number[]; plusDI: number[]; minusDI: number[] } { + if (period >= ohlcv.length) { + return { adx: [], plusDI: [], minusDI: [] }; + } + + const trueRanges: number[] = []; + const plusDM: number[] = []; + const minusDM: number[] = []; + + // Calculate True Range and Directional Movements + for (let i = 1; i < ohlcv.length; i++) { + const current = ohlcv[i]; + const previous = ohlcv[i - 1]; + if (!current || !previous) continue; + + // True Range + const tr = Math.max( + current.high - current.low, + Math.abs(current.high - previous.close), + Math.abs(current.low - previous.close) + ); + trueRanges.push(tr); + + // Directional Movements + const highDiff = current.high - previous.high; + const lowDiff = previous.low - current.low; + + const plusDMValue = highDiff > lowDiff && highDiff > 0 ? highDiff : 0; + const minusDMValue = lowDiff > highDiff && lowDiff > 0 ? lowDiff : 0; + + plusDM.push(plusDMValue); + minusDM.push(minusDMValue); + } + + // Calculate smoothed averages + const atrValues = sma(trueRanges, period); + const smoothedPlusDM = sma(plusDM, period); + const smoothedMinusDM = sma(minusDM, period); + + const plusDI: number[] = []; + const minusDI: number[] = []; + const dx: number[] = []; + + // Calculate DI+ and DI- + for (let i = 0; i < atrValues.length; i++) { + const atr = atrValues[i]; + const plusDMSmoothed = smoothedPlusDM[i]; + const minusDMSmoothed = smoothedMinusDM[i]; + if (atr === undefined || plusDMSmoothed === undefined || minusDMSmoothed === undefined) continue; + + const diPlus = atr > 0 ? (plusDMSmoothed / atr) * 100 : 0; + const diMinus = atr > 0 ? (minusDMSmoothed / atr) * 100 : 0; + + plusDI.push(diPlus); + minusDI.push(diMinus); + + // Calculate DX + const diSum = diPlus + diMinus; + const dxValue = diSum > 0 ? (Math.abs(diPlus - diMinus) / diSum) * 100 : 0; + dx.push(dxValue); + } + + // Calculate ADX (smoothed DX) + const adxValues = sma(dx, period); + + return { + adx: adxValues, + plusDI: plusDI.slice(period - 1), + minusDI: minusDI.slice(period - 1), + }; +} + +/** + * Volume Weighted Moving Average (VWMA) + */ +export function vwma(ohlcv: OHLCV[], period: number = 20): number[] { + if (period >= ohlcv.length) { + return []; + } + + const result: number[] = []; + + for (let i = period - 1; i < ohlcv.length; i++) { + const slice = ohlcv.slice(i - period + 1, i + 1); + + let totalVolumePrice = 0; + let totalVolume = 0; + + for (const candle of slice) { + const typicalPrice = (candle.high + candle.low + candle.close) / 3; + totalVolumePrice += typicalPrice * candle.volume; + totalVolume += candle.volume; + } + + const vwmaValue = totalVolume > 0 ? totalVolumePrice / totalVolume : 0; + result.push(vwmaValue); + } + + return result; +} + +/** + * Pivot Points (Standard) + */ +export function pivotPoints(ohlcv: OHLCV[]): 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: OHLCV[], + 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: OHLCV[], + 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: OHLCV[], + 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: OHLCV[], + 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: OHLCV[], 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: OHLCV[], 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: OHLCV[], 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: OHLCV[], + 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: OHLCV[], + 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: OHLCV[], 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: OHLCV[], + 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: OHLCV[]): 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: OHLCV[], 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: OHLCV[], 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: OHLCV[], 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: OHLCV[], + 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: OHLCV[], 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: OHLCV[]): 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: OHLCV[], 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: OHLCV[], 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: OHLCV[]): 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: OHLCV[]): 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: OHLCV[], 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: OHLCV[], 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: OHLCV[], + 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: OHLCV[], 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: OHLCV[]): 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: OHLCV[], + 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: OHLCV[], 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: OHLCV[]): 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: OHLCV[], 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: OHLCV[], 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: OHLCV[]): 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 OHLCV), + 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 OHLCV), + 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 OHLCV), + stochasticPeriod, + kSmoothPeriod + ); + const d = sma(k, dSmoothPeriod); + return { k, d }; +} + +/** + * Normalized Average True Range (NATR) + */ +export function normalizedAverageTrueRange(ohlcv: OHLCV[], 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: OHLCV[], 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: OHLCV[]): 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: OHLCV[], + 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: OHLCV[], 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: OHLCV[]): number[] { + return ohlcv.map(candle => candle.high - candle.low); +} + +/** + * Typical Price Range + * Calculates the range of typical prices + */ +export function typicalPriceRange(ohlcv: OHLCV[]): 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: OHLCV[]): 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: OHLCV[]): 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: OHLCV[], + 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: OHLCV[], 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: OHLCV[], + 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: OHLCV[], + 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: OHLCV[], + period: number = 14 +): { plusDI: number[]; minusDI: number[] } { + const { plusDI, minusDI } = adx(ohlcv, period); + return { plusDI, minusDI }; +} + +/** + * Elder's Cloud + */ +export function eldersCloud( + ohlcv: OHLCV[], + 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: OHLCV[], 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: OHLCV[], + 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: OHLCV[], + 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; +} diff --git a/libs/utils/src/calculations/volatility-models.ts b/libs/utils/src/calculations/volatility-models.ts.disabled similarity index 100% rename from libs/utils/src/calculations/volatility-models.ts rename to libs/utils/src/calculations/volatility-models.ts.disabled