Skip to content

Commit

Permalink
feat(DX): Add Directional Movement Index (DX) (#365)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored Nov 19, 2021
1 parent 2ecdbd2 commit 2c51d7c
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 30 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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:

Expand Down
42 changes: 22 additions & 20 deletions src/ATR/ATR.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions src/DX/DX.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
87 changes: 87 additions & 0 deletions src/DX/DX.ts
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
11 changes: 5 additions & 6 deletions src/RSI/RSI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 7 additions & 0 deletions src/start/startBenchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 2c51d7c

Please sign in to comment.