Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(ADX): Return direct result and +DI & -DI only via getters #368

Merged
merged 3 commits into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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