From 2c51d7c0fc35ce3342414b4130edb64d4652e693 Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Fri, 19 Nov 2021 01:06:31 +0100 Subject: [PATCH] feat(DX): Add Directional Movement Index (DX) (#365) --- README.md | 10 +++-- src/ATR/ATR.test.ts | 42 +++++++++--------- src/DX/DX.test.ts | 75 ++++++++++++++++++++++++++++++++ src/DX/DX.ts | 87 +++++++++++++++++++++++++++++++++++++ src/RSI/RSI.ts | 11 +++-- src/index.ts | 1 + src/start/startBenchmark.ts | 7 +++ 7 files changed, 203 insertions(+), 30 deletions(-) create mode 100644 src/DX/DX.test.ts create mode 100644 src/DX/DX.ts diff --git a/README.md b/README.md index ea995f279..0ccf9c19f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ All indicators can be updated over time by streaming data (prices or candles) to - **Accurate.** Indicators with intervals will return a result only when the period is reached. - **Convenient.** Indicators with intervals will save their all-time highs and lows. - **Fast.** If you need high throughput, you can use the included [faster implementations][2]. +- **Flexible.** All advanced indicators support different smoothing overlays (WSMA, etc.). - **Precise.** Better accuracy than calculating with primitive numbers thanks to [big.js][1]. - **Tested.** Code coverage is 100%. No surprises when using it. - **Typed.** Source code is 100% TypeScript. No need to install external typings. @@ -25,10 +26,10 @@ All indicators can be updated over time by streaming data (prices or candles) to ## Technical Indicator Types -- Trend indicators: Measure the direction of a trend +- Trend indicators: Measure the direction of a trend (uptrend, downtrend or sideways trend) - Volume indicators: Measure the strength of a trend (based on volume) -- Volatility indicators: Measure the strength of a trend (based on price) -- Momentum indicators: Measure the speed of price movement +- Volatility indicators: Measure how much disagreement there is in the market based on price (statistical measure of its dispersion) +- Momentum indicators: Measure the strength of a trend (based on price / speed of price movement) ## Supported Technical Indicators @@ -40,6 +41,7 @@ All indicators can be updated over time by streaming data (prices or candles) to 1. Bollinger Bands (BBANDS) 1. Center of Gravity (CG) 1. Commodity Channel Index (CCI) +1. Directional Movement Index (DMI / DX) 1. Double Exponential Moving Average (DEMA) 1. Dual Moving Average (DMA) 1. Exponential Moving Average (EMA) @@ -51,7 +53,7 @@ All indicators can be updated over time by streaming data (prices or candles) to 1. Simple Moving Average (SMA) 1. Stochastic Oscillator (STOCH) 1. True Range (TR) -1. Wilder's Smoothed Moving Average (WSMA) +1. Wilder's Smoothed Moving Average (WSMA / WMA / WWS / SMMA / MEMA) Utility Methods: diff --git a/src/ATR/ATR.test.ts b/src/ATR/ATR.test.ts index 7e2fdee95..740a30ef6 100644 --- a/src/ATR/ATR.test.ts +++ b/src/ATR/ATR.test.ts @@ -2,27 +2,29 @@ import {ATR} from './ATR'; import {NotEnoughDataError} from '..'; describe('ATR', () => { - const candles = [ - {close: 81.59, high: 82.15, low: 81.29}, - {close: 81.06, high: 81.89, low: 80.64}, - {close: 82.87, high: 83.03, low: 81.31}, - {close: 83.0, high: 83.3, low: 82.65}, - {close: 83.61, high: 83.85, low: 83.07}, - {close: 83.15, high: 83.9, low: 83.11}, - {close: 82.84, high: 83.33, low: 82.49}, - {close: 83.99, high: 84.3, low: 82.3}, - {close: 84.55, high: 84.84, low: 84.15}, - {close: 84.36, high: 85.0, low: 84.11}, - {close: 85.53, high: 85.9, low: 84.03}, - {close: 86.54, high: 86.58, low: 85.39}, - {close: 86.89, high: 86.98, low: 85.76}, - {close: 87.77, high: 88.0, low: 87.17}, - {close: 87.29, high: 87.87, low: 87.01}, - ]; - const expectations = ['1.12', '1.05', '1.01', '1.21', '1.14', '1.09', '1.24', '1.23', '1.23', '1.21', '1.14']; - describe('getResult', () => { - it('calculates the ATR with interval 14', () => { + it('calculates the Average True Range (ATR)', () => { + // Test data verified with: + // https://tulipindicators.org/atr + const candles = [ + {close: 81.59, high: 82.15, low: 81.29}, + {close: 81.06, high: 81.89, low: 80.64}, + {close: 82.87, high: 83.03, low: 81.31}, + {close: 83.0, high: 83.3, low: 82.65}, + {close: 83.61, high: 83.85, low: 83.07}, + {close: 83.15, high: 83.9, low: 83.11}, + {close: 82.84, high: 83.33, low: 82.49}, + {close: 83.99, high: 84.3, low: 82.3}, + {close: 84.55, high: 84.84, low: 84.15}, + {close: 84.36, high: 85.0, low: 84.11}, + {close: 85.53, high: 85.9, low: 84.03}, + {close: 86.54, high: 86.58, low: 85.39}, + {close: 86.89, high: 86.98, low: 85.76}, + {close: 87.77, high: 88.0, low: 87.17}, + {close: 87.29, high: 87.87, low: 87.01}, + ]; + const expectations = ['1.12', '1.05', '1.01', '1.21', '1.14', '1.09', '1.24', '1.23', '1.23', '1.21', '1.14']; + const atr = new ATR(5); for (const candle of candles) { atr.update(candle); diff --git a/src/DX/DX.test.ts b/src/DX/DX.test.ts new file mode 100644 index 000000000..d907029b6 --- /dev/null +++ b/src/DX/DX.test.ts @@ -0,0 +1,75 @@ +import {DX} from './DX'; + +describe('DX', () => { + describe('getResult', () => { + it('calculates the Directional Movement Index (DX)', () => { + // Test data verified with: + // https://tulipindicators.org/dx + const candles = [ + {high: 82.15, low: 81.29, close: 81.59}, + {high: 81.89, low: 80.64, close: 81.06}, + {high: 83.03, low: 81.31, close: 82.87}, + {high: 83.3, low: 82.65, close: 83.0}, + {high: 83.85, low: 83.07, close: 83.61}, + {high: 83.9, low: 83.11, close: 83.15}, + {high: 83.33, low: 82.49, close: 82.84}, + {high: 84.3, low: 82.3, close: 83.99}, + {high: 84.84, low: 84.15, close: 84.55}, + {high: 85.0, low: 84.11, close: 84.36}, + {high: 85.9, low: 84.03, close: 85.53}, + {high: 86.58, low: 85.39, close: 86.54}, + {high: 86.98, low: 85.76, close: 86.89}, + {high: 88.0, low: 87.17, close: 87.77}, + {high: 87.87, low: 87.01, close: 87.29}, + ]; + + const expectations = [ + '50.19', + '51.36', + '11.09', + '41.52', + '52.77', + '55.91', + '69.96', + '76.90', // The official TI page has a rounding mistake here + '80.26', + '86.51', + '75.61', + ]; + + const dx = new DX(5); + + for (const candle of candles) { + dx.update(candle); + if (dx.isStable) { + const expected = expectations.shift(); + expect(dx.getResult().toFixed(2)).toBe(expected!); + } + } + + expect(dx.isStable).toBeTrue(); + expect(dx.getResult().toFixed(2)).toBe('75.61'); + expect(dx.lowest!.toFixed(2)).toBe('11.09'); + expect(dx.highest!.toFixed(2)).toBe('86.51'); + }); + + it('returns zero when there is no trend', () => { + const candles = [ + {high: 100, low: 90, close: 95}, + {high: 100, low: 90, close: 95}, + {high: 100, low: 90, close: 95}, + {high: 100, low: 90, close: 95}, + {high: 100, low: 90, close: 95}, + ]; + + const dx = new DX(5); + + for (const candle of candles) { + dx.update(candle); + } + + expect(dx.isStable).toBeTrue(); + expect(dx.getResult().valueOf()).toBe('0'); + }); + }); +}); diff --git a/src/DX/DX.ts b/src/DX/DX.ts new file mode 100644 index 000000000..94ff05cbc --- /dev/null +++ b/src/DX/DX.ts @@ -0,0 +1,87 @@ +import {BigIndicatorSeries} from '../Indicator'; +import {HighLowClose} from '../util'; +import Big from 'big.js'; +import {MovingAverage} from '../MA/MovingAverage'; +import {MovingAverageTypeContext} from '../MA/MovingAverageTypeContext'; +import {WSMA} from '../WSMA/WSMA'; +import {ATR} from '../ATR/ATR'; + +/** + * Directional Movement Index (DMI / DX) + * Type: Momentum + * + * The DX was developed by **John Welles Wilder, Jr.**. and may help traders assess the strength of a trend but NOT the + * 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). 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 + */ +export class DX extends BigIndicatorSeries { + private readonly movesUp: MovingAverage; + private readonly movesDown: MovingAverage; + private previousCandle?: HighLowClose; + private readonly atr: ATR; + + constructor(public readonly interval: number, SmoothingIndicator: MovingAverageTypeContext = WSMA) { + super(); + this.movesUp = new SmoothingIndicator(this.interval); + this.movesDown = new SmoothingIndicator(this.interval); + this.atr = new ATR(this.interval, SmoothingIndicator); + } + + update(candle: HighLowClose): Big | void { + if (!this.previousCandle) { + this.atr.update(candle); + this.movesUp.update(new Big(0)); + this.movesDown.update(new Big(0)); + this.previousCandle = candle; + return; + } + + const currentHigh = new Big(candle.high); + const previousHigh = new Big(this.previousCandle.high); + + const currentLow = new Big(candle.low); + const previousLow = new Big(this.previousCandle.low); + + const higherHigh = currentHigh.minus(previousHigh); + const lowerLow = previousLow.minus(currentLow); + + const noHigherHighs = higherHigh.lt(0); + const lowsRiseFaster = higherHigh.lt(lowerLow); + + // Plus Directional Movement (+DM) + const pdm = noHigherHighs || lowsRiseFaster ? new Big(0) : higherHigh; + + const noLowerLows = lowerLow.lt(0); + const highsRiseFaster = lowerLow.lt(higherHigh); + + // Minus Directional Movement (-DM) + const mdm = noLowerLows || highsRiseFaster ? new Big(0) : lowerLow; + + this.movesUp.update(pdm); + this.movesDown.update(mdm); + this.atr.update(candle); + this.previousCandle = candle; + + if (this.movesUp.isStable) { + // Plus Directional Indicator (+DI) + const pdi = this.movesUp.getResult().div(this.atr.getResult()); + // Minus Directional Indicator (-DI) + const mdi = this.movesDown.getResult().div(this.atr.getResult()); + + const dmDiff = pdi.minus(mdi).abs(); + const dmSum = pdi.plus(mdi); + + // Prevent division by zero + if (dmSum.eq(0)) { + return this.setResult(new Big(0)); + } + + return this.setResult(dmDiff.div(dmSum).mul(100)); + } + } +} diff --git a/src/RSI/RSI.ts b/src/RSI/RSI.ts index a21edb04a..c2b43ae52 100644 --- a/src/RSI/RSI.ts +++ b/src/RSI/RSI.ts @@ -33,22 +33,21 @@ export class RSI extends BigIndicatorSeries { } override update(price: BigSource): void | Big { - const currentClose = new Big(price); - if (!this.previousPrice) { // At least 2 prices are required to do a calculation this.previousPrice = price; return; } - const lastClose = new Big(this.previousPrice); + const currentPrice = new Big(price); + const previousPrice = new Big(this.previousPrice); // Update average gain/loss - if (currentClose.gt(lastClose)) { + if (currentPrice.gt(previousPrice)) { this.avgLoss.update(new Big(0)); // price went up, therefore no loss - this.avgGain.update(currentClose.sub(lastClose)); + this.avgGain.update(currentPrice.sub(previousPrice)); } else { - this.avgLoss.update(lastClose.sub(currentClose)); + this.avgLoss.update(previousPrice.sub(currentPrice)); this.avgGain.update(new Big(0)); // price went down, therefore no gain } diff --git a/src/index.ts b/src/index.ts index cb3541340..3a70a27b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export * from './CCI/CCI'; export * from './CG/CG'; export * from './DEMA/DEMA'; export * from './DMA/DMA'; +export * from './DX/DX'; export * from './EMA/EMA'; export * from './error'; export * from './Indicator'; diff --git a/src/start/startBenchmark.ts b/src/start/startBenchmark.ts index f3ae7f32c..ec8405349 100644 --- a/src/start/startBenchmark.ts +++ b/src/start/startBenchmark.ts @@ -13,6 +13,7 @@ import { import {FasterSMA, SMA} from '../SMA/SMA'; import {CCI, FasterCCI} from '../CCI/CCI'; import {FasterTR, TR} from '../TR/TR'; +import {DX} from '../DX/DX'; const interval = 20; const prices: number[] = candles.map(candle => parseInt(candle.close, 10)); @@ -41,6 +42,12 @@ new Benchmark.Suite('Technical Indicators') cci.update(candle); } }) + .add('DX', () => { + const dx = new DX(interval); + for (const candle of highLowCloses) { + dx.update(candle); + } + }) .add('FasterCCI', () => { const fasterCCI = new FasterCCI(interval); for (const candle of highLowCloses) {