Skip to content

Commit

Permalink
refactor(ADX): Return direct result and +DI & -DI only via getters (#368
Browse files Browse the repository at this point in the history
)
  • Loading branch information
bennycode authored Nov 19, 2021
1 parent 65823cb commit 2c0818f
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 242 deletions.
14 changes: 0 additions & 14 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
81 changes: 35 additions & 46 deletions src/ADX/ADX.test.ts
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');
});
});
});
178 changes: 27 additions & 151 deletions src/ADX/ADX.ts
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),
};
}
}
40 changes: 20 additions & 20 deletions src/DX/DX.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 2c0818f

Please sign in to comment.