From 38a645de45db1fb2e072c4dfb9df9a124600ef9a Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Sun, 31 May 2020 17:48:40 +0200 Subject: [PATCH] feat: Add Moving Average Convergence Divergence (MACD) indicator (#7) --- README.md | 1 + package.json | 1 + src/DMA/DMA.ts | 4 +- src/MACD/MACD.test.ts | 66 +++++++++++++++ src/MACD/MACD.ts | 80 ++++++++++++++++++ src/index.ts | 1 + src/test/fixtures/MACD/results.json | 125 ++++++++++++++++++++++++++++ 7 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/MACD/MACD.test.ts create mode 100644 src/MACD/MACD.ts create mode 100644 src/test/fixtures/MACD/results.json diff --git a/README.md b/README.md index 2bf0058dd..b238df3cd 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Provide a TypeScript implementation for common technical indicators with arbitra 1. Double Exponential Moving Average (DEMA) 1. Double Moving Average (DMA) 1. Exponential Moving Average (EMA) +1. Moving Average Convergence Divergence (MACD) 1. Rate-of-Change (ROC) 1. Relative Strength Index (RSI) 1. Simple Moving Average (SMA) diff --git a/package.json b/package.json index 056e2d784..b9e70fda4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "ema", "exponential-moving-average", "indicator", + "macd", "moving-average", "roc", "rsi", diff --git a/src/DMA/DMA.ts b/src/DMA/DMA.ts index 9594cec1e..2ca467d48 100644 --- a/src/DMA/DMA.ts +++ b/src/DMA/DMA.ts @@ -1,6 +1,8 @@ import Big, {BigSource} from 'big.js'; import {SMA} from '..'; +export type DMAResult = {long: Big; short: Big}; + export class DMA { public readonly long: SMA; public readonly short: SMA; @@ -17,7 +19,7 @@ export class DMA { this.long.update(price); } - getResult(): {long: Big; short: Big} { + getResult(): DMAResult { return { long: this.long.getResult(), short: this.short.getResult(), diff --git a/src/MACD/MACD.test.ts b/src/MACD/MACD.test.ts new file mode 100644 index 000000000..ded44fad4 --- /dev/null +++ b/src/MACD/MACD.test.ts @@ -0,0 +1,66 @@ +import {MACD} from './MACD'; +import Big from 'big.js'; + +import prices from '../test/fixtures/prices.json'; +import results from '../test/fixtures/MACD/results.json'; + +describe('MACD', () => { + describe('getResult', () => { + it('calculates MACD diff, signal & result with 12/26/9', () => { + const indicator = new MACD({longInterval: 26, shortInterval: 12, signalInterval: 9, useDEMA: false}); + prices.forEach((price, index) => { + indicator.update(new Big(price)); + + const {macd, signal, diff} = indicator.getResult(); + + const resMACD = new Big(results.macd[index]); + const resSignal = new Big(results.signal[index]); + const resDiff = new Big(results.diff[index]); + + expect(macd.toPrecision(12)).toEqual(resMACD.toPrecision(12)); + expect(signal.toPrecision(12)).toEqual(resSignal.toPrecision(12)); + expect(diff.toPrecision(12)).toEqual(resDiff.toPrecision(12)); + }); + }); + + it('throws an error when there is not enough input data', () => { + const indicator = new MACD({longInterval: 26, shortInterval: 12, signalInterval: 9, useDEMA: true}); + expect(() => indicator.getResult()).toThrowError(); + }); + }); + + describe('isStable', () => { + it('knows when it can return reliable data', () => { + const longInterval = 18; + const indicator = new MACD({longInterval, shortInterval: 9, signalInterval: 9, useDEMA: false}); + + const mockedPrices = [ + new Big('0.00019040'), + new Big('0.00019071'), + new Big('0.00019198'), + new Big('0.00019220'), + new Big('0.00019214'), + new Big('0.00019205'), + new Big('0.00019214'), + new Big('0.00019222'), + new Big('0.00019144'), + new Big('0.00019128'), + new Big('0.00019159'), + new Big('0.00019143'), + new Big('0.00019199'), + new Big('0.00019214'), + new Big('0.00019119'), + new Big('0.00019202'), + new Big('0.00019220'), + new Big('0.00019207'), + ]; + + expect(mockedPrices.length).toBe(longInterval); + expect(indicator.isStable()).toBe(false); + + mockedPrices.forEach(price => indicator.update(price)); + + expect(indicator.isStable()).toBe(true); + }); + }); +}); diff --git a/src/MACD/MACD.ts b/src/MACD/MACD.ts new file mode 100644 index 000000000..f01b007c5 --- /dev/null +++ b/src/MACD/MACD.ts @@ -0,0 +1,80 @@ +import {EMA} from '../EMA/EMA'; +import Big, {BigSource} from 'big.js'; +import {DEMA} from '..'; + +export type MACDConfig = { + longInterval: number; + shortInterval: number; + signalInterval: number; + useDEMA: boolean; +}; + +export type MACDResult = { + diff: Big; + macd: Big; + signal: Big; +}; + +export class MACD { + private readonly long: EMA | DEMA; + private readonly short: EMA | DEMA; + private readonly signal: EMA | DEMA; + + private readonly config: any; + private age: number = 0; + + private result: MACDResult | undefined; + + constructor(config: MACDConfig) { + this.config = config; + + this.long = config.useDEMA ? new DEMA(config.longInterval) : new EMA(config.longInterval); + this.short = config.useDEMA ? new DEMA(config.shortInterval) : new EMA(config.shortInterval); + this.signal = config.useDEMA ? new DEMA(config.signalInterval) : new EMA(config.signalInterval); + } + + isStable(): boolean { + return this.age >= this.config.longInterval; + } + + update(_price: BigSource): void { + const price = new Big(_price); + + this.short.update(price); + this.long.update(price); + + const shortEMA = this.short.getResult(); + const longEMA = this.long.getResult(); + + /** + * Standard MACD is the short (usually 12 periods) EMA less the long (usually 26 periods) EMA. Closing prices are + * used to form the moving averages. + */ + const diff = shortEMA.sub(longEMA); + + /** + * A short (usually 9 periods) EMA of MACD is plotted along side to act as a signal line to identify turns in the + * indicator. + */ + this.signal.update(diff); + + /** + * The MACD-Histogram represents the difference between MACD and its 9-day EMA, the signal line. + */ + this.result = { + diff: diff, + macd: diff.sub(this.signal.getResult()), + signal: this.signal.getResult(), + }; + + this.age++; + } + + getResult(): MACDResult { + if (!this.result) { + throw Error('Not enough input data'); + } + + return this.result; + } +} diff --git a/src/index.ts b/src/index.ts index 24f9eb58a..654b55a54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './DEMA/DEMA'; export * from './DMA/DMA'; export * from './EMA/EMA'; +export * from './MACD/MACD'; export * from './ROC/ROC'; export * from './RSI/RSI'; export * from './SMA/SMA'; diff --git a/src/test/fixtures/MACD/results.json b/src/test/fixtures/MACD/results.json new file mode 100644 index 000000000..935ba1976 --- /dev/null +++ b/src/test/fixtures/MACD/results.json @@ -0,0 +1,125 @@ +{ + "macd": [ + 0, + -3.6376068376068362, + -2.4639072734799217, + -5.031340295620034, + -5.521802745494824, + -6.076152724792792, + -3.053848272963074, + 0.33568310636747434, + 2.9473056543174314, + -1.7093100949105917, + 0.8774837070937913, + 2.1231125756183227, + 2.585623647541957, + -1.5571775822256484, + 1.0154188470421683, + 1.8525149219271597, + -0.42461879714695083, + 0.3167899361939739, + -0.7270321370494885, + -3.3439676735158663, + -4.504658691176687, + -4.107132512783021, + -1.6595236920538383, + -1.7529125979171347, + 0.7180893397975616, + 1.8572538942113148, + -0.07210193668202258, + 3.4517711329096086, + 5.350885446717413, + 6.247517316069724, + 7.3480176240854345, + 1.5457357005888681, + 2.1198449676882127, + -2.0235768893083987, + -2.839360636962912, + -4.3281862834160645, + 0.2946340051329588, + 1.6766181261237905, + -1.2825765539962966 + ], + "signal": [ + -0.00000000000000162, + -0.9094017094017091, + -1.5253785277716898, + -2.7832136016766986, + -4.163664288050406, + -5.6827024692486034, + -6.446164537489373, + -6.362243760897504, + -5.625417347318147, + -6.052744871045796, + -5.833373944272349, + -5.30259580036777, + -4.65618988848228, + -5.045484284038693, + -4.791629572278152, + -4.328500841796362, + -4.4346555410831, + -4.355458057034607, + -4.537216091296979, + -5.373208009675945, + -6.499372682470117, + -7.526155810665873, + -7.941036733679333, + -8.379264883158616, + -8.199742548209226, + -7.735429074656398, + -7.753454558826904, + -6.890511775599502, + -5.55279041392015, + -3.990911084902719, + -2.153906678881361, + -1.767472753734144, + -1.2375115118120907, + -1.7434057341391904, + -2.4532458933799184, + -3.535292464233935, + -3.4616339629506956, + -3.0424794314197485, + -3.363123569918823 + ], + "diff": [ + -0.00000000000000162, + -4.547008547008545, + -3.9892858012516115, + -7.814553897296733, + -9.68546703354523, + -11.758855194041395, + -9.500012810452446, + -6.02656065453003, + -2.678111693000716, + -7.762054965956388, + -4.955890237178558, + -3.179483224749447, + -2.070566240940323, + -6.6026618662643415, + -3.776210725235984, + -2.4759859198692027, + -4.859274338230051, + -4.038668120840633, + -5.264248228346467, + -8.717175683191812, + -11.004031373646804, + -11.633288323448895, + -9.600560425733171, + -10.13217748107575, + -7.4816532084116645, + -5.8781751804450835, + -7.825556495508927, + -3.4387406426898934, + -0.20190496720273643, + 2.256606231167005, + 5.1941109452040735, + -0.2217370531452758, + 0.8823334558761218, + -3.766982623447589, + -5.29260653034283, + -7.863478747649999, + -3.166999957817737, + -1.365861305295958, + -4.64570012391512 + ] +}