UNPKG

fast-technical-indicators

Version:

High-performance technical indicators with zero dependencies - compatible with technicalindicators package

1,391 lines (1,374 loc) 274 kB
'use strict'; class CandleData { constructor(open, high, low, close, volume) { this.open = open; this.high = high; this.low = low; this.close = close; this.volume = volume; } } function sma(input) { const { period, values } = input; if (period <= 0 || period > values.length) { return []; } const result = []; for (let i = period - 1; i < values.length; i++) { let sum = 0; for (let j = i - period + 1; j <= i; j++) { sum += values[j]; } result.push(sum / period); } return result; } class SMA { constructor(input) { this.values = []; this.sum = 0; this.period = input.period; if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } nextValue(value) { this.values.push(value); this.sum += value; if (this.values.length > this.period) { this.sum -= this.values.shift(); } if (this.values.length === this.period) { return this.sum / this.period; } return undefined; } getResult() { if (this.values.length < this.period) { return []; } return [this.sum / this.period]; } } SMA.calculate = sma; function ema(input) { const { period, values } = input; if (period <= 0 || values.length < period) { return []; } const result = []; const multiplier = 2 / (period + 1); // Use SMA for the first EMA value const firstSMA = sma({ period, values: values.slice(0, period) }); let previousEMA = firstSMA[0]; result.push(previousEMA); for (let i = period; i < values.length; i++) { const currentEMA = (values[i] - previousEMA) * multiplier + previousEMA; result.push(currentEMA); previousEMA = currentEMA; } return result; } class EMA { constructor(input) { this.values = []; this.initialized = false; this.period = input.period; this.multiplier = 2 / (input.period + 1); if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } nextValue(value) { this.values.push(value); if (!this.initialized) { if (this.values.length === this.period) { // Initialize with SMA const sum = this.values.reduce((acc, val) => acc + val, 0); this.previousEMA = sum / this.period; this.initialized = true; return this.previousEMA; } return undefined; } // Calculate EMA const currentEMA = (value - this.previousEMA) * this.multiplier + this.previousEMA; this.previousEMA = currentEMA; return currentEMA; } getResult() { if (!this.initialized || !this.previousEMA) { return []; } return [this.previousEMA]; } } EMA.calculate = ema; function wma(input) { const { period, values } = input; if (period <= 0 || period > values.length) { return []; } const result = []; for (let i = period - 1; i < values.length; i++) { let weightedSum = 0; let weightSum = 0; for (let j = 0; j < period; j++) { const weight = j + 1; weightedSum += values[i - period + 1 + j] * weight; weightSum += weight; } result.push(weightedSum / weightSum); } return result; } class WMA { constructor(input) { this.values = []; this.period = input.period; if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } nextValue(value) { this.values.push(value); if (this.values.length > this.period) { this.values.shift(); } if (this.values.length === this.period) { let weightedSum = 0; let weightSum = 0; for (let i = 0; i < this.period; i++) { const weight = i + 1; weightedSum += this.values[i] * weight; weightSum += weight; } return weightedSum / weightSum; } return undefined; } getResult() { if (this.values.length < this.period) { return []; } let weightedSum = 0; let weightSum = 0; for (let i = 0; i < this.period; i++) { const weight = i + 1; weightedSum += this.values[i] * weight; weightSum += weight; } return [weightedSum / weightSum]; } } WMA.calculate = wma; function wema(input) { const { period, values } = input; if (period <= 0 || values.length < period) { return []; } const result = []; const alpha = 1 / period; // Initialize with SMA of first period values let sum = 0; for (let i = 0; i < period; i++) { sum += values[i]; } let previousWEMA = sum / period; result.push(previousWEMA); // Calculate subsequent WEMA values for (let i = period; i < values.length; i++) { const currentWEMA = alpha * values[i] + (1 - alpha) * previousWEMA; result.push(currentWEMA); previousWEMA = currentWEMA; } return result; } class WEMA { constructor(input) { this.values = []; this.initialized = false; this.period = input.period; this.alpha = 1 / input.period; if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } nextValue(value) { this.values.push(value); if (!this.initialized) { if (this.values.length === this.period) { // Initialize with SMA const sum = this.values.reduce((acc, val) => acc + val, 0); this.previousWEMA = sum / this.period; this.initialized = true; return this.previousWEMA; } return undefined; } // Calculate WEMA using Wilder's smoothing const currentWEMA = this.alpha * value + (1 - this.alpha) * this.previousWEMA; this.previousWEMA = currentWEMA; return currentWEMA; } getResult() { if (!this.initialized || !this.previousWEMA) { return []; } return [this.previousWEMA]; } } WEMA.calculate = wema; function macd(input) { const { values = [], fastPeriod = 12, slowPeriod = 26, signalPeriod = 9, SimpleMAOscillator = false, SimpleMASignal = false } = input; if (values.length < slowPeriod) { return []; } const result = []; // Calculate fast and slow moving averages const fastMA = SimpleMAOscillator ? sma({ period: fastPeriod, values }) : ema({ period: fastPeriod, values }); const slowMA = SimpleMAOscillator ? sma({ period: slowPeriod, values }) : ema({ period: slowPeriod, values }); // Calculate MACD line const macdLine = []; const startIndex = slowPeriod - fastPeriod; for (let i = 0; i < slowMA.length; i++) { macdLine.push(fastMA[i + startIndex] - slowMA[i]); } // Calculate signal line const signalLine = SimpleMASignal ? sma({ period: signalPeriod, values: macdLine }) : ema({ period: signalPeriod, values: macdLine }); // Create result array for (let i = 0; i < macdLine.length; i++) { const macdValue = macdLine[i]; const signalValue = i >= signalPeriod - 1 ? signalLine[i - signalPeriod + 1] : undefined; const histogramValue = signalValue !== undefined ? macdValue - signalValue : undefined; result.push({ MACD: macdValue, signal: signalValue, histogram: histogramValue }); } return result; } class MACD { constructor(input) { this.macdHistory = []; this.initialized = false; this.count = 0; this.fastPeriod = input.fastPeriod || 12; this.slowPeriod = input.slowPeriod || 26; this.signalPeriod = input.signalPeriod || 9; const EMACls = input.SimpleMAOscillator ? SMA : EMA; const SignalCls = input.SimpleMASignal ? SMA : EMA; this.fastEMA = new EMACls({ period: this.fastPeriod, values: [] }); this.slowEMA = new EMACls({ period: this.slowPeriod, values: [] }); this.signalEMA = new SignalCls({ period: this.signalPeriod, values: [] }); if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } nextValue(value) { this.count++; const fastValue = this.fastEMA.nextValue(value); const slowValue = this.slowEMA.nextValue(value); if (fastValue === undefined || slowValue === undefined) { return undefined; } const macdValue = fastValue - slowValue; this.macdHistory.push(macdValue); const signalValue = this.signalEMA.nextValue(macdValue); const histogramValue = signalValue !== undefined ? macdValue - signalValue : undefined; return { MACD: macdValue, signal: signalValue, histogram: histogramValue }; } getResult() { if (this.macdHistory.length === 0) { return []; } const lastMACD = this.macdHistory[this.macdHistory.length - 1]; const signalResult = this.signalEMA.getResult(); const lastSignal = signalResult.length > 0 ? signalResult[0] : undefined; const histogram = lastSignal !== undefined ? lastMACD - lastSignal : undefined; return [{ MACD: lastMACD, signal: lastSignal, histogram: histogram }]; } } MACD.calculate = macd; function rsi(input) { const { period = 14, values } = input; if (period <= 0 || values.length <= period) { return []; } const result = []; const gains = []; const losses = []; // Calculate initial gains and losses for (let i = 1; i < values.length; i++) { const difference = values[i] - values[i - 1]; gains.push(difference > 0 ? difference : 0); losses.push(difference < 0 ? Math.abs(difference) : 0); } // Calculate initial average gain and loss (SMA) let avgGain = gains.slice(0, period).reduce((sum, gain) => sum + gain, 0) / period; let avgLoss = losses.slice(0, period).reduce((sum, loss) => sum + loss, 0) / period; // Calculate first RSI let rs = avgLoss === 0 ? 100 : avgGain / avgLoss; result.push(parseFloat((100 - (100 / (1 + rs))).toFixed(2))); // Calculate subsequent RSI using Wilder's smoothing for (let i = period; i < gains.length; i++) { avgGain = ((avgGain * (period - 1)) + gains[i]) / period; avgLoss = ((avgLoss * (period - 1)) + losses[i]) / period; rs = avgLoss === 0 ? 100 : avgGain / avgLoss; result.push(parseFloat((100 - (100 / (1 + rs))).toFixed(2))); } return result; } class RSI { constructor(input) { this.values = []; this.gains = []; this.losses = []; this.avgGain = 0; this.avgLoss = 0; this.initialized = false; this.count = 0; this.period = input.period || 14; if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } nextValue(value) { this.values.push(value); this.count++; if (this.values.length === 1) { return undefined; // Need at least 2 values to calculate difference } const difference = value - this.values[this.values.length - 2]; const gain = difference > 0 ? difference : 0; const loss = difference < 0 ? Math.abs(difference) : 0; this.gains.push(gain); this.losses.push(loss); if (!this.initialized) { if (this.gains.length === this.period) { // Calculate initial average using SMA this.avgGain = this.gains.reduce((sum, g) => sum + g, 0) / this.period; this.avgLoss = this.losses.reduce((sum, l) => sum + l, 0) / this.period; this.initialized = true; const rs = this.avgLoss === 0 ? 100 : this.avgGain / this.avgLoss; return parseFloat((100 - (100 / (1 + rs))).toFixed(2)); } return undefined; } // Use Wilder's smoothing for subsequent values this.avgGain = ((this.avgGain * (this.period - 1)) + gain) / this.period; this.avgLoss = ((this.avgLoss * (this.period - 1)) + loss) / this.period; const rs = this.avgLoss === 0 ? 100 : this.avgGain / this.avgLoss; return parseFloat((100 - (100 / (1 + rs))).toFixed(2)); } getResult() { if (!this.initialized) { return []; } const rs = this.avgLoss === 0 ? 100 : this.avgGain / this.avgLoss; return [parseFloat((100 - (100 / (1 + rs))).toFixed(2))]; } } RSI.calculate = rsi; function cci(input) { const { period = 20, high, low, close } = input; if (high.length !== low.length || low.length !== close.length || close.length < period) { return []; } const result = []; for (let i = period - 1; i < close.length; i++) { // Calculate typical prices for the period const typicalPrices = []; for (let j = i - period + 1; j <= i; j++) { typicalPrices.push((high[j] + low[j] + close[j]) / 3); } // Calculate SMA of typical prices const smaTP = typicalPrices.reduce((sum, tp) => sum + tp, 0) / period; // Calculate mean deviation const meanDeviation = typicalPrices.reduce((sum, tp) => sum + Math.abs(tp - smaTP), 0) / period; // Calculate CCI const currentTypicalPrice = (high[i] + low[i] + close[i]) / 3; let cciValue; if (meanDeviation === 0) { cciValue = 0; // Avoid division by zero } else { cciValue = (currentTypicalPrice - smaTP) / (0.015 * meanDeviation); } result.push(cciValue); } return result; } class CCI { constructor(input) { this.highValues = []; this.lowValues = []; this.closeValues = []; this.period = input.period || 20; } nextValue(high, low, close) { this.highValues.push(high); this.lowValues.push(low); this.closeValues.push(close); if (this.highValues.length > this.period) { this.highValues.shift(); this.lowValues.shift(); this.closeValues.shift(); } if (this.highValues.length === this.period) { // Calculate typical prices for the period const typicalPrices = []; for (let i = 0; i < this.period; i++) { typicalPrices.push((this.highValues[i] + this.lowValues[i] + this.closeValues[i]) / 3); } // Calculate SMA of typical prices const smaTP = typicalPrices.reduce((sum, tp) => sum + tp, 0) / this.period; // Calculate mean deviation const meanDeviation = typicalPrices.reduce((sum, tp) => sum + Math.abs(tp - smaTP), 0) / this.period; // Calculate CCI const currentTypicalPrice = (high + low + close) / 3; if (meanDeviation === 0) { return 0; } return (currentTypicalPrice - smaTP) / (0.015 * meanDeviation); } return undefined; } getResult() { if (this.highValues.length < this.period) { return []; } // Calculate typical prices const typicalPrices = []; for (let i = 0; i < this.period; i++) { typicalPrices.push((this.highValues[i] + this.lowValues[i] + this.closeValues[i]) / 3); } // Calculate SMA of typical prices const smaTP = typicalPrices.reduce((sum, tp) => sum + tp, 0) / this.period; // Calculate mean deviation const meanDeviation = typicalPrices.reduce((sum, tp) => sum + Math.abs(tp - smaTP), 0) / this.period; // Calculate CCI const currentTypicalPrice = typicalPrices[typicalPrices.length - 1]; if (meanDeviation === 0) { return [0]; } return [(currentTypicalPrice - smaTP) / (0.015 * meanDeviation)]; } } CCI.calculate = cci; function awesomeoscillator(input) { const { high, low, fastPeriod = 5, slowPeriod = 34 } = input; if (high.length !== low.length || high.length < slowPeriod) { return []; } // Calculate midpoint prices (HL2) const midpoints = []; for (let i = 0; i < high.length; i++) { midpoints.push((high[i] + low[i]) / 2); } // Calculate fast and slow SMAs of midpoints const fastSMA = sma({ period: fastPeriod, values: midpoints }); const slowSMA = sma({ period: slowPeriod, values: midpoints }); // Calculate AO (Fast SMA - Slow SMA) const result = []; const startIndex = slowPeriod - fastPeriod; for (let i = 0; i < slowSMA.length; i++) { const aoValue = fastSMA[i + startIndex] - slowSMA[i]; result.push(aoValue); } return result; } class AwesomeOscillator { constructor(input) { this.highValues = []; this.lowValues = []; this.midpoints = []; this.fastPeriod = input.fastPeriod || 5; this.slowPeriod = input.slowPeriod || 34; // Create SMA calculators this.fastSMACalculator = { values: [], period: this.fastPeriod, sum: 0, nextValue: function (value) { this.values.push(value); this.sum += value; if (this.values.length > this.period) { this.sum -= this.values.shift(); } if (this.values.length === this.period) { return this.sum / this.period; } return undefined; } }; this.slowSMACalculator = { values: [], period: this.slowPeriod, sum: 0, nextValue: function (value) { this.values.push(value); this.sum += value; if (this.values.length > this.period) { this.sum -= this.values.shift(); } if (this.values.length === this.period) { return this.sum / this.period; } return undefined; } }; } nextValue(high, low) { this.highValues.push(high); this.lowValues.push(low); const midpoint = (high + low) / 2; this.midpoints.push(midpoint); const fastSMA = this.fastSMACalculator.nextValue(midpoint); const slowSMA = this.slowSMACalculator.nextValue(midpoint); if (fastSMA !== undefined && slowSMA !== undefined) { return fastSMA - slowSMA; } return undefined; } getResult() { if (this.midpoints.length === 0) { return []; } this.midpoints[this.midpoints.length - 1]; const fastSMA = this.fastSMACalculator.values.length === this.fastPeriod ? this.fastSMACalculator.sum / this.fastPeriod : undefined; const slowSMA = this.slowSMACalculator.values.length === this.slowPeriod ? this.slowSMACalculator.sum / this.slowPeriod : undefined; if (fastSMA !== undefined && slowSMA !== undefined) { return [fastSMA - slowSMA]; } return []; } } AwesomeOscillator.calculate = awesomeoscillator; function roc(input) { const { period, values } = input; if (period <= 0 || values.length <= period) { return []; } const result = []; for (let i = period; i < values.length; i++) { const currentPrice = values[i]; const pastPrice = values[i - period]; if (pastPrice === 0) { result.push(0); } else { const rocValue = ((currentPrice - pastPrice) / pastPrice) * 100; result.push(rocValue); } } return result; } class ROC { constructor(input) { this.values = []; this.period = input.period; if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } nextValue(value) { this.values.push(value); if (this.values.length > this.period + 1) { this.values.shift(); } if (this.values.length === this.period + 1) { const currentPrice = this.values[this.values.length - 1]; const pastPrice = this.values[0]; if (pastPrice === 0) { return 0; } return ((currentPrice - pastPrice) / pastPrice) * 100; } return undefined; } getResult() { if (this.values.length < this.period + 1) { return []; } const currentPrice = this.values[this.values.length - 1]; const pastPrice = this.values[0]; if (pastPrice === 0) { return [0]; } return [((currentPrice - pastPrice) / pastPrice) * 100]; } } ROC.calculate = roc; function stochastic(input) { const { period = 14, signalPeriod = 3, high, low, close } = input; if (high.length !== low.length || low.length !== close.length || close.length < period) { return []; } const result = []; // Calculate %K values const kValues = []; for (let i = period - 1; i < close.length; i++) { const highestHigh = Math.max(...high.slice(i - period + 1, i + 1)); const lowestLow = Math.min(...low.slice(i - period + 1, i + 1)); let kValue; if (highestHigh === lowestLow) { kValue = 50; // Avoid division by zero } else { kValue = ((close[i] - lowestLow) / (highestHigh - lowestLow)) * 100; } kValues.push(kValue); } // Calculate %D (SMA of %K) const dValues = sma({ period: signalPeriod, values: kValues }); // Combine results for (let i = 0; i < kValues.length; i++) { const dValue = i >= signalPeriod - 1 ? dValues[i - signalPeriod + 1] : undefined; result.push({ k: kValues[i], d: dValue }); } return result; } class Stochastic { constructor(input) { this.highValues = []; this.lowValues = []; this.closeValues = []; this.kValues = []; this.period = input.period || 14; this.signalPeriod = input.signalPeriod || 3; // Import SMA class for D calculation - we'll handle this inline to avoid circular imports this.dCalculator = { values: [], period: this.signalPeriod, nextValue: (value) => { this.dCalculator.values.push(value); if (this.dCalculator.values.length > this.dCalculator.period) { this.dCalculator.values.shift(); } if (this.dCalculator.values.length === this.dCalculator.period) { return this.dCalculator.values.reduce((sum, val) => sum + val, 0) / this.dCalculator.period; } return undefined; } }; } nextValue(high, low, close) { this.highValues.push(high); this.lowValues.push(low); this.closeValues.push(close); // Keep only the required period if (this.highValues.length > this.period) { this.highValues.shift(); this.lowValues.shift(); this.closeValues.shift(); } if (this.highValues.length === this.period) { const highestHigh = Math.max(...this.highValues); const lowestLow = Math.min(...this.lowValues); const currentClose = close; let kValue; if (highestHigh === lowestLow) { kValue = 50; } else { kValue = ((currentClose - lowestLow) / (highestHigh - lowestLow)) * 100; } this.kValues.push(kValue); const dValue = this.dCalculator.nextValue(kValue); return { k: kValue, d: dValue }; } return undefined; } getResult() { if (this.kValues.length === 0) { return []; } const lastK = this.kValues[this.kValues.length - 1]; const lastD = this.dCalculator.values.length === this.signalPeriod ? this.dCalculator.values.reduce((sum, val) => sum + val, 0) / this.signalPeriod : undefined; return [{ k: lastK, d: lastD }]; } } Stochastic.calculate = stochastic; function williamsr(input) { const { period = 14, high, low, close } = input; if (high.length !== low.length || low.length !== close.length || close.length < period) { return []; } const result = []; for (let i = period - 1; i < close.length; i++) { const highestHigh = Math.max(...high.slice(i - period + 1, i + 1)); const lowestLow = Math.min(...low.slice(i - period + 1, i + 1)); let williamsR; if (highestHigh === lowestLow) { williamsR = -50; // Middle value when no range } else { williamsR = ((highestHigh - close[i]) / (highestHigh - lowestLow)) * -100; } result.push(williamsR); } return result; } class WilliamsR { constructor(input) { this.highValues = []; this.lowValues = []; this.closeValues = []; this.period = input.period || 14; } nextValue(high, low, close) { this.highValues.push(high); this.lowValues.push(low); this.closeValues.push(close); if (this.highValues.length > this.period) { this.highValues.shift(); this.lowValues.shift(); this.closeValues.shift(); } if (this.highValues.length === this.period) { const highestHigh = Math.max(...this.highValues); const lowestLow = Math.min(...this.lowValues); const currentClose = close; if (highestHigh === lowestLow) { return -50; } return ((highestHigh - currentClose) / (highestHigh - lowestLow)) * -100; } return undefined; } getResult() { if (this.highValues.length < this.period) { return []; } const highestHigh = Math.max(...this.highValues); const lowestLow = Math.min(...this.lowValues); const currentClose = this.closeValues[this.closeValues.length - 1]; if (highestHigh === lowestLow) { return [-50]; } return [((highestHigh - currentClose) / (highestHigh - lowestLow)) * -100]; } } WilliamsR.calculate = williamsr; function trix(input) { const { period = 14, values } = input; if (values.length < period * 3) { return []; } // First EMA const ema1 = ema({ period, values }); // Second EMA of first EMA const ema2 = ema({ period, values: ema1 }); // Third EMA of second EMA const ema3 = ema({ period, values: ema2 }); // Calculate TRIX: Rate of change of triple EMA const result = []; for (let i = 1; i < ema3.length; i++) { const currentValue = ema3[i]; const previousValue = ema3[i - 1]; if (previousValue === 0) { result.push(0); } else { const trixValue = ((currentValue - previousValue) / previousValue) * 100; result.push(trixValue); } } return result; } class TRIX { constructor(input) { this.initialized = false; this.period = input.period || 14; // Create inline EMA calculators to avoid circular imports this.ema1Calculator = this.createEMACalculator(this.period); this.ema2Calculator = this.createEMACalculator(this.period); this.ema3Calculator = this.createEMACalculator(this.period); if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } createEMACalculator(period) { const multiplier = 2 / (period + 1); return { values: [], period, multiplier, previousEMA: undefined, initialized: false, nextValue: function (value) { this.values.push(value); if (!this.initialized) { if (this.values.length === this.period) { const sum = this.values.reduce((acc, val) => acc + val, 0); this.previousEMA = sum / this.period; this.initialized = true; return this.previousEMA; } return undefined; } const currentEMA = (value - this.previousEMA) * this.multiplier + this.previousEMA; this.previousEMA = currentEMA; return currentEMA; } }; } nextValue(value) { const ema1Value = this.ema1Calculator.nextValue(value); if (ema1Value === undefined) return undefined; const ema2Value = this.ema2Calculator.nextValue(ema1Value); if (ema2Value === undefined) return undefined; const ema3Value = this.ema3Calculator.nextValue(ema2Value); if (ema3Value === undefined) return undefined; if (!this.initialized) { this.previousTripleEMA = ema3Value; this.initialized = true; return undefined; // Need at least 2 values to calculate rate of change } const currentTripleEMA = ema3Value; let trixValue; if (this.previousTripleEMA === 0) { trixValue = 0; } else { trixValue = ((currentTripleEMA - this.previousTripleEMA) / this.previousTripleEMA) * 100; } this.previousTripleEMA = currentTripleEMA; return trixValue; } getResult() { if (!this.initialized || this.previousTripleEMA === undefined) { return []; } // Return the last calculated TRIX value const currentTripleEMA = this.ema3Calculator.previousEMA; if (currentTripleEMA === undefined) return []; let trixValue; if (this.previousTripleEMA === 0) { trixValue = 0; } else { trixValue = ((currentTripleEMA - this.previousTripleEMA) / this.previousTripleEMA) * 100; } return [trixValue]; } } TRIX.calculate = trix; function stochasticrsi(input) { const { rsiPeriod = 14, stochasticPeriod = 14, kPeriod = 3, dPeriod = 3, values } = input; // Need enough data for full calculation const requiredLength = rsiPeriod + stochasticPeriod + kPeriod + dPeriod - 1; if (values.length < requiredLength) { return []; } // Calculate RSI first const rsiValues = rsi({ period: rsiPeriod, values }); if (rsiValues.length < stochasticPeriod) { return []; } const result = []; const stochRSIValues = []; // Calculate Stochastic RSI for (let i = stochasticPeriod - 1; i < rsiValues.length; i++) { const rsiSlice = rsiValues.slice(i - stochasticPeriod + 1, i + 1); const highestRSI = Math.max(...rsiSlice); const lowestRSI = Math.min(...rsiSlice); let stochRSI; if (highestRSI === lowestRSI) { stochRSI = 0; // Avoid division by zero } else { stochRSI = ((rsiValues[i] - lowestRSI) / (highestRSI - lowestRSI)) * 100; } stochRSIValues.push(stochRSI); } // Calculate %K (SMA of Stochastic RSI) const kValues = sma({ period: kPeriod, values: stochRSIValues }); // Calculate %D (SMA of %K) const dValues = sma({ period: dPeriod, values: kValues }); // Only return results when we have complete D values // This means we start from kPeriod + dPeriod - 2 index in stochRSI const startIndex = kPeriod + dPeriod - 2; for (let i = startIndex; i < stochRSIValues.length; i++) { const kIndex = i - kPeriod + 1; const dIndex = i - kPeriod - dPeriod + 2; result.push({ stochRSI: stochRSIValues[i], k: kValues[kIndex], d: dValues[dIndex] }); } return result; } class StochasticRSI { constructor(input) { this.values = []; this.rsiValues = []; this.stochRSIValues = []; this.rsiPeriod = input.rsiPeriod || 14; this.stochasticPeriod = input.stochasticPeriod || 14; this.kPeriod = input.kPeriod || 3; this.dPeriod = input.dPeriod || 3; // Create inline RSI calculator this.rsiCalculator = this.createRSICalculator(this.rsiPeriod); // Create SMA calculators for K and D this.kCalculator = this.createSMACalculator(this.kPeriod); this.dCalculator = this.createSMACalculator(this.dPeriod); if (input.values?.length) { input.values.forEach(value => this.nextValue(value)); } } createRSICalculator(period) { return { values: [], gains: [], losses: [], avgGain: 0, avgLoss: 0, initialized: false, period, nextValue: function (value) { this.values.push(value); if (this.values.length === 1) return undefined; const diff = value - this.values[this.values.length - 2]; const gain = diff > 0 ? diff : 0; const loss = diff < 0 ? Math.abs(diff) : 0; this.gains.push(gain); this.losses.push(loss); if (!this.initialized) { if (this.gains.length === this.period) { this.avgGain = this.gains.reduce((sum, g) => sum + g, 0) / this.period; this.avgLoss = this.losses.reduce((sum, l) => sum + l, 0) / this.period; this.initialized = true; const rs = this.avgLoss === 0 ? 100 : this.avgGain / this.avgLoss; return parseFloat((100 - (100 / (1 + rs))).toFixed(2)); } return undefined; } this.avgGain = ((this.avgGain * (this.period - 1)) + gain) / this.period; this.avgLoss = ((this.avgLoss * (this.period - 1)) + loss) / this.period; const rs = this.avgLoss === 0 ? 100 : this.avgGain / this.avgLoss; return parseFloat((100 - (100 / (1 + rs))).toFixed(2)); } }; } createSMACalculator(period) { return { values: [], period, nextValue: function (value) { this.values.push(value); if (this.values.length > this.period) { this.values.shift(); } if (this.values.length === this.period) { return this.values.reduce((sum, val) => sum + val, 0) / this.period; } return undefined; } }; } nextValue(value) { this.values.push(value); // Calculate RSI const rsiValue = this.rsiCalculator.nextValue(value); if (rsiValue === undefined) return undefined; this.rsiValues.push(rsiValue); // Keep only required RSI values if (this.rsiValues.length > this.stochasticPeriod) { this.rsiValues.shift(); } if (this.rsiValues.length < this.stochasticPeriod) { return undefined; } // Calculate Stochastic RSI const highestRSI = Math.max(...this.rsiValues); const lowestRSI = Math.min(...this.rsiValues); let stochRSI; if (highestRSI === lowestRSI) { stochRSI = 0; } else { stochRSI = ((rsiValue - lowestRSI) / (highestRSI - lowestRSI)) * 100; } this.stochRSIValues.push(stochRSI); // Calculate %K and %D const kValue = this.kCalculator.nextValue(stochRSI); const dValue = kValue !== undefined ? this.dCalculator.nextValue(kValue) : undefined; // Only return when we have complete %D values (same logic as functional version) if (dValue === undefined) { return undefined; } return { stochRSI: stochRSI, k: kValue, d: dValue }; } getResult() { if (this.stochRSIValues.length === 0) { return []; } const lastStochRSI = this.stochRSIValues[this.stochRSIValues.length - 1]; const kResult = this.kCalculator.values.length === this.kPeriod ? this.kCalculator.values.reduce((sum, val) => sum + val, 0) / this.kPeriod : undefined; const dResult = this.dCalculator.values.length === this.dPeriod ? this.dCalculator.values.reduce((sum, val) => sum + val, 0) / this.dPeriod : undefined; return [{ stochRSI: lastStochRSI, k: kResult, d: dResult }]; } } StochasticRSI.calculate = stochasticrsi; function psar(input) { const { high, low, step = 0.02, max = 0.2 } = input; if (high.length !== low.length || high.length < 1) { return []; } const result = []; let curr; let extreme; let sar; let furthest; let up = true; let accel = step; let prev; // Process each period for (let i = 0; i < high.length; i++) { curr = { high: high[i], low: low[i] }; if (prev && curr && sar !== undefined && extreme !== undefined && furthest !== undefined) { sar = sar + accel * (extreme - sar); if (up) { sar = Math.min(sar, furthest.low, prev.low); if (curr.high > extreme) { extreme = curr.high; accel = Math.min(accel + step, max); } } else { sar = Math.max(sar, furthest.high, prev.high); if (curr.low < extreme) { extreme = curr.low; accel = Math.min(accel + step, max); } } if ((up && curr.low < sar) || (!up && curr.high > sar)) { accel = step; sar = extreme; up = !up; extreme = !up ? curr.low : curr.high; } } else { // Initialize on first data point sar = curr.low; extreme = curr.high; } furthest = prev || curr; if (curr) { prev = curr; } result.push(sar); } return result; } class PSAR { constructor(input) { this.up = true; this.results = []; this.step = input.step || 0.02; this.max = input.max || 0.2; this.accel = this.step; } nextValue(high, low) { this.curr = { high, low }; if (this.prev) { if (this.prev && this.curr) { this.sar = this.sar + this.accel * (this.extreme - this.sar); if (this.up) { this.sar = Math.min(this.sar, this.furthest.low, this.prev.low); if (this.curr.high > this.extreme) { this.extreme = this.curr.high; this.accel = Math.min(this.accel + this.step, this.max); } } else { this.sar = Math.max(this.sar, this.furthest.high, this.prev.high); if (this.curr.low < this.extreme) { this.extreme = this.curr.low; this.accel = Math.min(this.accel + this.step, this.max); } } if ((this.up && this.curr.low < this.sar) || (!this.up && this.curr.high > this.sar)) { this.accel = this.step; this.sar = this.extreme; this.up = !this.up; this.extreme = !this.up ? this.curr.low : this.curr.high; } } } else { // Initialize on first data point this.sar = this.curr.low; this.extreme = this.curr.high; } this.furthest = this.prev || this.curr; if (this.curr) { this.prev = this.curr; } this.results.push(this.sar); return this.sar; } getResult() { return this.results; } } PSAR.calculate = psar; function kst(input) { const { values, ROCPer1 = 10, ROCPer2 = 15, ROCPer3 = 20, ROCPer4 = 30, SMAROCPer1 = 10, SMAROCPer2 = 10, SMAROCPer3 = 10, SMAROCPer4 = 15, signalPeriod = 9 } = input; if (!values || values.length === 0) { return []; } const result = []; // Calculate minimum required data points for first result const firstResult = Math.max(ROCPer1 + SMAROCPer1, ROCPer2 + SMAROCPer2, ROCPer3 + SMAROCPer3, ROCPer4 + SMAROCPer4); if (values.length < firstResult) { return []; } // Arrays to store ROC values for SMA calculation const roc1Values = []; const roc2Values = []; const roc3Values = []; const roc4Values = []; // KST values for signal calculation const kstValues = []; for (let i = 0; i < values.length; i++) { // Calculate ROC values let roc1Value; let roc2Value; let roc3Value; let roc4Value; if (i >= ROCPer1) { roc1Value = ((values[i] - values[i - ROCPer1]) / values[i - ROCPer1]) * 100; roc1Values.push(roc1Value); } if (i >= ROCPer2) { roc2Value = ((values[i] - values[i - ROCPer2]) / values[i - ROCPer2]) * 100; roc2Values.push(roc2Value); } if (i >= ROCPer3) { roc3Value = ((values[i] - values[i - ROCPer3]) / values[i - ROCPer3]) * 100; roc3Values.push(roc3Value); } if (i >= ROCPer4) { roc4Value = ((values[i] - values[i - ROCPer4]) / values[i - ROCPer4]) * 100; roc4Values.push(roc4Value); } // Calculate SMA values let rcma1; let rcma2; let rcma3; let rcma4; if (roc1Values.length >= SMAROCPer1) { const sma1Sum = roc1Values.slice(-SMAROCPer1).reduce((sum, val) => sum + val, 0); rcma1 = sma1Sum / SMAROCPer1; } if (roc2Values.length >= SMAROCPer2) { const sma2Sum = roc2Values.slice(-SMAROCPer2).reduce((sum, val) => sum + val, 0); rcma2 = sma2Sum / SMAROCPer2; } if (roc3Values.length >= SMAROCPer3) { const sma3Sum = roc3Values.slice(-SMAROCPer3).reduce((sum, val) => sum + val, 0); rcma3 = sma3Sum / SMAROCPer3; } if (roc4Values.length >= SMAROCPer4) { const sma4Sum = roc4Values.slice(-SMAROCPer4).reduce((sum, val) => sum + val, 0); rcma4 = sma4Sum / SMAROCPer4; } // Calculate KST if we have all components if (i >= firstResult - 1 && rcma1 !== undefined && rcma2 !== undefined && rcma3 !== undefined && rcma4 !== undefined) { const kstValue = (rcma1 * 1) + (rcma2 * 2) + (rcma3 * 3) + (rcma4 * 4); kstValues.push(kstValue); // Calculate signal (SMA of KST) let signalValue; if (kstValues.length >= signalPeriod) { const signalSum = kstValues.slice(-signalPeriod).reduce((sum, val) => sum + val, 0); signalValue = signalSum / signalPeriod; } result.push({ kst: kstValue, signal: signalValue }); } } return result; } class KST { constructor(input) { this.values = []; // Arrays to store ROC values for SMA calculation this.roc1Values = []; this.roc2Values = []; this.roc3Values = []; this.roc4Values = []; // KST values for signal calculation this.kstValues = []; this.results = []; this.ROCPer1 = input.ROCPer1 || 10; this.ROCPer2 = input.ROCPer2 || 15; this.ROCPer3 = input.ROCPer3 || 20; this.ROCPer4 = input.ROCPer4 || 30; this.SMAROCPer1 = input.SMAROCPer1 || 10; this.SMAROCPer2 = input.SMAROCPer2 || 10; this.SMAROCPer3 = input.SMAROCPer3 || 10; this.SMAROCPer4 = input.SMAROCPer4 || 15; this.signalPeriod = input.signalPeriod || 9; this.firstResult = Math.max(this.ROCPer1 + this.SMAROCPer1, this.ROCPer2 + this.SMAROCPer2, this.ROCPer3 + this.SMAROCPer3, this.ROCPer4 + this.SMAROCPer4); if (input.values && input.values.length) { input.values.forEach(value => this.nextValue(value)); } } nextValue(value) { this.values.push(value); const i = this.values.length - 1; // Calculate ROC values if (i >= this.ROCPer1) { const roc1Value = ((value - this.values[i - this.ROCPer1]) / this.values[i - this.ROCPer1]) * 100; this.roc1Values.push(roc1Value); } if (i >= this.ROCPer2) { const roc2Value = ((value - this.values[i - this.ROCPer2]) / this.values[i - this.ROCPer2]) * 100; this.roc2Values.push(roc2Value); } if (i >= this.ROCPer3) { const roc3Value = ((value - this.values[i - this.ROCPer3]) / this.values[i - this.ROCPer3]) * 100; this.roc3Values.push(roc3Value); } if (i >= this.ROCPer4) { const roc4Value = ((value - this.values[i - this.ROCPer4]) / this.values[i - this.ROCPer4]) * 100; this.roc4Values.push(roc4Value); } // Calculate SMA values let rcma1; let rcma2; let rcma3; let rcma4; if (this.roc1Values.length >= this.SMAROCPer1) { const sma1Sum = this.roc1Values.slice(-this.SMAROCPer1).reduce((sum, val) => sum + val, 0); rcma1 = sma1Sum / this.SMAROCPer1; } if (this.roc2Values.length >= this.SMAROCPer2) { const sma2Sum = this.roc2Values.slice(-this.SMAROCPer2).reduce((sum, val) => sum + val, 0); rcma2 = sma2Sum / this.SMAROCPer2; } if (this.roc3Values.length >= this.SMAROCPer3) { const sma3Sum = this.roc3Values.slice(-this.SMAROCPer3).reduce((sum, val) => sum + val, 0); rcma3 = sma3Sum / this.SMAROCPer3; } if (this.roc4Values.length >= this.SMAROCPer4) { const sma4Sum = this.roc4Values.slice(-this.SMAROCPer4).reduce((sum, val) => sum + val, 0); rcma4 = sma4Sum / this.SMAROCPer4; } // Calculate KST if we have all components if (i >= this.firstResult - 1 && rcma1 !== undefined && rcma2 !== undefined && rcma3 !== undefined && rcma4 !== undefined) { const kstValue = (rcma1 * 1) + (rcma2 * 2) + (rcma3 * 3) + (rcma4 * 4); this.kstValues.push(kstValue); // Calculate signal (SMA of KST) let signalValue; if (this.kstValues.length >= this.signalPeriod) { const signalSum = this.kstValues.slice(-this.signalPeriod).reduce((sum, val) => sum + val, 0); signalValue = signalSum / this.signalPeriod; } const result = { kst: kstValue, signal: signalValue }; this.results.push(result); return result; } return undefined; } getResult() { return this.results; } } KST.calculate = kst; function trueRange$2(high, low, previousClose) { const highLow = high - low; const highPrevClose = Math.abs(high - previousClose); const lowPrevClose = Math.abs(low - previousClose); return Math.max(highLow, highPrevClose, lowPrevClose); } function buyingPressure(high, low, close) { return close - Math.min(low, close); } function calculateSum(values, period, startIndex) { let sum = 0; for (let i = Math.max(0, startIndex - period + 1); i <= startIndex; i++) { sum += values[i]; } return sum; } function ultimateoscillator(input) { const { high, low, close, period1 = 7, period2 = 14, period3 = 28 } = input; if (high.length !== low.length || low.length !== close.length || close.length < period3 + 1) { return []; } const result = []; const trueRanges = []; const buyingPressures = []; // Calculate true ranges and buying pressures (skip first value since we need previous close) for (let i = 1; i < close.length; i++) { const tr = trueRange$2(high[i], low[i], close[i - 1]); const bp = buyingPressure(high[i], low[i], close[i]); trueRanges.push(tr); buyingPressures.push(bp); } // Calculate Ultimate Oscillator starting from period3 for (let i = period3 - 1; i < trueRanges.length; i++) { // Calculate sums for each period const bp1Sum = calculateSum(buyingPressures, period1, i); const tr1Sum = calculateSum(trueRanges, period1, i); const raw1 = bp1Sum / tr1Sum; const