meridianalgo-js
Version:
Advanced algorithmic trading library for Node.js & TypeScript with 100+ technical indicators, pattern recognition, and risk management tools.
803 lines • 34.2 kB
JavaScript
"use strict";
/**
* @fileoverview Technical Indicators Module for MeridianAlgo
* @description Comprehensive collection of technical analysis indicators for quantitative finance
* @author MeridianAlgo
* @version 1.0.0
* @license MIT
*/
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Indicators = exports.IndicatorError = void 0;
/**
* Custom error class for indicator-related errors
* @class IndicatorError
* @extends Error
* @description Thrown when indicator calculations encounter invalid inputs or conditions
* @example
* ```typescript
* try {
* Indicators.sma([], 5);
* } catch (error) {
* if (error instanceof IndicatorError) {
* console.log('Indicator error:', error.message);
* }
* }
* ```
*/
class IndicatorError extends Error {
constructor(message) {
super(message);
this.name = 'IndicatorError';
Object.setPrototypeOf(this, IndicatorError.prototype);
}
}
exports.IndicatorError = IndicatorError;
/**
* Collection of technical analysis indicators with robust error handling and multiple calculation methods.
*
* This class provides a comprehensive suite of technical analysis indicators commonly used in quantitative finance.
* All methods include input validation, error handling, and return consistent data structures.
*
* @class Indicators
* @description Main class containing all technical analysis indicators
* @example
* ```typescript
* import { Indicators } from 'meridianalgo-js';
*
* const prices = [100, 102, 101, 103, 105, 104, 106, 108, 107, 109];
*
* // Calculate Simple Moving Average
* const sma = Indicators.sma(prices, 5);
* console.log('SMA:', sma);
*
* // Calculate RSI
* const rsi = Indicators.rsi(prices, 14);
* console.log('RSI:', rsi);
*
* // Calculate MACD
* const macd = Indicators.macd(prices, 12, 26, 9);
* console.log('MACD:', macd);
* ```
*/
class Indicators {
/**
* Validates input parameters for indicator functions
* @private
*/
static _validateInput(data, period, minPeriod = 1, requireMultiplePoints = true) {
if (!Array.isArray(data)) {
throw new IndicatorError('Input data must be an array of numbers');
}
if (data.length === 0) {
throw new IndicatorError('Input data array cannot be empty');
}
if (data.some(isNaN)) {
throw new IndicatorError('Input data contains invalid (NaN) values');
}
if (typeof period !== 'number' || isNaN(period) || period < minPeriod || !Number.isInteger(period)) {
throw new IndicatorError(`Period must be an integer >= ${minPeriod}`);
}
if (requireMultiplePoints && data.length < period) {
throw new IndicatorError(`Insufficient data points. Need at least ${period} data points, got ${data.length}`);
}
}
/**
* Calculate Simple Moving Average (SMA)
*
* The Simple Moving Average is the average of a security's price over a given time period.
* It's one of the most widely used technical indicators and helps smooth out price data
* to identify trends and potential reversal points.
*
* @param data - Array of price data (typically closing prices)
* @param period - Number of periods to use for calculation (must be positive integer)
* @returns Array of SMA values with same length as input data (padded with NaN for insufficient data)
*
* @throws {IndicatorError} When data is invalid or period is invalid
*
* @example
* ```typescript
* const prices = [100, 102, 101, 103, 105, 104, 106, 108, 107, 109];
* const sma5 = Indicators.sma(prices, 5);
* console.log('5-period SMA:', sma5);
* // Output: [NaN, NaN, NaN, NaN, 102.2, 103, 103.8, 104.6, 105.4, 106.2]
* ```
*
* @see {@link https://www.investopedia.com/terms/s/sma.asp} Simple Moving Average definition
*/
static sma(data, period) {
try {
this._validateInput(data, period, 1, false); // Don't require multiple points
if (data.length < period) {
return [];
}
const result = [];
for (let i = period - 1; i < data.length; i++) {
let sum = 0;
for (let j = i - period + 1; j <= i; j++) {
sum += data[j];
}
result.push(sum / period);
}
return result;
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in SMA calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Exponential Moving Average (EMA)
* @param data - Array of price data
* @param period - Number of periods to use for calculation
* @returns Array of EMA values
*/
static ema(data, period) {
try {
this._validateInput(data, period, 1);
if (data.length === 0)
return [];
const k = 2 / (period + 1);
const ema = [data[0]]; // First value is the same as the first data point
for (let i = 1; i < data.length; i++) {
ema.push(data[i] * k + ema[i - 1] * (1 - k));
}
return ema;
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in EMA calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Weighted Moving Average (WMA)
* @param data - Array of price data
* @param period - Number of periods to use for calculation
* @returns Array of WMA values
*/
static wma(data, period) {
try {
this._validateInput(data, period);
if (data.length < period)
return [];
const result = [];
const weight = period * (period + 1) / 2; // Sum of weights
for (let i = period - 1; i < data.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
sum += data[i - period + 1 + j] * (j + 1);
}
result.push(sum / weight);
}
return result;
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in WMA calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Double Exponential Moving Average (DEMA)
* @param data - Array of price data
* @param period - Number of periods to use for calculation
* @returns Array of DEMA values
*/
static dema(data, period) {
try {
this._validateInput(data, period);
const ema1 = this.ema(data, period);
const ema2 = this.ema(ema1, period);
return ema1.map((val, i) => 2 * val - ema2[i]);
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in DEMA calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Triple Exponential Moving Average (TEMA)
* @param data - Array of price data
* @param period - Number of periods to use for calculation
* @returns Array of TEMA values
*/
static tema(data, period) {
try {
this._validateInput(data, period);
const ema1 = this.ema(data, period);
const ema2 = this.ema(ema1, period);
const ema3 = this.ema(ema2, period);
return ema1.map((val, i) => 3 * val - 3 * ema2[i] + ema3[i]);
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in TEMA calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Kaufman's Adaptive Moving Average (KAMA)
* @param data - Array of price data
* @param period - Number of periods to use for calculation
* @param fast - Fast EMA period (default: 2)
* @param slow - Slow EMA period (default: 30)
* @returns Array of KAMA values
*/
static kama(data, period, fast = 2, slow = 30) {
try {
this._validateInput(data, period);
if (fast >= slow) {
throw new IndicatorError('Fast period must be less than slow period');
}
if (data.length < period)
return [];
const kama = [data[0]]; // Initialize with first price
// Calculate efficiency ratio (ER)
const change = Math.abs(data[data.length - 1] - data[0]);
const volatility = data.slice(1).reduce((sum, val, i) => sum + Math.abs(val - data[i]), 0);
const er = volatility !== 0 ? change / volatility : 0;
// Calculate smoothing constant (SC)
const fastSC = 2 / (fast + 1);
const slowSC = 2 / (slow + 1);
const sc = Math.pow(er * (fastSC - slowSC) + slowSC, 2);
// Calculate KAMA
for (let i = 1; i < data.length; i++) {
kama.push(kama[i - 1] + sc * (data[i] - kama[i - 1]));
}
return kama;
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in KAMA calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate T3 Moving Average
* @param data - Array of price data
* @param period - Number of periods to use for calculation
* @param volumeFactor - Volume factor (default: 0.7)
* @returns Array of T3 values
*/
static t3(data, period, volumeFactor = 0.7) {
try {
this._validateInput(data, period);
if (volumeFactor <= 0 || volumeFactor >= 1) {
throw new IndicatorError('Volume factor must be between 0 and 1');
}
const ema1 = this.ema(data, period);
const ema2 = this.ema(ema1, period);
const ema3 = this.ema(ema2, period);
const ema4 = this.ema(ema3, period);
const ema5 = this.ema(ema4, period);
const ema6 = this.ema(ema5, period);
const c1 = -Math.pow(volumeFactor, 3);
const c2 = 3 * Math.pow(volumeFactor, 2) + 3 * Math.pow(volumeFactor, 3);
const c3 = -6 * Math.pow(volumeFactor, 2) - 3 * volumeFactor - 3 * Math.pow(volumeFactor, 3);
const c4 = 1 + 3 * volumeFactor + Math.pow(volumeFactor, 3) + 3 * Math.pow(volumeFactor, 2);
const t3 = [];
for (let i = 0; i < data.length; i++) {
if (i < 6 * (period - 1)) {
t3.push(NaN);
}
else {
t3.push(c1 * ema6[i] + c2 * ema5[i] + c3 * ema4[i] + c4 * ema3[i]);
}
}
return t3;
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in T3 calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Generic moving average function that supports multiple types
* @param type - Type of moving average ('sma', 'ema', 'wma', 'dema', 'tema', 'kama', 't3')
* @param data - Array of price data
* @param period - Number of periods to use for calculation
* @param args - Additional arguments specific to the moving average type
* @returns Array of moving average values
*/
static movingAverage(type, data, period, ...args) {
switch (type.toLowerCase()) {
case 'sma':
return this.sma(data, period);
case 'ema':
return this.ema(data, period);
case 'wma':
return this.wma(data, period);
case 'dema':
return this.dema(data, period);
case 'tema':
return this.tema(data, period);
case 'kama':
return this.kama(data, period, ...args);
case 't3':
return this.t3(data, period, ...args);
default:
throw new IndicatorError(`Unsupported moving average type: ${type}`);
}
}
/**
* Calculate Relative Strength Index (RSI)
* @param data - Array of price data
* @param period - Number of periods to use for calculation (default: 14)
* @param maType - Type of moving average to use ('sma', 'ema', etc.)
* @returns Array of RSI values
*/
static rsi(data, period = 14, maType = 'ema') {
try {
this._validateInput(data, period, 2);
if (data.length < period + 1)
return [];
const deltas = [];
for (let i = 1; i < data.length; i++) {
deltas.push(data[i] - data[i - 1]);
}
const gains = deltas.map(d => Math.max(0, d));
const losses = deltas.map(d => Math.abs(Math.min(0, d)));
// Use the specified moving average type for smoothing
const avgGain = this.movingAverage(maType, gains, period);
const avgLoss = this.movingAverage(maType, losses, period);
const rsi = [];
for (let i = 0; i < avgGain.length; i++) {
const rs = avgLoss[i] === 0 ? Infinity : avgGain[i] / avgLoss[i];
rsi.push(100 - (100 / (1 + rs)));
}
// Add leading NaN values to match input length
return new Array(data.length - rsi.length).fill(NaN).concat(rsi);
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in RSI calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate MACD (Moving Average Convergence Divergence)
* @param data - Array of price data
* @param fast - Fast EMA period (default: 12)
* @param slow - Slow EMA period (default: 26)
* @param signal - Signal line period (default: 9)
* @param maType - Type of moving average to use ('ema' or 'sma')
* @returns Object containing MACD line, signal line, and histogram
*/
static macd(data, fast = 12, slow = 26, signal = 9, maType = 'ema') {
try {
this._validateInput(data, Math.max(fast, slow, signal));
if (fast >= slow) {
throw new IndicatorError('Fast period must be less than slow period');
}
// Calculate MACD line (fast MA - slow MA)
const fastMA = this.movingAverage(maType, data, fast);
const slowMA = this.movingAverage(maType, data, slow);
// Align the arrays and calculate MACD
const offset = Math.abs(fastMA.length - slowMA.length);
const macd = [];
for (let i = 0; i < Math.min(fastMA.length, slowMA.length); i++) {
macd.push(fastMA[i + (fastMA.length > slowMA.length ? offset : 0)] -
slowMA[i + (slowMA.length > fastMA.length ? offset : 0)]);
}
// Calculate signal line (MA of MACD)
const signalLine = this.movingAverage(maType, macd, signal);
// Calculate histogram (MACD - Signal)
const histogram = [];
const signalOffset = macd.length - signalLine.length;
for (let i = 0; i < signalLine.length; i++) {
histogram.push(macd[i + signalOffset] - signalLine[i]);
}
// Pad the beginning with NaN to match input length
const padLength = data.length - macd.length;
const padArray = (arr) => new Array(padLength).fill(NaN).concat(arr);
return {
macd: padArray(macd),
signal: padArray(signalLine),
histogram: padArray(histogram)
};
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in MACD calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Bollinger Bands
* @param data - Array of price data
* @param period - Number of periods to use for calculation (default: 20)
* @param stdDev - Number of standard deviations for the bands (default: 2)
* @param maType - Type of moving average to use for the middle band
* @returns Object containing upper, middle, and lower band values
*/
static bollingerBands(data, period = 20, stdDev = 2, maType = 'sma') {
try {
this._validateInput(data, period);
if (data.length < period) {
const empty = [];
return { upper: empty, middle: empty, lower: empty };
}
const middle = this.movingAverage(maType, data, period);
const upper = [];
const lower = [];
for (let i = period - 1; i < data.length; i++) {
const slice = data.slice(i - period + 1, i + 1);
const mean = middle[i - period + 1];
const variance = slice.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / period;
const std = Math.sqrt(variance);
upper.push(mean + stdDev * std);
lower.push(mean - stdDev * std);
}
// Pad the beginning with NaN to match input length
const padLength = data.length - upper.length;
const padArray = (arr) => new Array(padLength).fill(NaN).concat(arr);
return {
upper: padArray(upper),
middle: padArray(middle.slice(period - 1)),
lower: padArray(lower)
};
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in Bollinger Bands calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Stochastic Oscillator
* @param high - Array of high prices
* @param low - Array of low prices
* @param close - Array of closing prices
* @param kPeriod - %K period (default: 14)
* @param dPeriod - %D period (default: 3)
* @param smooth - Smoothing period for %K (default: 1)
* @returns Object containing %K and %D values
*/
static stochastic(high, low, close, kPeriod = 14, dPeriod = 3, smooth = 1) {
try {
if (high.length !== low.length || high.length !== close.length) {
throw new IndicatorError('High, low, and close arrays must have the same length');
}
this._validateInput(high, kPeriod);
this._validateInput(high, dPeriod);
if (high.length < kPeriod) {
return { k: [], d: [] };
}
// Calculate %K
const k = [];
for (let i = kPeriod - 1; i < close.length; i++) {
const highSlice = high.slice(i - kPeriod + 1, i + 1);
const lowSlice = low.slice(i - kPeriod + 1, i + 1);
const highestHigh = Math.max(...highSlice);
const lowestLow = Math.min(...lowSlice);
const range = highestHigh - lowestLow;
const currentClose = close[i];
const stochK = range !== 0 ? 100 * ((currentClose - lowestLow) / range) : 0;
k.push(Math.min(100, Math.max(0, stochK))); // Clamp between 0 and 100
}
// Smooth %K if needed
let smoothedK = k;
if (smooth > 1) {
smoothedK = this.sma(k, smooth);
}
// Calculate %D (signal line)
const d = this.sma(smoothedK, dPeriod);
// Pad the beginning with NaN to match input length
const padLength = close.length - smoothedK.length;
const padArray = (arr) => new Array(padLength).fill(NaN).concat(arr);
return {
k: padArray(smoothedK),
d: padArray(d)
};
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in Stochastic Oscillator calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Average True Range (ATR)
* @param high - Array of high prices
* @param low - Array of low prices
* @param close - Array of closing prices
* @param period - Number of periods to use for calculation (default: 14)
* @param maType - Type of moving average to use (default: 'sma')
* @returns Array of ATR values
*/
static atr(high, low, close, period = 14, maType = 'sma') {
try {
if (high.length !== low.length || high.length !== close.length) {
throw new IndicatorError('High, low, and close arrays must have the same length');
}
this._validateInput(high, period);
if (high.length < 2)
return [];
// Calculate True Range (TR)
const tr = [high[0] - low[0]]; // First TR is just high - low
for (let i = 1; i < high.length; i++) {
const tr1 = high[i] - low[i];
const tr2 = Math.abs(high[i] - close[i - 1]);
const tr3 = Math.abs(low[i] - close[i - 1]);
tr.push(Math.max(tr1, tr2, tr3));
}
// Calculate ATR using the specified moving average
return this.movingAverage(maType, tr, period);
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in ATR calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Volume Moving Average
* @param volume - Array of volume values
* @param period - Number of periods to use for calculation (default: 20)
* @param maType - Type of moving average to use (default: 'sma')
* @returns Array of volume moving average values
*/
static volumeMA(volume, period = 20, maType = 'sma') {
try {
this._validateInput(volume, period);
return this.movingAverage(maType, volume, period);
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in Volume MA calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate On-Balance Volume (OBV)
* @param close - Array of closing prices
* @param volume - Array of volume values
* @returns Array of OBV values
*/
static obv(close, volume) {
try {
if (close.length !== volume.length) {
throw new IndicatorError('Close and volume arrays must have the same length');
}
if (close.length === 0)
return [];
const obv = [volume[0]]; // Start with the first volume
for (let i = 1; i < close.length; i++) {
if (close[i] > close[i - 1]) {
// If price went up, add volume
obv.push(obv[i - 1] + volume[i]);
}
else if (close[i] < close[i - 1]) {
// If price went down, subtract volume
obv.push(obv[i - 1] - volume[i]);
}
else {
// If price didn't change, keep the same OBV
obv.push(obv[i - 1]);
}
}
return obv;
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in OBV calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Donchian Channels (Price Channels)
* @param high - Array of high prices
* @param low - Array of low prices
* @param period - Number of periods to use for calculation (default: 20)
* @returns Object containing upper, middle, and lower channel values
*/
static donchianChannels(high, low, period = 20) {
try {
if (high.length !== low.length) {
throw new IndicatorError('High and low arrays must have the same length');
}
this._validateInput(high, period);
if (high.length < period) {
const empty = [];
return { upper: empty, middle: empty, lower: empty };
}
const upper = [];
const lower = [];
for (let i = period - 1; i < high.length; i++) {
const highSlice = high.slice(i - period + 1, i + 1);
const lowSlice = low.slice(i - period + 1, i + 1);
upper.push(Math.max(...highSlice));
lower.push(Math.min(...lowSlice));
}
const middle = upper.map((u, i) => (u + lower[i]) / 2);
// Pad the beginning with NaN to match input length
const padLength = high.length - upper.length;
const padArray = (arr) => new Array(padLength).fill(NaN).concat(arr);
return {
upper: padArray(upper),
middle: padArray(middle),
lower: padArray(lower)
};
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in Donchian Channels calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Williams %R
* @param high - Array of high prices
* @param low - Array of low prices
* @param close - Array of closing prices
* @param period - Number of periods to use for calculation (default: 14)
* @returns Array of Williams %R values
*/
static williamsR(high, low, close, period = 14) {
try {
if (high.length !== low.length || high.length !== close.length) {
throw new IndicatorError('High, low, and close arrays must have the same length');
}
this._validateInput(high, period);
if (high.length < period)
return [];
const result = [];
for (let i = period - 1; i < close.length; i++) {
const highSlice = high.slice(i - period + 1, i + 1);
const lowSlice = low.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...highSlice);
const lowestLow = Math.min(...lowSlice);
const range = highestHigh - lowestLow;
if (range === 0) {
result.push(0); // Avoid division by zero
}
else {
result.push((-100 * (highestHigh - close[i])) / range);
}
}
// Pad the beginning with NaN to match input length
const padLength = close.length - result.length;
return new Array(padLength).fill(NaN).concat(result);
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in Williams %R calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Commodity Channel Index (CCI)
* @param high - Array of high prices
* @param low - Array of low prices
* @param close - Array of closing prices
* @param period - Number of periods to use for calculation (default: 20)
* @returns Array of CCI values
*/
static cci(high, low, close, period = 20) {
try {
if (high.length !== low.length || high.length !== close.length) {
throw new IndicatorError('High, low, and close arrays must have the same length');
}
this._validateInput(high, period);
if (high.length < period)
return [];
// Calculate Typical Price
const typicalPrice = [];
for (let i = 0; i < high.length; i++) {
typicalPrice.push((high[i] + low[i] + close[i]) / 3);
}
// Calculate SMA of Typical Price
const smaTp = this.sma(typicalPrice, period);
// Calculate Mean Deviation
const meanDeviation = [];
for (let i = period - 1; i < typicalPrice.length; i++) {
const slice = typicalPrice.slice(i - period + 1, i + 1);
const mean = smaTp[i - period + 1];
const sumDeviation = slice.reduce((sum, val) => sum + Math.abs(val - mean), 0);
meanDeviation.push(sumDeviation / period);
}
// Calculate CCI
const cci = [];
for (let i = 0; i < meanDeviation.length; i++) {
const tpIndex = i + period - 1;
const deviation = meanDeviation[i];
if (deviation === 0) {
cci.push(0); // Avoid division by zero
}
else {
cci.push((typicalPrice[tpIndex] - smaTp[i]) / (0.015 * deviation));
}
}
// Pad the beginning with NaN to match input length
const padLength = high.length - cci.length;
return new Array(padLength).fill(NaN).concat(cci);
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in CCI calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate Average Directional Index (ADX)
* @param high - Array of high prices
* @param low - Array of low prices
* @param close - Array of closing prices
* @param period - Number of periods to use for calculation (default: 14)
* @returns Object containing ADX, +DI, and -DI values
*/
static adx(high, low, close, period = 14) {
try {
if (high.length !== low.length || high.length !== close.length) {
throw new IndicatorError('High, low, and close arrays must have the same length');
}
this._validateInput(high, period);
if (high.length < period * 2) {
const empty = [];
return { adx: empty, plusDI: empty, minusDI: empty };
}
// Calculate +DM, -DM, and True Range
const plusDM = [0];
const minusDM = [0];
const tr = [high[0] - low[0]]; // First TR is just high - low
for (let i = 1; i < high.length; i++) {
// Calculate directional movements
const upMove = high[i] - high[i - 1];
const downMove = low[i - 1] - low[i];
// +DM is the up move if it's greater than the down move and positive
plusDM.push(upMove > downMove && upMove > 0 ? upMove : 0);
// -DM is the down move if it's greater than the up move and positive
minusDM.push(downMove > upMove && downMove > 0 ? downMove : 0);
// True Range is the greatest of:
// 1. Current High - Current Low
// 2. Current High - Previous Close (absolute value)
// 3. Current Low - Previous Close (absolute value)
const tr1 = high[i] - low[i];
const tr2 = Math.abs(high[i] - close[i - 1]);
const tr3 = Math.abs(low[i] - close[i - 1]);
tr.push(Math.max(tr1, tr2, tr3));
}
// Smooth the DMs and TR
const smoothTR = this.ema(tr, period);
const smoothPlusDM = this.ema(plusDM, period);
const smoothMinusDM = this.ema(minusDM, period);
// Calculate +DI and -DI
const plusDI = [];
const minusDI = [];
for (let i = 0; i < smoothTR.length; i++) {
plusDI.push(smoothTR[i] !== 0 ? (100 * smoothPlusDM[i]) / smoothTR[i] : 0);
minusDI.push(smoothTR[i] !== 0 ? (100 * smoothMinusDM[i]) / smoothTR[i] : 0);
}
// Calculate DX and ADX
const dx = [];
for (let i = 0; i < plusDI.length; i++) {
const sumDI = plusDI[i] + minusDI[i];
const diffDI = Math.abs(plusDI[i] - minusDI[i]);
dx.push(sumDI !== 0 ? (100 * diffDI) / sumDI : 0);
}
const adx = this.ema(dx, period);
// Pad the beginning with NaN to match input length
const padLength = high.length - adx.length;
const padArray = (arr) => new Array(padLength).fill(NaN).concat(arr);
return {
adx: padArray(adx),
plusDI: padArray(plusDI.slice(0, adx.length)),
minusDI: padArray(minusDI.slice(0, adx.length))
};
}
catch (error) {
if (error instanceof IndicatorError)
throw error;
throw new IndicatorError(`Error in ADX calculation: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
exports.Indicators = Indicators;
_a = Indicators;
// Alias for backward compatibility
Indicators.priceChannels = _a.donchianChannels;
//# sourceMappingURL=indicators.js.map