UNPKG

trading-signals

Version:

Technical indicators to run technical analysis with JavaScript / TypeScript.

1,418 lines (1,383 loc) 74.2 kB
//#region src/error/NotEnoughDataError.ts var NotEnoughDataError = class extends Error { constructor(requiredAmount) { super(`Not enough data. A minimum of "${requiredAmount}" inputs is required to perform the computation.`); Object.setPrototypeOf(this, new.target.prototype); this.name = "NotEnoughDataError"; } }; //#endregion //#region src/types/Indicator.ts const TradingSignal = { BEARISH: "BEARISH", BULLISH: "BULLISH", SIDEWAYS: "SIDEWAYS", UNKNOWN: "UNKNOWN" }; /** * Implements common update behaviour among indicators. */ var TechnicalIndicator = class { result; getResult() { try { return this.getResultOrThrow(); } catch { return null; } } getResultOrThrow() { if (this.result === void 0) throw new NotEnoughDataError(this.getRequiredInputs()); return this.result; } get isStable() { return this.result !== void 0; } add(input) { return this.update(input, false); } replace(input) { return this.update(input, true); } updates(inputs, replace = false) { return inputs.map((input) => this.update(input, replace)); } }; /** * Tracks results of an indicator over time. */ var IndicatorSeries = class extends TechnicalIndicator { previousResult; setResult(value, replace) { if (replace) this.result = this.previousResult; this.previousResult = this.result; return this.result = value; } }; /** * Calculates a signal for an indicator. */ var TrendIndicatorSeries = class extends IndicatorSeries { #previousSignalState; setResult(value, replace) { if (replace && this.previousResult !== void 0) this.#previousSignalState = this.calculateSignalState(this.previousResult); else if (!replace) this.#previousSignalState = this.calculateSignalState(this.result); return super.setResult(value, replace); } getSignal() { const currentState = this.calculateSignalState(this.getResult()); return { hasChanged: this.#previousSignalState !== void 0 && this.#previousSignalState !== currentState, state: currentState }; } }; //#endregion //#region src/trend/MA/MovingAverage.ts /** * Moving Average (MA) * Type: Trend * * Base class for trend-following (lagging) indicators. The longer the moving average interval, the greater the lag. * * @see https://www.investopedia.com/terms/m/movingaverage.asp */ var MovingAverage = class extends IndicatorSeries { constructor(interval) { super(); this.interval = interval; } }; //#endregion //#region src/util/getAverage.ts /** * Return the mean / average value. */ function getAverage(values) { return values.length ? values.reduce((sum, x) => sum + x, 0) / values.length : 0; } //#endregion //#region src/util/pushUpdate.ts /** * Adds an item to the array or replaces the last item in the array. * If the array limit size is exceeded, the oldest array element will be removed and returned by the function. */ function pushUpdate(array, replace, item, maxLength) { if (array.length > 0 && replace === true) array[array.length - 1] = item; else array.push(item); if (array.length > maxLength) return array.shift(); return null; } //#endregion //#region src/trend/SMA/SMA.ts /** * Simple Moving Average (SMA) * Type: Trend * * The Simple Moving Average (SMA) creates an average of all prices within a fixed interval. The SMA weights the prices of all periods equally which makes it not as responsive to recent prices as the EMA. * * @see https://www.investopedia.com/terms/s/sma.asp */ var SMA = class extends MovingAverage { prices = []; getRequiredInputs() { return this.interval; } update(price, replace) { pushUpdate(this.prices, replace, price, this.interval); if (this.prices.length === this.interval) return this.setResult(getAverage(this.prices), replace); return null; } }; //#endregion //#region src/momentum/AO/AO.ts /** * Awesome Oscillator (AO) * Type: Momentum * * The Awesome Oscillator (AO) is an indicator used to measure market momentum. * It has been developed by the technical analyst and charting enthusiast Bill Williams. * * When AO crosses above Zero, short term momentum is rising faster than long term momentum which signals a bullish * buying opportunity. * * When AO crosses below Zero, short term momentum is falling faster then the long term momentum * which signals a bearish selling opportunity. * * @see https://www.tradingview.com/support/solutions/43000501826-awesome-oscillator-ao/ * @see https://tradingstrategyguides.com/bill-williams-awesome-oscillator-strategy/ */ var AO = class extends TrendIndicatorSeries { long; short; constructor(shortInterval, longInterval, SmoothingIndicator = SMA) { super(); this.shortInterval = shortInterval; this.longInterval = longInterval; this.short = new SmoothingIndicator(shortInterval); this.long = new SmoothingIndicator(longInterval); } getRequiredInputs() { return this.long.getRequiredInputs(); } update({ low, high }, replace) { const medianPrice = (low + high) / 2; this.short.update(medianPrice, replace); this.long.update(medianPrice, replace); if (this.long.isStable) return this.setResult(this.short.getResultOrThrow() - this.long.getResultOrThrow(), replace); return null; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isBullish = hasResult && result > 0; const isBearish = hasResult && result < 0; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isBullish: return TradingSignal.BULLISH; case isBearish: return TradingSignal.BEARISH; default: return TradingSignal.SIDEWAYS; } } }; //#endregion //#region src/momentum/MOM/MOM.ts /** * Momentum Indicator (MOM / MTM) * Type: Momentum * * The Momentum indicator returns the change between the current price and the price n times ago. * * @see https://en.wikipedia.org/wiki/Momentum_(technical_analysis) * @see https://www.warriortrading.com/momentum-indicator/ */ var MOM = class extends TrendIndicatorSeries { #history; #historyLength; constructor(interval) { super(); this.interval = interval; this.#historyLength = interval + 1; this.#history = []; } getRequiredInputs() { return this.#historyLength; } update(value, replace) { pushUpdate(this.#history, replace, value, this.#historyLength); if (this.#history.length === this.#historyLength) return this.setResult(value - this.#history[0], replace); return null; } calculateSignalState(result) { if (!(result !== null && result !== void 0)) return TradingSignal.UNKNOWN; if (result > 0) return TradingSignal.BULLISH; if (result < 0) return TradingSignal.BEARISH; return TradingSignal.SIDEWAYS; } }; //#endregion //#region src/momentum/AC/AC.ts /** * Accelerator Oscillator (AC) * Type: Momentum * * The Accelerator Oscillator (AC) is an indicator used to detect when a momentum changes. It has been developed by Bill Williams. If the momentum in an uptrend is starting to slow down, that could suggest that there is less interest in the asset. This typically leads to selling. In the inverse, momentum to the downside will start to slow down before buy orders come in. The Accelerator Oscillator also looks at whether there is an acceleration in the change of momentum. * * @see https://www.thinkmarkets.com/en/indicators/bill-williams-accelerator/ * @see https://help.quantower.com/quantower/analytics-panels/chart/technical-indicators/oscillators/accelerator-oscillator */ var AC = class extends TrendIndicatorSeries { ao; momentum; signal; constructor(shortAO, longAO, signalInterval) { super(); this.shortAO = shortAO; this.longAO = longAO; this.signalInterval = signalInterval; this.ao = new AO(shortAO, longAO); this.momentum = new MOM(1); this.signal = new SMA(signalInterval); } getRequiredInputs() { return this.ao.getRequiredInputs() + this.signal.getRequiredInputs(); } update(input, replace) { const ao = this.ao.update(input, replace); if (ao) { this.signal.update(ao, replace); if (this.signal.isStable) { const result = this.setResult(ao - this.signal.getResultOrThrow(), replace); this.momentum.update(result, replace); return result; } } return null; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isPositive = hasResult && result > 0; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isPositive: return TradingSignal.BULLISH; default: return TradingSignal.BEARISH; } } }; //#endregion //#region src/util/getGrid.ts function getGrid({ lower, upper, levels, tickSize, spacing }) { const prices = []; if (spacing === "arithmetic") { const step = (upper - lower) / (levels - 1); for (let i = 0; i < levels; i++) prices.push(lower + step * i); } else { const ratio = Math.pow(upper / lower, 1 / (levels - 1)); for (let i = 0; i < levels; i++) prices.push(lower * Math.pow(ratio, i)); } if (tickSize) { const roundToTick = (x) => Math.round(x / tickSize) * tickSize; return prices.map(roundToTick); } return prices; } //#endregion //#region src/util/getMaximum.ts function getMaximum(values) { let max = Number.MIN_SAFE_INTEGER; for (const value of values) if (max < value) max = value; return max; } //#endregion //#region src/util/getMedian.ts function getMedian(values) { const n = values.length; if (n === 0) throw new Error("Cannot calculate median of empty array"); if (n % 2 === 0) return (values[n / 2 - 1] + values[n / 2]) / 2; return values[Math.floor(n / 2)]; } //#endregion //#region src/util/getMinimum.ts function getMinimum(values) { let min = Number.MAX_SAFE_INTEGER; for (const value of values) if (min > value) min = value; return min; } //#endregion //#region src/util/getQuartile.ts function getQuartile(values, q) { const sorted = [...values].sort((a, b) => a - b); const n = sorted.length; const medianIndex = Math.floor(n / 2); if (q === .25) return getMedian(sorted.slice(0, medianIndex)); else if (q === .75) { if (n % 2 === 0) return getMedian(sorted.slice(medianIndex)); return getMedian(sorted.slice(medianIndex + 1)); } const pos = (sorted.length - 1) * q; const base = Math.floor(pos); const rest = pos - base; return sorted[base] + (sorted[base + 1] - sorted[base]) * rest; } //#endregion //#region src/util/getStandardDeviation.ts /** * Standard deviation calculates how prices for a collection of prices are spread out from the average price of these * prices. Standard deviation makes outliers even more visible than mean absolute deviation (MAD). * * @see https://www.mathsisfun.com/data/standard-deviation-formulas.html * @see https://www.youtube.com/watch?v=9-8E8L_77-8 */ function getStandardDeviation(values, average) { const middle = average || getAverage(values); const averageDifference = getAverage(values.map((value) => value - middle).map((value) => value * value)); return Math.sqrt(averageDifference); } //#endregion //#region src/util/getStreaks.ts /** * Tracks the lengths (streaks) of continuous price movements (up or down). * * @param prices A series of prices * @param keepSide If you want to receive only uptrends or downtrends * @returns An array of objects representing the filtered streaks */ function getStreaks(prices, keepSide) { const streaks = []; let currentStreak = 0; function saveStreak(i) { const endPrice = prices[i - 1]; const startPrice = prices[i - currentStreak - 1]; const percentage = (endPrice - startPrice) / startPrice * 100; streaks.push({ length: currentStreak, percentage }); } for (let i = 1; i < prices.length; i++) { const isUpward = keepSide === "up" && prices[i] > prices[i - 1]; const isDownward = keepSide === "down" && prices[i] < prices[i - 1]; if (isUpward || isDownward) currentStreak++; else { if (currentStreak > 0) saveStreak(i); currentStreak = 0; } } if (currentStreak > 0) saveStreak(prices.length); return streaks; } //#endregion //#region src/util/getWeekday.ts /** * Get the weekday name for a given date in a specific timezone. * Uses IANA timezone identifiers (e.g., 'America/New_York', 'Europe/London'). */ function getWeekday(timezone, date = new Date(Date.now())) { return new Intl.DateTimeFormat("en-US", { timeZone: timezone, weekday: "long" }).format(date); } function isMonday(timezone, date = new Date(Date.now())) { return getWeekday(timezone, date) === "Monday"; } function isTuesday(timezone, date = new Date(Date.now())) { return getWeekday(timezone, date) === "Tuesday"; } function isWednesday(timezone, date = new Date(Date.now())) { return getWeekday(timezone, date) === "Wednesday"; } function isThursday(timezone, date = new Date(Date.now())) { return getWeekday(timezone, date) === "Thursday"; } function isFriday(timezone, date = new Date(Date.now())) { return getWeekday(timezone, date) === "Friday"; } function isSaturday(timezone, date = new Date(Date.now())) { return getWeekday(timezone, date) === "Saturday"; } function isSunday(timezone, date = new Date(Date.now())) { return getWeekday(timezone, date) === "Sunday"; } //#endregion //#region src/volatility/MAD/MAD.ts /** * Mean Absolute Deviation (MAD) * Type: Volatility * * The mean absolute deviation (MAD) is calculating the absolute deviation / difference from the mean over a period. Large outliers will reflect in a higher MAD. * * @see https://en.wikipedia.org/wiki/Average_absolute_deviation */ var MAD = class extends IndicatorSeries { prices = []; constructor(interval) { super(); this.interval = interval; } getRequiredInputs() { return this.interval; } update(price, replace) { pushUpdate(this.prices, replace, price, this.interval); if (this.prices.length === this.interval) { const mean = getAverage(this.prices); let sum = 0; for (let i = 0; i < this.interval; i++) { const deviation = Math.abs(this.prices[i] - mean); sum += deviation; } return this.setResult(sum / this.interval, replace); } return null; } static getResultFromBatch(prices, average) { if (prices.length === 0) return 0; const mean = average || getAverage(prices); let sum = 0; for (let i = 0; i < prices.length; i++) { const deviation = Math.abs(prices[i] - mean); sum += deviation; } return sum / prices.length; } }; //#endregion //#region src/momentum/CCI/CCI.ts /** * Commodity Channel Index (CCI) * Type: Momentum * * The Commodity Channel Index (CCI), developed by Donald Lambert in 1980, compares the current mean price with the * average mean price over a period of time. Approximately 70 to 80 percent of CCI values are between −100 and +100, * which makes it an oscillator. Values above +100 imply an overbought condition, while values below −100 imply an * oversold condition. * * According to * [Investopia.com](https://www.investopedia.com/articles/active-trading/031914/how-traders-can-utilize-cci-commodity-channel-index-trade-stock-trends.asp#multiple-timeframe-cci-strategy), * traders often buy when the CCI dips below -100 and then rallies back above -100 to sell the security when it moves * above +100 and then drops back below +100. * * Interpretation: * -100 and below: Indicates an oversold condition or the start of a strong downtrend. * +100 and above: Indicates an overbought condition or the start of a strong uptrend. * Values near 0 often signal a lack of clear momentum. * * Note: Traders often combine CCI with other indicators to confirm trends or signals, as using it alone can lead to false signals. * It's particularly useful in volatile markets or when identifying shorter-term trading opportunities. * * @see https://en.wikipedia.org/wiki/Commodity_channel_index */ var CCI = class extends TrendIndicatorSeries { #sma; #typicalPrices; constructor(interval) { super(); this.interval = interval; this.#sma = new SMA(this.interval); this.#typicalPrices = []; } getRequiredInputs() { return this.#sma.getRequiredInputs(); } update(candle, replace) { const typicalPrice = this.#cacheTypicalPrice(candle, replace); this.#sma.update(typicalPrice, replace); if (this.#sma.isStable) { const mean = this.#sma.getResultOrThrow(); const meanDeviation = MAD.getResultFromBatch(this.#typicalPrices, mean); const numerator = typicalPrice - mean; const denominator = .015 * meanDeviation; return this.setResult(numerator / denominator, replace); } return null; } #cacheTypicalPrice({ high, low, close }, replace) { const typicalPrice = (high + low + close) / 3; pushUpdate(this.#typicalPrices, replace, typicalPrice, this.interval); return typicalPrice; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isOversold = hasResult && result <= -100; const isOverbought = hasResult && result >= 100; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isOversold: return TradingSignal.BEARISH; case isOverbought: return TradingSignal.BULLISH; default: return TradingSignal.SIDEWAYS; } } }; //#endregion //#region src/momentum/CG/CG.ts /** * Center of Gravity (CG) * Type: Momentum * * Implementation of the Center of Gravity (CG) oscillator by John Ehlers. The Center of Gravity (CG) aims to identify turning points in price action with minimal lag (leading indicator). Peaks and troughs in CG can precede actual price highs and lows. The CG is often paired with its own signal line for entry/exit triggers. * * Interpretation: * Crossing the zero line may suggest a shift in trend. * * Note: * - According to the specification, the price inputs shall be calculated the following way: ((High Price + Low Price) / 2) * - The selected interval should be half the dominant cycle length (signal line) * - If the interval gets too short, the CG oscillator loses its smoothing and gets a little too nervous for profitable trading * @see http://www.mesasoftware.com/papers/TheCGOscillator.pdf */ var CG = class extends TrendIndicatorSeries { signal; prices = []; get isStable() { return this.signal.isStable; } constructor(interval, signalInterval) { super(); this.interval = interval; this.signalInterval = signalInterval; this.signal = new SMA(signalInterval); } getRequiredInputs() { return this.signal.getRequiredInputs(); } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const signalValue = this.signal.getResult(); const hasSignal = signalValue !== null; const isAboveSignal = hasResult && hasSignal && result > signalValue; const isBelowSignal = hasResult && hasSignal && result < signalValue; switch (true) { case !hasResult || !hasSignal: return TradingSignal.UNKNOWN; case isAboveSignal: return TradingSignal.BULLISH; case isBelowSignal: return TradingSignal.BEARISH; default: return TradingSignal.SIDEWAYS; } } update(price, replace) { pushUpdate(this.prices, replace, price, this.interval); let nominator = 0; let denominator = 0; for (let i = 0; i < this.prices.length; ++i) { const price$1 = this.prices[i]; nominator = nominator + price$1 * (i + 1); denominator = denominator + price$1; } const cg = denominator > 0 ? nominator / denominator : 0; this.signal.update(cg, replace); if (this.signal.isStable) return this.setResult(cg, replace); return null; } }; //#endregion //#region src/momentum/MACD/MACD.ts /** * Moving Average Convergence Divergence (MACD) * Type: Momentum * * The MACD triggers trading signals when it crosses above (bullish buying opportunity) or below (bearish selling * opportunity) its signal line. MACD can be used together with the RSI to provide a more accurate trading signal. * * @see https://www.investopedia.com/terms/m/macd.asp */ var MACD = class extends TechnicalIndicator { prices = []; #previousResult; constructor(short, long, signal) { super(); this.short = short; this.long = long; this.signal = signal; } getRequiredInputs() { return this.long.getRequiredInputs(); } update(price, replace) { pushUpdate(this.prices, replace, price, this.long.interval); const short = this.short.update(price, replace); const long = this.long.update(price, replace); if (this.prices.length === this.long.interval) { const macd = short - long; const signal = this.signal.update(macd, replace); if (replace) this.result = this.#previousResult; this.#previousResult = this.result; return this.result = { histogram: macd - signal, macd, signal }; } return null; } calculateSignal(result) { const hasResult = result !== null && result !== void 0; const isBullish = hasResult && result.histogram > 0; const isBearish = hasResult && result.histogram < 0; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isBullish: return TradingSignal.BULLISH; case isBearish: default: return TradingSignal.BEARISH; } } getSignal() { const previousState = this.calculateSignal(this.#previousResult); const state = this.calculateSignal(this.getResult()); return { hasChanged: previousState !== state, state }; } }; //#endregion //#region src/momentum/OBV/OBV.ts /** * On-Balance Volume (OBV) * Type: Momentum * * On-balance volume (OBV) is a technical trading momentum indicator that uses volume flow to predict changes in stock price. Joseph Granville first developed the OBV metric in the 1963 book Granville's New Key to Stock Market Profits. * * @see https://www.investopedia.com/terms/o/onbalancevolume.asp */ var OBV = class extends TrendIndicatorSeries { candles = []; constructor(interval) { super(); this.interval = interval; } getRequiredInputs() { return this.interval; } update(candle, replace) { pushUpdate(this.candles, replace, candle, this.getRequiredInputs()); if (this.candles.length < this.getRequiredInputs()) return null; const prevPrice = this.candles[this.candles.length - 2].close; const prevResult = this.result ?? 0; const currentPrice = candle.close; const nextResult = currentPrice > prevPrice ? candle.volume : currentPrice < prevPrice ? -candle.volume : 0; return this.setResult(prevResult + nextResult, false); } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const previousResult = this.previousResult; const hasPreviousResult = previousResult !== void 0; const isBullish = hasResult && hasPreviousResult && result > previousResult; const isBearish = hasResult && hasPreviousResult && result < previousResult; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isBullish: return TradingSignal.BULLISH; case isBearish: return TradingSignal.BEARISH; default: return TradingSignal.SIDEWAYS; } } }; //#endregion //#region src/momentum/REI/REI.ts /** * Range Expansion Index (REI) * Type: Momentum Oscillator * * The Range Expansion Index (REI) is a momentum oscillator, measuring the velocity and magnitude of directional price movements. Developed by Thomas DeMark, it compares the current day's range to the average range over a given period. It quantifies whether the current price range represents a contraction or expansion compared to the average. The REI is most typically used on an 8 day timeframe. Extreme REI values often signal potential reversal points, as they reflect sharp directional moves that may not be sustainable. * * Interpretation: * According to Thomas DeMark, potential shifts in momentum when the REI rises above +60 and then drops below (price weakness). Conversely, when it falls below -60 and then climbs back above, it may signal price strength. * * - REI > +60: Overbought condition — strong upward momentum that may be unsustainable (when crossed from above) * - REI between +60 and -60: Neutral zone — the market is neither gaining upward strength nor showing downside pressure * - REI < -60: Oversold condition — strong downward momentum that could reverse (when crossed from below) * * @see https://en.wikipedia.org/wiki/Range_expansion_index * @see https://www.quantifiedstrategies.com/range-expansion-index/ * @see https://www.prorealcode.com/prorealtime-indicators/range-expansion-index-rei/ * @see https://github.com/EarnForex/Range-Expansion-Index * @see https://www.sierrachart.com/index.php?page=doc/StudiesReference.php&ID=448 */ var REI = class extends TrendIndicatorSeries { #highs = []; #lows = []; #closes = []; constructor(interval) { super(); this.interval = interval; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isOverbought = hasResult && result > 60; const isOversold = hasResult && result < -60; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isOverbought: return TradingSignal.BULLISH; case isOversold: return TradingSignal.BEARISH; default: return TradingSignal.SIDEWAYS; } } getRequiredInputs() { return this.interval + 8; } #calculateN(j) { if (this.#highs[j - 2] < this.#closes[j - 7] && this.#highs[j - 2] < this.#closes[j - 8] && this.#highs[j] < this.#highs[j - 5] && this.#highs[j] < this.#highs[j - 6]) return 0; return 1; } #calculateM(j) { if (this.#lows[j - 2] > this.#closes[j - 7] && this.#lows[j - 2] > this.#closes[j - 8] && this.#lows[j] > this.#lows[j - 5] && this.#lows[j] > this.#lows[j - 6]) return 0; return 1; } update(candle, replace) { if (replace) { this.#highs.pop(); this.#lows.pop(); this.#closes.pop(); } this.#highs.push(candle.high); this.#lows.push(candle.low); this.#closes.push(candle.close); if (this.#highs.length < this.getRequiredInputs()) return null; let subValueSum = 0; let absValueSum = 0; const limitIndex = this.#highs.length - 1; for (let j = limitIndex; j > this.interval; j--) { const diffHighs = this.#highs[j] - this.#highs[j - 2]; const diffLows = this.#lows[j] - this.#lows[j - 2]; const n = this.#calculateN(j); const m = this.#calculateM(j); const s = diffHighs + diffLows; const subValue = n * m * s; const absDailyValue = Math.abs(diffHighs) + Math.abs(diffLows); subValueSum += subValue; absValueSum += absDailyValue; } if (absValueSum === 0) return this.setResult(0, replace); const rei = subValueSum / absValueSum * 100; return this.setResult(rei, replace); } }; //#endregion //#region src/momentum/ROC/ROC.ts /** * Rate Of Change Indicator (ROC) * Type: Momentum * * A positive Rate of Change (ROC) signals a high momentum and a positive trend. A decreasing ROC or even negative ROC * indicates a downtrend. * * @see https://www.investopedia.com/terms/r/rateofchange.asp */ var ROC = class extends TrendIndicatorSeries { prices = []; constructor(interval) { super(); this.interval = interval; } getRequiredInputs() { return this.interval; } update(price, replace) { const comparePrice = this.prices.length === this.interval ? this.prices[0] : null; pushUpdate(this.prices, replace, price, this.interval); if (comparePrice !== null) return this.setResult((price - comparePrice) / comparePrice, replace); return null; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isBearish = hasResult && result < 0; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isBearish: return TradingSignal.BEARISH; default: return TradingSignal.BULLISH; } } }; //#endregion //#region src/trend/WSMA/WSMA.ts /** * Wilder's Smoothed Moving Average (WSMA) * Type: Trend * * Developed by John Welles Wilder (Jr.) to help identifying and spotting bullish and bearish trends. Similar to * Exponential Moving Averages with the difference that a smoothing factor of 1/interval is being used, which makes it * respond more slowly to price changes. * * Synonyms: * - Modified Exponential Moving Average (MEMA) * - Smoothed Moving Average (SMMA) * - Welles Wilder's Smoothing (WWS) * * @see https://tlc.thinkorswim.com/center/reference/Tech-Indicators/studies-library/V-Z/WildersSmoothing */ var WSMA = class extends IndicatorSeries { #indicator; #smoothingFactor; constructor(interval) { super(); this.interval = interval; this.#indicator = new SMA(interval); this.#smoothingFactor = 1 / this.interval; } getRequiredInputs() { return this.interval; } update(price, replace) { const sma = this.#indicator.update(price, replace); if (replace && this.previousResult !== void 0) { const smoothed = (price - this.previousResult) * this.#smoothingFactor; return this.setResult(smoothed + this.previousResult, replace); } else if (!replace && this.result !== void 0) { const smoothed = (price - this.result) * this.#smoothingFactor; return this.setResult(smoothed + this.result, replace); } else if (this.result === void 0 && sma !== null) return this.setResult(sma, replace); return null; } }; //#endregion //#region src/momentum/RSI/RSI.ts /** * Relative Strength Index (RSI) * Type: Momentum * * The Relative Strength Index (RSI) is an oscillator that ranges between 0 and 100. The RSI can be used to find trend reversals, i.e. when a downtrend doesn't generate a RSI below 30 and rallies above 70 it could mean that a trend reversal to the upside is taking place. Trend lines and moving averages should be used to validate such statements. * * The RSI is mostly useful in markets that alternate between bullish and bearish movements. * * Interpretation: * A RSI value of 30 or below indicates an oversold condition (not a good time to sell because there is an oversupply). * A RSI value of 70 or above indicates an overbought condition (sell high opportunity because market may correct the price in the near future). * * @see https://en.wikipedia.org/wiki/Relative_strength_index * @see https://www.investopedia.com/terms/r/rsi.asp */ var RSI = class extends TrendIndicatorSeries { #previousPrices = []; #avgGain; #avgLoss; #maxValue = 100; constructor(interval, SmoothingIndicator = WSMA) { super(); this.interval = interval; this.#avgGain = new SmoothingIndicator(this.interval); this.#avgLoss = new SmoothingIndicator(this.interval); } getRequiredInputs() { return this.#avgGain.getRequiredInputs(); } update(price, replace) { pushUpdate(this.#previousPrices, replace, price, this.interval); if (this.#previousPrices.length < 2) return null; const currentPrice = price; const previousPrice = this.#previousPrices[this.#previousPrices.length - 2]; if (currentPrice > previousPrice) { this.#avgLoss.update(0, replace); this.#avgGain.update(price - previousPrice, replace); } else { this.#avgLoss.update(previousPrice - currentPrice, replace); this.#avgGain.update(0, replace); } if (this.#avgGain.isStable) { const avgLoss = this.#avgLoss.getResultOrThrow(); if (avgLoss === 0) return this.setResult(100, replace); const relativeStrength = this.#avgGain.getResultOrThrow() / avgLoss; return this.setResult(this.#maxValue - this.#maxValue / (relativeStrength + 1), replace); } return null; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isOversold = hasResult && result <= 30; const isOverbought = hasResult && result >= 70; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isOversold: return TradingSignal.BEARISH; case isOverbought: return TradingSignal.BULLISH; default: return TradingSignal.SIDEWAYS; } } }; //#endregion //#region src/momentum/STOCH/StochasticOscillator.ts /** * Stochastic Oscillator (STOCH) * Type: Momentum * * The Stochastic Oscillator was developed by George Lane and is range-bound between 0 and 100. The Stochastic * Oscillator attempts to predict price turning points. A value of 80 indicates that the asset is on the verge of being * overbought. By default, a Simple Moving Average (SMA) is used. When the momentum starts to slow down, the Stochastic * Oscillator values start to turn down. In the case of an uptrend, prices tend to make higher highs, and the * settlement price usually tends to be in the upper end of that time period's trading range. * * The stochastic k (%k) values represent the relation between current close to the period's price range (high/low). It * is sometimes referred as the "fast" stochastic period (fastk). The stochastic d (%d) values represent a Moving * Average of the %k values. It is sometimes referred as the "slow" period. * * @see https://en.wikipedia.org/wiki/Stochastic_oscillator * @see https://www.investopedia.com/terms/s/stochasticoscillator.asp * @see https://tulipindicators.org/stoch */ var StochasticOscillator = class extends TechnicalIndicator { candles = []; #periodM; #periodP; #previousResult; /** * @param n The %k period * @param m The %k slowing period * @param p The %d period */ constructor(n, m, p) { super(); this.n = n; this.m = m; this.p = p; this.#periodM = new SMA(m); this.#periodP = new SMA(p); } getRequiredInputs() { return this.n + this.p + 1; } update(candle, replace) { pushUpdate(this.candles, replace, candle, this.n); if (this.candles.length === this.n) { const highest = Math.max(...this.candles.map((candle$1) => candle$1.high)); const lowest = Math.min(...this.candles.map((candle$1) => candle$1.low)); const divisor = highest - lowest; let fastK = (candle.close - lowest) * 100; fastK = fastK / (divisor === 0 ? 1 : divisor); const stochK = this.#periodM.update(fastK, replace); const stochD = stochK && this.#periodP.update(stochK, replace); if (stochK !== null && stochD !== null) { if (replace) this.result = this.#previousResult; this.#previousResult = this.result; return this.result = { stochD, stochK }; } } return null; } calculateSignal(result) { const hasResult = result !== null && result !== void 0; const isOversold = hasResult && result.stochK <= 20; const isOverbought = hasResult && result.stochK >= 80; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isOversold: return TradingSignal.BEARISH; case isOverbought: return TradingSignal.BULLISH; default: return TradingSignal.SIDEWAYS; } } getSignal() { const previousState = this.calculateSignal(this.#previousResult); const state = this.calculateSignal(this.getResult()); return { hasChanged: previousState !== state, state }; } }; //#endregion //#region src/types/Period.ts var Period = class extends TechnicalIndicator { values; /** Highest return value during the current period. */ #highest; /** Lowest return value during the current period. */ #lowest; get highest() { return this.#highest; } get lowest() { return this.#lowest; } constructor(interval) { super(); this.interval = interval; this.values = []; } getRequiredInputs() { return this.interval; } update(value, replace) { pushUpdate(this.values, replace, value, this.interval); if (this.values.length === this.interval) { this.#lowest = Math.min(...this.values); this.#highest = Math.max(...this.values); return this.result = { highest: this.#highest, lowest: this.#lowest }; } return null; } }; //#endregion //#region src/momentum/STOCHRSI/StochasticRSI.ts /** * Stochastic RSI (STOCHRSI) * Type: Momentum * * The Stochastic RSI is an oscillator ranging from 0 to 1 and was developed by Tushar S. Chande and Stanley Kroll. * Compared to the RSI, the Stochastic RSI is much steeper and often resides at the extremes (0 or 1). It can be used to identify short-term trends. * * - A return value of 0.2 or below indicates an oversold condition * - A return value around 0.5 indicates a neutral market * - A return value of 0.8 or above indicates an overbought condition * - Overbought doesn't mean that the price will reverse lower but it shows that the RSI reached an extreme * - Oversold doesn't mean that the price will reverse higher but it shows that the RSI reached an extreme * * The Stochastic RSI is often read through crossovers of its %K (fast) and %D (signal) lines. A %K crossing above %D in the oversold zone (below 20) may suggest a buy, while a %K crossing below %D in the overbought zone (above 80) can signal a potential sell. * * @see https://www.investopedia.com/terms/s/stochrsi.asp * @see https://lakshmishree.com/blog/stochastic-rsi-indicator/ * @see https://alchemymarkets.com/education/indicators/stochastic-rsi/ */ var StochasticRSI = class extends TrendIndicatorSeries { #period; #rsi; constructor(interval, SmoothingRSI = WSMA, smoothing = { d: new SMA(3), k: new SMA(3) }) { super(); this.interval = interval; this.smoothing = smoothing; this.#period = new Period(interval); this.#rsi = new RSI(interval, SmoothingRSI); } getRequiredInputs() { return this.#rsi.getRequiredInputs() + this.#period.getRequiredInputs(); } update(price, replace) { const rsiResult = this.#rsi.update(price, replace); if (rsiResult) { const periodResult = this.#period.update(rsiResult, replace); if (periodResult) { const min = periodResult.lowest; const denominator = periodResult.highest - min; if (denominator === 0) return this.setResult(100, replace); const stochRSI = (rsiResult - min) / denominator; const k = this.smoothing.k.update(stochRSI, replace); if (k) this.smoothing.d.update(k, replace); return this.setResult(stochRSI, replace); } } return null; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isOversold = hasResult && result <= .2; const isOverbought = hasResult && result >= .8; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isOversold: return TradingSignal.BEARISH; case isOverbought: return TradingSignal.BULLISH; default: return TradingSignal.SIDEWAYS; } } }; //#endregion //#region src/momentum/TDS/TDS.ts /** * Tom Demark's Sequential Indicator (TDS) * Type: Momentum * * The TD Sequential indicator is used to identify potential turning points in the price of an asset. * It consists of two phases: TD Setup and TD Countdown. This implementation focuses on the TD Setup phase, * which is the most commonly used part for trend exhaustion signals. * * - A bullish setup occurs when there are 9 consecutive closes greater than the close 4 bars earlier. A possible sell opportunity is when the low of bars 6 and 7 in the count are exceeded by the low of bars 8 or 9. * - A bearish setup occurs when there are 9 consecutive closes less than the close 4 bars earlier. A possible buy opportunity is when the low of bars 6 and 7 in the count are exceeded by the low of bars 8 or 9. * * @see https://github.com/bennycode/trading-signals/discussions/239 * @see https://hackernoon.com/how-to-buy-sell-cryptocurrency-with-number-indicator-td-sequential-5af46f0ebce1 * @see https://practicaltechnicalanalysis.blogspot.com/2013/01/tom-demark-sequential.html */ var TDS = class extends TrendIndicatorSeries { closes = []; setupCount = 0; setupDirection = null; getRequiredInputs() { return 9; } update(close, replace) { if (replace) this.closes.pop(); this.closes.push(close); if (this.closes.length < 5) return null; if (this.closes.length > 13) this.closes.shift(); const index = this.closes.length - 1; const prev4 = this.closes[index - 4]; if (close > prev4) if (this.setupDirection === "bearish") { this.setupCount = 1; this.setupDirection = "bullish"; } else { this.setupCount++; this.setupDirection = "bullish"; } else if (close < prev4) if (this.setupDirection === "bullish") { this.setupCount = 1; this.setupDirection = "bearish"; } else { this.setupCount++; this.setupDirection = "bearish"; } if (this.setupCount >= this.getRequiredInputs()) { const result = this.setupDirection === "bullish" ? 1 : -1; this.setupCount = 0; this.setupDirection = null; return this.setResult(result, replace); } return null; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isOverbought = hasResult && result === 1; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isOverbought: return TradingSignal.BULLISH; default: return TradingSignal.BEARISH; } } }; //#endregion //#region src/momentum/WILLR/WilliamsR.ts /** * Williams %R (Williams Percent Range) * Type: Momentum * * The Williams %R indicator, developed by Larry Williams, is a momentum indicator that measures overbought * and oversold levels. It is similar to the Stochastic Oscillator but is plotted on an inverted scale, * ranging from 0 to -100. Readings from 0 to -20 are considered overbought, while readings from -80 to -100 * are considered oversold. * * The Williams %R is arithmetically exactly equivalent to the %K stochastic oscillator, mirrored at the 0%-line. * * Formula: %R = (Highest High - Close) / (Highest High - Lowest Low) × -100 * * @see https://en.wikipedia.org/wiki/Williams_%25R * @see https://www.investopedia.com/terms/w/williamsr.asp */ var WilliamsR = class extends TrendIndicatorSeries { candles = []; constructor(interval) { super(); this.interval = interval; } getRequiredInputs() { return this.interval; } update(candle, replace) { pushUpdate(this.candles, replace, candle, this.interval); if (this.candles.length === this.interval) { let highest = this.candles[0].high; let lowest = this.candles[0].low; for (let i = 1; i < this.candles.length; i++) { if (this.candles[i].high > highest) highest = this.candles[i].high; if (this.candles[i].low < lowest) lowest = this.candles[i].low; } const divisor = highest - lowest; if (divisor === 0) return this.setResult(-100, replace); const willR = (highest - candle.close) / divisor * -100; return this.setResult(willR, replace); } return null; } calculateSignalState(result) { const hasResult = result !== null && result !== void 0; const isOverbought = hasResult && result >= -20; const isOversold = hasResult && result <= -80; switch (true) { case !hasResult: return TradingSignal.UNKNOWN; case isOverbought: return TradingSignal.BULLISH; case isOversold: return TradingSignal.BEARISH; default: return TradingSignal.SIDEWAYS; } } }; //#endregion //#region src/volatility/TR/TR.ts /** * True Range (TR) * Type: Volatility * * The True Range (TR) was developed by John Welles Wilder (Jr.). The range (R) is a candle's highest price minus it's lowest price. The true range extends it to yesterday's closing price if it was outside of the current range. * * Low return values indicate a sideways trend with little volatility. * * @see https://www.linnsoft.com/techind/true-range-tr */ var TR = class extends IndicatorSeries { #previousCandle; #twoPreviousCandle; getRequiredInputs() { return 2; } update(candle, replace) { const { high, low } = candle; const highLow = high - low; if (this.#previousCandle && replace) this.#previousCandle = this.#twoPreviousCandle; if (this.#previousCandle) { const highClose = Math.abs(high - this.#previousCandle.close); const lowClose = Math.abs(low - this.#previousCandle.close); this.#twoPreviousCandle = this.#previousCandle; this.#previousCandle = candle; return this.setResult(Math.max(highLow, highClose, lowClose), replace); } this.#twoPreviousCandle = this.#previousCandle; this.#previousCandle = candle; return this.setResult(highLow, replace); } }; //#endregion //#region src/volatility/ATR/ATR.ts /** * Average True Range (ATR) * Type: Volatility * * The ATR was developed by John Welles Wilder (Jr.). The idea of ranges is that they show the commitment or enthusiasm of traders. Large or increasing ranges suggest traders prepared to continue to bid up or sell down a stock through the course of the day. Decreasing range indicates declining interest. * * A stock with a higher ATR is indicative of increased volatility, while a lower ATR suggests decreased volatility during the assessed time frame. * * - Low ATR (e.g., 0.5 to 1): Typically associated with low-volatility stocks or markets. Prices tend to move in a relatively calm and steady manner. * * - Moderate ATR (e.g., 1 to 2): Indicates moderate volatility. Prices may experience periodic fluctuations, but they are not extreme. Many traders find stocks with ATR around 2 to be suitable for trading with manageable risk. * * - High ATR (e.g., 2 or higher): Suggests higher volatility. Stocks with ATR values greater than 2 are prone to more significant price swings, and they may exhibit larger price movements. * * @see https://www.investopedia.com/terms/a/atr.asp */ var ATR = class extends IndicatorSeries { #tr; #smoothing; constructor(interval, SmoothingIndicator = WSMA) { super(); this.interval = interval; this.#tr = new TR(); this.#smoothing = new SmoothingIndicator(interval); } getRequiredInputs() { return this.#smoothing.getRequiredInputs(); } update(candle, replace) { const trueRange = this.#tr.update(candle, replace); this.#smoothing.update(trueRange, replace); if (this.#smoothing.isStable) return this.setResult(this.#smoothing.getResultOrThrow(), replace); return null; } }; //#endregion //#region src/trend/DX/DX.ts /** * Directional Movement Index (DMI / DX) * Type: Trend (using +DI & -DI) * * The DX was developed by John Welles Wilder (Jr.). and may help traders assess the strength of a trend (momentum) * and direction of the trend. * * If there is no change in the trend, then the DX is `0`. The return value increases when there is a stronger trend * (either negative or positive). To detect if the trend is bullish or bearish you have to compare +DI and -DI. When * +DI is above -DI, then there is more upward pressure than downward pressure in the market. * * @see https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/dmi */ var DX = class extends IndicatorSeries { #movesUp; #movesDown; #previousCandle; #secondLastCandle; #atr; mdi; pdi; constructor(interval, SmoothingIndicator = WSMA) { super(); this.interval = interval; this.#atr = new ATR(this.interval, SmoothingIndicator); this.#movesDown = new SmoothingIndicator(this.interval); this.#movesUp = new SmoothingIndicator(this.interval); } #updateState(candle, pdm, mdm, replace) { this.#atr.update(candle, replace); this.#movesUp.update(pdm, replace); this.#movesDown.update(mdm, replace); if (this.#previousCandle) this.#secondLastCandle = this.#previousCandle; this.#previousCandle = candle; } getRequiredInputs() { return this.#movesUp.getRequiredInputs(); } update(candle, replace) { if (!this.#previousCandle) { this.#updateState(candle, 0, 0, replace); return null; } if (this.#secondLastCandle && replace) this.#previousCandle = this.#secondLastCandle; const currentHigh = candle.high; const previousHigh = this.#previousCandle.high; const currentLow = candle.low; const previousLow = this.#previousCandle.low; const higherHigh = currentHigh - previousHigh; const lowerLow = previousLow - currentLow; const pdm = higherHigh < 0 || higherHigh < lowerLow ? 0 : higherHigh; const mdm = lowerLow < 0 || lowerLow < higherHigh ? 0 : lowerLow; this.#updateState(candle, pdm, mdm, replace); if (this.#movesUp.isStable) { this.pdi = this.#movesUp.getResultOrThrow() / this.#atr.getResultOrThrow(); this.mdi = this.#movesDown.getResultOrThrow() / this.#atr.getResultOrThrow(); const dmDiff = Math.abs(this.pdi - this.mdi); const dmSum = this.pdi + this.mdi; if (dmSum === 0) return this.setResult(0, replace); return this.setResult(dmDiff / dmSum * 100, replace); } return null; } }; //#endregion //#region src/trend/ADX/ADX.ts /** * Average Directional Index (ADX) * Type: Trend (using +DI & -DI), Volatility * * The ADX was developed by John Welles Wilder (Jr.). It is a lagging indicator; that is, a * trend must have established itself before the ADX will generate a signal that a trend is under way. * * ADX will range between 0 and 100 which makes it an oscillator. It is a smoothed average of the Directional Movement * Index (DMI / DX). * * Generally, ADX readings below 20 indicate trend weakness, and readings above 40 indicate trend strength. * A strong trend is indicated by readings above 50. ADX values of 75-100 signal an extremely strong trend. * * Interpretation: * If ADX increases, it means that volatility is increasing and indicating the beginning of a new trend. * If ADX decreases, it means that volatility is decreasing, and the current trend is slowing down and may even * reverse. * When +DI is above -DI, then there is more upward pressure than downward pressure in the market. * * Note: * The ADX calculation relies on the DX becoming stable before producing meaningful results. * For an interval of 5, at least 9 candles are required. The first 5 candles are used to stabilize the DX, which then generates the initial ADX value. * The subsequent 4 candles produce additional ADX values, allowing it to stabilize with 5 values for an interval of 5. * * @see https://www.investopedia.com/terms/a/adx.asp * @see https://en.wikipedia.org/wiki/Average_directional_movement_index * @see https://paperswithbacktest.com/wiki/wilders-adx-dmi-indicator-calculation-method * @see https://www.youtube.com/watch?v=n2J1H3NeF70 * @see https://learn.tradimo.com/technical-analysis-how-to-work-with-indicators/adx-determing-the-strength-of-price-movement * @see https://medium.com/codex/algorithmic-trading-with-average-directional-index-in-python-2b5a20ecf06a */ var ADX = class extends IndicatorSeries { #dx; #smoothed; constructor(interval, SmoothingIndicator = WSMA) { super(); this.interval = interval; this.#smoothed = new SmoothingIndicator(this.interval); this.#dx = new DX(interval, SmoothingIndicator); } get mdi() { return this.#dx.mdi; } get pdi() { return this.#dx.pdi; } getRequiredInputs() { return this.interval * 2 - 1; } update(candle, replace) { const result = this.#dx.update(candle, replace); if (result !== null) this.#smoothed.update(result, repla