From a46e531b3cf38412bdb602a8181b825c106aad3a Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Fri, 19 Nov 2021 02:28:54 +0100 Subject: [PATCH 1/3] refactor(ADX): Return direct result and +DI & -DI only via getters --- .eslintrc.json | 14 ---- src/ADX/ADX.test.ts | 81 +++++++++----------- src/ADX/ADX.ts | 178 +++++++------------------------------------- src/DX/DX.test.ts | 30 ++++---- src/DX/DX.ts | 24 +++--- 5 files changed, 90 insertions(+), 237 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 667777401..f61e27ff5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,20 +18,6 @@ "@typescript-eslint/array-type": "error", "@typescript-eslint/consistent-type-assertions": "error", "@typescript-eslint/explicit-function-return-type": "error", - "@typescript-eslint/member-ordering": [ - "error", - { - "default": [ - "public-instance-field", - "private-instance-field", - "public-constructor", - "private-constructor", - "public-instance-method", - "protected-instance-method", - "private-instance-method" - ] - } - ], "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-this-alias": "error", diff --git a/src/ADX/ADX.test.ts b/src/ADX/ADX.test.ts index 252813cfa..6be4a7efb 100644 --- a/src/ADX/ADX.test.ts +++ b/src/ADX/ADX.test.ts @@ -1,59 +1,48 @@ import {ADX} from './ADX'; -import {NotEnoughDataError} from '..'; describe('ADX', () => { describe('getResult', () => { - it('throws an error when there is not enough input data', () => { - const adx = new ADX(14); + it('calculates the Average Directional Index (ADX)', () => { + // Test data verified with: + // https://tulipindicators.org/adx + 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}, + ]; - try { - adx.getResult(); - fail('Expected error'); - } catch (error) { - expect(adx.isStable).toBeFalse(); - expect(error).toBeInstanceOf(NotEnoughDataError); - } - }); - - it('returns the directional indicators (+DI & -DI)', () => { - /** - * Test data from: - * https://github.com/anandanand84/technicalindicators/blob/v3.1.0/test/directionalmovement/ADX.js#L23-L28 - */ - const data = { - close: [ - 29.87, 30.24, 30.1, 28.9, 28.92, 28.48, 28.56, 27.56, 28.47, 28.28, 27.49, 27.23, 26.35, 26.33, 27.03, 26.22, - 26.01, 25.46, 27.03, 27.45, 28.36, 28.43, 27.95, 29.01, 29.38, 29.36, 28.91, 30.61, 30.05, 30.19, 31.12, - 30.54, 29.78, 30.04, 30.49, 31.47, 32.05, 31.97, 31.13, 31.66, 32.64, 32.59, 32.19, 32.1, 32.93, 33.0, 31.94, - ], - high: [ - 30.2, 30.28, 30.45, 29.35, 29.35, 29.29, 28.83, 28.73, 28.67, 28.85, 28.64, 27.68, 27.21, 26.87, 27.41, 26.94, - 26.52, 26.52, 27.09, 27.69, 28.45, 28.53, 28.67, 29.01, 29.87, 29.8, 29.75, 30.65, 30.6, 30.76, 31.17, 30.89, - 30.04, 30.66, 30.6, 31.97, 32.1, 32.03, 31.63, 31.85, 32.71, 32.76, 32.58, 32.13, 33.12, 33.19, 32.52, - ], - low: [ - 29.41, 29.32, 29.96, 28.74, 28.56, 28.41, 28.08, 27.43, 27.66, 27.83, 27.4, 27.09, 26.18, 26.13, 26.63, 26.13, - 25.43, 25.35, 25.88, 26.96, 27.14, 28.01, 27.88, 27.99, 28.76, 29.14, 28.71, 28.93, 30.03, 29.39, 30.14, - 30.43, 29.35, 29.99, 29.52, 30.94, 31.54, 31.36, 30.92, 31.2, 32.13, 32.23, 31.97, 31.56, 32.21, 32.63, 31.76, - ], - }; + const expectations = [41.38, 44.29, 49.42, 54.92, 59.99, 65.29, 67.36]; - const adx = new ADX(14); + const adx = new ADX(5); - for (let i = 0; i < Object.keys(data.low).length; i++) { - adx.update({ - close: data.close[i], - high: data.high[i], - low: data.low[i], - }); + for (const candle of candles) { + adx.update(candle); + if (adx.isStable) { + const expected = expectations.shift(); + expect(adx.getResult().toFixed(2)).toBe(`${expected}`); + } } - /** - * Expectation from: - * https://github.com/anandanand84/technicalindicators/blob/v3.1.0/test/directionalmovement/ADX.js#L128 - */ expect(adx.isStable).toBeTrue(); - expect(adx.getResult().adx.toFixed(3)).toBe('17.288'); + expect(adx.getResult().toFixed(2)).toBe('67.36'); + expect(adx.lowest!.toFixed(2)).toBe('41.38'); + expect(adx.highest!.toFixed(2)).toBe('67.36'); + // Verify uptrend detection (+DI > -DI): + expect(adx.pdi!.gt(adx.mdi!)).toBeTrue(); + expect(adx.pdi!.toFixed(2)).toBe('0.42'); + expect(adx.mdi!.toFixed(2)).toBe('0.06'); }); }); }); diff --git a/src/ADX/ADX.ts b/src/ADX/ADX.ts index d005e93af..e98cb2a94 100644 --- a/src/ADX/ADX.ts +++ b/src/ADX/ADX.ts @@ -1,29 +1,20 @@ import {Big} from 'big.js'; -import {NotEnoughDataError} from '../error'; -import {Indicator} from '../Indicator'; -import {getAverage} from '../util/getAverage'; +import {BigIndicatorSeries} from '../Indicator'; import {MovingAverage} from '../MA/MovingAverage'; -import {ATR} from '../ATR/ATR'; import {HighLowClose} from '../util/HighLowClose'; import {MovingAverageTypes} from '../MA/MovingAverageTypes'; import {WSMA} from '../WSMA/WSMA'; - -export type ADXResult = { - adx: Big; - /** Minus Directional Indicator (-DI) */ - mdi: Big; - /** Plus Directional Indicator (+DI) */ - pdi: Big; -}; +import {DX} from '../DX/DX'; /** * Average Directional Index (ADX) - * Type: Volatility + * Type: Momentum, 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. + * 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. @@ -31,153 +22,38 @@ export type ADXResult = { * 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. * * @see https://www.investopedia.com/terms/a/adx.asp + * @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 */ -export class ADX implements Indicator { - private readonly candles: HighLowClose[] = []; - private readonly atr: ATR; - private readonly smoothedPDM: MovingAverage; - private readonly smoothedMDM: MovingAverage; - private readonly dxValues: Big[] = []; - private prevCandle: HighLowClose | undefined; - private adx: Big | undefined; - private pdi: Big = new Big(0); - private mdi: Big = new Big(0); - - constructor(public interval: number, SmoothingIndicator: MovingAverageTypes = WSMA) { - this.atr = new ATR(interval, SmoothingIndicator); - this.smoothedPDM = new SmoothingIndicator(interval); - this.smoothedMDM = new SmoothingIndicator(interval); +export class ADX extends BigIndicatorSeries { + private readonly dx: DX; + private readonly adx: MovingAverage; + + constructor(public readonly interval: number, SmoothingIndicator: MovingAverageTypes = WSMA) { + super(); + this.dx = new DX(interval, SmoothingIndicator); + this.adx = new SmoothingIndicator(this.interval); } - get isStable(): boolean { - return this.dxValues.length >= this.interval; + get mdi(): Big | void { + return this.dx.mdi; } - update(candle: HighLowClose): void { - this.candles.push(candle); - const atrResult = this.atr.update(candle); - - if (!this.prevCandle) { - this.prevCandle = candle; - return; - } - - /** - * Plus Directional Movement (+DM) and - * Minus Directional Movement (-DM) - * for this period. - */ - const {mdm, pdm} = this.directionalMovement(this.prevCandle, candle); - - // Smooth these periodic values: - this.smoothedMDM.update(mdm); - this.smoothedPDM.update(pdm); - - // Previous candle isn't needed anymore therefore we can update it for the next iteration: - this.prevCandle = candle; - - if (this.candles.length <= this.interval) { - return; - } - - /** - * Divide the smoothed Plus Directional Movement (+DM) - * by the smoothed True Range (ATR) to find the Plus Directional Indicator (+DI). - * Multiply by 100 to move the decimal point two places. - * - * This is the green Plus Directional Indicator line (+DI) when plotting. - */ - this.pdi = this.smoothedPDM.getResult().div(atrResult!).times(100); - - /** - * Divide the smoothed Minus Directional Movement (-DM) - * by the smoothed True Range (ATR) to find the Minus Directional Indicator (-DI). - * Multiply by 100 to move the decimal point two places. - * - * This is the red Minus Directional Indicator line (-DI) when plotting. - */ - this.mdi = this.smoothedMDM.getResult().div(atrResult!).times(100); - - /** - * The Directional Movement Index (DX) equals - * the absolute value of +DI less -DI - * divided by the sum of +DI and -DI. - * - * Multiply by 100 to move the decimal point two places. - */ - const dx = this.pdi.sub(this.mdi).abs().div(this.pdi.add(this.mdi)).times(100); - - /** - * The dx values only really have to be kept for the very first ADX calculation - */ - this.dxValues.push(dx); - if (this.dxValues.length > this.interval) { - this.dxValues.shift(); - } - - if (this.dxValues.length < this.interval) { - /** - * ADX can only be calculated once dx values have been calculated. - * This means the ADX needs * 2 candles before being able to give any results. - */ - return; - } - - if (!this.adx) { - /** - * The first ADX value is simply a average of DX. - */ - this.adx = getAverage(this.dxValues); - return; - } - - /** - * Subsequent ADX values are smoothed by multiplying - * the previous ADX value by , - * adding the most recent DX value, - * and dividing this total by . - */ - this.adx = this.adx - .times(this.interval - 1) - .add(dx) - .div(this.interval); + get pdi(): Big | void { + return this.dx.pdi; } - getResult(): ADXResult { - if (!this.adx) { - throw new NotEnoughDataError(); + update(candle: HighLowClose): Big | void { + const result = this.dx.update(candle); + if (result) { + this.adx.update(result); + } + if (this.adx.isStable) { + return this.setResult(this.adx.getResult()); } - return { - adx: this.adx, - mdi: this.mdi, - pdi: this.pdi, - }; - } - - private directionalMovement(prevCandle: HighLowClose, currentCandle: HighLowClose): {mdm: Big; pdm: Big} { - const currentHigh = new Big(currentCandle.high); - const lastHigh = new Big(prevCandle.high); - - const currentLow = new Big(currentCandle.low); - const lastLow = new Big(prevCandle.low); - - const upMove = currentHigh.sub(lastHigh); - const downMove = lastLow.sub(currentLow); - - return { - /** - * If the down-move is greater than the up-move and greater than zero, - * the -DM equals the down-move; otherwise, it equals zero. - */ - mdm: downMove.gt(upMove) && downMove.gt(new Big(0)) ? downMove : new Big(0), - /** - * If the up-move is greater than the down-move and greater than zero, - * the +DM equals the up-move; otherwise, it equals zero. - */ - pdm: upMove.gt(downMove) && upMove.gt(new Big(0)) ? upMove : new Big(0), - }; } } diff --git a/src/DX/DX.test.ts b/src/DX/DX.test.ts index d907029b6..4c32af6fa 100644 --- a/src/DX/DX.test.ts +++ b/src/DX/DX.test.ts @@ -6,21 +6,21 @@ describe('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}, + {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 = [ diff --git a/src/DX/DX.ts b/src/DX/DX.ts index 2ba8f8d05..3b52c7cd7 100644 --- a/src/DX/DX.ts +++ b/src/DX/DX.ts @@ -8,14 +8,14 @@ import {ATR} from '../ATR/ATR'; /** * Directional Movement Index (DMI / DX) - * Type: Momentum + * Type: Momentum, Trend (using +DI & -DI) * - * 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. + * 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). When +DI is above -DI, then there is more upward pressure than downward pressure in - * the market. + * (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 */ @@ -24,6 +24,10 @@ export class DX extends BigIndicatorSeries { private readonly movesDown: MovingAverage; private previousCandle?: HighLowClose; private readonly atr: ATR; + /** Minus Directional Indicator (-DI) */ + public mdi?: Big; + /** Plus Directional Indicator (+DI) */ + public pdi?: Big; constructor(public readonly interval: number, SmoothingIndicator: MovingAverageTypes = WSMA) { super(); @@ -68,13 +72,11 @@ export class DX extends BigIndicatorSeries { 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()); + this.pdi = this.movesUp.getResult().div(this.atr.getResult()); + this.mdi = this.movesDown.getResult().div(this.atr.getResult()); - const dmDiff = pdi.minus(mdi).abs(); - const dmSum = pdi.plus(mdi); + const dmDiff = this.pdi.minus(this.mdi).abs(); + const dmSum = this.pdi.plus(this.mdi); // Prevent division by zero if (dmSum.eq(0)) { From a2667fba1215a759bd6ffcc65b2107538363723c Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Fri, 19 Nov 2021 02:30:52 +0100 Subject: [PATCH 2/3] benchmark --- src/start/startBenchmark.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/start/startBenchmark.ts b/src/start/startBenchmark.ts index 1078ea640..64656f86e 100644 --- a/src/start/startBenchmark.ts +++ b/src/start/startBenchmark.ts @@ -1,5 +1,6 @@ import Benchmark, {Event} from 'benchmark'; import candles from '../test/fixtures/candles/100-candles.json'; +import {ADX} from '../ADX/ADX'; import {FasterMAD, MAD} from '../MAD/MAD'; import {BollingerBands, FasterBollingerBands} from '../BBANDS/BollingerBands'; import {FasterEMA, EMA} from '../EMA/EMA'; @@ -26,6 +27,12 @@ const highLowCloses: HighLowCloseNumbers[] = candles.map(candle => ({ })); new Benchmark.Suite('Technical Indicators') + .add('ADX', () => { + const adx = new ADX(interval); + for (const candle of highLowCloses) { + adx.update(candle); + } + }) .add('ATR', () => { const atr = new ATR(interval); for (const candle of highLowCloses) { From 82ce337efad72019d96b7cc39cd99eff2df4caba Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Fri, 19 Nov 2021 02:33:51 +0100 Subject: [PATCH 3/3] A-Z --- src/DX/DX.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/DX/DX.test.ts b/src/DX/DX.test.ts index 4c32af6fa..a3c9965b4 100644 --- a/src/DX/DX.test.ts +++ b/src/DX/DX.test.ts @@ -55,11 +55,11 @@ describe('DX', () => { 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}, + {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, high: 100, low: 90}, ]; const dx = new DX(5);