generated from bennycode/ts-node-starter
-
-
Notifications
You must be signed in to change notification settings - Fork 96
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(ADX): Return direct result and +DI & -DI only via getters (#368
- Loading branch information
Showing
6 changed files
with
102 additions
and
242 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,183 +1,59 @@ | ||
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. | ||
* | ||
* 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<ADXResult> { | ||
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 <interval> dx values have been calculated. | ||
* This means the ADX needs <interval> * 2 candles before being able to give any results. | ||
*/ | ||
return; | ||
} | ||
|
||
if (!this.adx) { | ||
/** | ||
* The first ADX value is simply a <interval> average of DX. | ||
*/ | ||
this.adx = getAverage(this.dxValues); | ||
return; | ||
} | ||
|
||
/** | ||
* Subsequent ADX values are smoothed by multiplying | ||
* the previous ADX value by <interval - 1>, | ||
* adding the most recent DX value, | ||
* and dividing this total by <interval>. | ||
*/ | ||
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), | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.