Skip to content

Commit

Permalink
feat(ADX,ATR,RSI): Add option to use EMA or SMA for smoothing results (
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored Sep 1, 2021
1 parent 7cf7784 commit c75f34e
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 46 deletions.
56 changes: 42 additions & 14 deletions src/ADX/ADX.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,59 @@
import {Big} from 'big.js';
import {ADX} from './ADX';

import data from '../test/fixtures/ADX/data.json';
import {NotEnoughDataError} from '..';
import {EMA, NotEnoughDataError} from '..';

const candles = data.candles;
const adx14results = data.interval_14;

describe('ADX', () => {
describe('getResult', () => {
it('calculates the ADX with interval 14', () => {
const indicator = new ADX(14);
it('uses SMMA by default for smoothing the results', () => {
const adx = new ADX(14);
candles.forEach((candle, index) => {
indicator.update(candle);
adx.update(candle);

if (indicator.isStable) {
if (adx.isStable) {
const result = new Big(adx14results[index] || 0);
expect(indicator.getResult().adx.toFixed(4)).toEqual(result.toFixed(4));
expect(adx.getResult().adx.toFixed(4)).toEqual(result.toFixed(4));
}
});
});

it('supports different smoothing indicators', () => {
// Test vectors taken from:
// https://github.com/TulipCharts/tulipindicators/commit/41e59fb33cef5bc97b03d2751dab2b006525c23f
const highs = [
148.115, 148.226, 148.027, 149.603, 149.739, 150.782, 151.247, 151.084, 151.855, 151.874, 150.977, 151.693,
151.877, 151.65, 151.316, 150.525, 150.072, 150.656, 150.669, 149.428, 150.144, 151.386, 151.188, 150.325,
149.625, 149.237, 147.656, 147.188, 147.792,
];
const lows = [
147.734, 147.786, 147.787, 148.026, 149.368, 149.957, 150.467, 150.407, 150.967, 151.003, 150.69, 150.373,
151.312, 151.028, 150.525, 149.725, 149.562, 149.885, 149.122, 149.193, 149.497, 149.887, 149.945, 149.493,
148.715, 148.321, 146.874, 146.813, 147.335,
];
const closes = [
147.846, 148.027, 148.026, 149.603, 149.682, 150.782, 150.469, 151.084, 151.855, 151.003, 150.69, 151.693,
151.312, 151.028, 150.525, 149.725, 150.072, 150.656, 149.122, 149.326, 150.144, 151.386, 150.232, 149.625,
148.888, 148.321, 146.874, 147.166, 147.792,
];
const adx = new ADX(14, EMA);
for (let i = 0; i < Object.keys(highs).length; i++) {
adx.update({
close: closes[i],
high: highs[i],
low: lows[i],
});
}
expect(adx.getResult().adx.toFixed(2)).toBe('20.28');
});

it('throws an error when there is not enough input data', () => {
const indicator = new ADX(14);
const adx = new ADX(14);

try {
indicator.getResult();
adx.getResult();
fail('Expected error');
} catch (error) {
expect(error).toBeInstanceOf(NotEnoughDataError);
Expand Down Expand Up @@ -55,10 +83,10 @@ describe('ADX', () => {
],
};

const indicator = new ADX(14);
const adx = new ADX(14);

for (let i = 0; i < Object.keys(data.low).length; i++) {
indicator.update({
adx.update({
close: data.close[i],
high: data.high[i],
low: data.low[i],
Expand All @@ -69,9 +97,9 @@ describe('ADX', () => {
* Expectation from:
* https://github.com/anandanand84/technicalindicators/blob/v3.1.0/test/directionalmovement/ADX.js#L128-L130
*/
expect(indicator.getResult().adx.toFixed(3)).toBe('17.288');
expect(indicator.getResult().mdi.toFixed(3)).toBe('24.460');
expect(indicator.getResult().pdi.toFixed(3)).toBe('27.420');
expect(adx.getResult().adx.toFixed(3)).toBe('17.288');
expect(adx.getResult().mdi.toFixed(3)).toBe('24.460');
expect(adx.getResult().pdi.toFixed(3)).toBe('27.420');
});
});
});
42 changes: 22 additions & 20 deletions src/ADX/ADX.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Big} from 'big.js';
import {NotEnoughDataError} from '../error';
import {ATR, ATRCandle, SMMA} from '..';
import {ATR, ATRCandle, MovingAverageTypeContext, SMMA} from '..';
import {Indicator} from '../Indicator';
import {getAverage} from '../util/getAverage';
import {MovingAverage} from '../MA/MovingAverage';

export type ADXResult = {
adx: Big;
Expand All @@ -13,31 +14,34 @@ export type ADXResult = {
};

/**
* Average Directional Index
* Average Directional Index (Trend Strength)
*
* The ADX does not indicate trend direction or momentum, only trend strength. 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.
*
* Generally, ADX readings below 20 indicate trend weakness, and readings above 40 indicate trend strength.
* An extremely strong trend is indicated by readings above 50.
* A strong trend is indicated by readings above 50. ADX values of 75-100 signal an extremely strong trend.
*
* @see https://www.investopedia.com/terms/a/adx.asp
* @see https://learn.tradimo.com/technical-analysis-how-to-work-with-indicators/adx-determing-the-strength-of-price-movement
*/
export class ADX implements Indicator<ADXResult> {
private readonly candles: ATRCandle[] = [];
private readonly atr: ATR;
private readonly smoothedPDM: SMMA;
private readonly smoothedMDM: SMMA;
private readonly smoothedPDM: MovingAverage;
private readonly smoothedMDM: MovingAverage;
private readonly dxValues: Big[] = [];
private prevCandle: ATRCandle | undefined;
private adx: Big | undefined;
private pdi: Big = new Big(0);
private mdi: Big = new Big(0);

constructor(public interval: number) {
this.atr = new ATR(interval);
this.smoothedPDM = new SMMA(interval);
this.smoothedMDM = new SMMA(interval);
constructor(public interval: number, SmoothingIndicator: MovingAverageTypeContext = SMMA) {
this.atr = new ATR(interval, SmoothingIndicator);
this.smoothedPDM = new SmoothingIndicator(interval);
this.smoothedMDM = new SmoothingIndicator(interval);
}

get isStable(): boolean {
Expand All @@ -46,7 +50,7 @@ export class ADX implements Indicator<ADXResult> {

update(candle: ATRCandle): void {
this.candles.push(candle);
this.atr.update(candle);
const atrResult = this.atr.update(candle);

if (!this.prevCandle) {
this.prevCandle = candle;
Expand All @@ -60,13 +64,11 @@ export class ADX implements Indicator<ADXResult> {
*/
const {mdm, pdm} = this.directionalMovement(this.prevCandle, candle);

/**
* Smooth these periodic values using SMMA.
*/
// 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.
// Previous candle isn't needed anymore therefore we can update it for the next iteration:
this.prevCandle = candle;

if (this.candles.length <= this.interval) {
Expand All @@ -80,7 +82,7 @@ export class ADX implements Indicator<ADXResult> {
*
* This is the green Plus Directional Indicator line (+DI) when plotting.
*/
this.pdi = this.smoothedPDM.getResult().div(this.atr.getResult()).times(100);
this.pdi = this.smoothedPDM.getResult().div(atrResult!).times(100);

/**
* Divide the smoothed Minus Directional Movement (-DM)
Expand All @@ -89,7 +91,7 @@ export class ADX implements Indicator<ADXResult> {
*
* This is the red Minus Directional Indicator line (-DI) when plotting.
*/
this.mdi = this.smoothedMDM.getResult().div(this.atr.getResult()).times(100);
this.mdi = this.smoothedMDM.getResult().div(atrResult!).times(100);

/**
* The Directional Movement Index (DX) equals
Expand Down Expand Up @@ -159,13 +161,13 @@ export class ADX implements Indicator<ADXResult> {

return {
/**
* If the downmove is greater than the upmove and greater than zero,
* the -DM equals the downmove; otherwise, it equals zero.
* 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 upmove is greater than the downmove and greater than zero,
* the +DM equals the upmove; otherwise, it equals zero.
* 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),
};
Expand Down
6 changes: 3 additions & 3 deletions src/ATR/ATR.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ describe('ATR', () => {
atr.update(candle);

if (atr.isStable) {
const res = new Big(Number(atr14results[index]));
expect(atr.getResult().toFixed(4)).toEqual(res.toFixed(4));
const result = new Big(Number(atr14results[index]));
expect(atr.getResult().toFixed(4)).toEqual(result.toFixed(4));
}
});

expect(atr.lowest!.toFixed(2)).toBe('0.00');
expect(atr.lowest!.toFixed(2)).toBe('0.55');
expect(atr.highest!.toFixed(2)).toBe('1.37');
});

Expand Down
20 changes: 13 additions & 7 deletions src/ATR/ATR.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
import Big, {BigSource} from 'big.js';
import {NotEnoughDataError, SMMA} from '..';
import {SimpleIndicator} from '../Indicator';
import {MovingAverage} from '../MA/MovingAverage';
import {MovingAverageTypeContext} from '../MA/MovingAverageTypeContext';

export type ATRCandle = {close: BigSource; high: BigSource; low: BigSource};

/**
* Average True Range
* Average True Range (Volatility)
*
* The idea of ranges is that they show the commitment or enthusiasm of traders. Large or increasing ranges suggest
* traders prepared to continue to bid up or sell down a stock through the course of the day. Decreasing range suggests
* waning interest.
*
* @see https://www.investopedia.com/terms/a/atr.asp
*/
export class ATR extends SimpleIndicator {
private readonly smma: SMMA;
private readonly candles: ATRCandle[] = [];
private readonly smoothing: MovingAverage;
private prevCandle: ATRCandle | undefined;

constructor(public readonly interval: number) {
constructor(public readonly interval: number, SmoothingIndicator: MovingAverageTypeContext = SMMA) {
super();
this.smma = new SMMA(interval);
this.smoothing = new SmoothingIndicator(interval);
}

override get isStable(): boolean {
return this.candles.length > this.interval;
}

override update(candle: ATRCandle): void {
override update(candle: ATRCandle): Big | void {
this.candles.push(candle);

if (!this.prevCandle) {
Expand All @@ -43,10 +47,12 @@ export class ATR extends SimpleIndicator {

const trueRange = this.trueRange(this.prevCandle, candle);

this.smma.update(trueRange);
this.smoothing.update(trueRange);

this.setResult(this.smma.getResult());
this.prevCandle = candle;
if (this.smoothing.isStable) {
return this.setResult(this.smoothing.getResult());
}
}

override getResult(): Big {
Expand Down
5 changes: 5 additions & 0 deletions src/MA/MovingAverageTypeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {EMA} from '../EMA/EMA';
import {SMA} from '../SMA/SMA';
import {SMMA} from '../SMMA/SMMA';

export type MovingAverageTypeContext = typeof EMA | typeof SMA | typeof SMMA;
4 changes: 2 additions & 2 deletions src/RSI/RSI.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Big, {BigSource} from 'big.js';
import {EMA, NotEnoughDataError, SMMA} from '..';
import {MovingAverageTypeContext, NotEnoughDataError, SMMA} from '..';
import {MovingAverage} from '../MA/MovingAverage';
import {SimpleIndicator} from '../Indicator';

Expand All @@ -8,7 +8,7 @@ export class RSI extends SimpleIndicator {
private readonly avgGain: MovingAverage;
private readonly avgLoss: MovingAverage;

constructor(public readonly interval: number, Indicator: typeof EMA | typeof SMMA = SMMA) {
constructor(public readonly interval: number, Indicator: MovingAverageTypeContext = SMMA) {
super();
this.avgGain = new Indicator(this.interval);
this.avgLoss = new Indicator(this.interval);
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export * from './DMA/DMA';
export * from './EMA/EMA';
export * from './error';
export * from './Indicator';
export * from './MA/MovingAverage';
export * from './MA/MovingAverageTypeContext';
export * from './MACD/MACD';
export * from './MOM/MOM';
export * from './ROC/ROC';
Expand Down

0 comments on commit c75f34e

Please sign in to comment.