From cabc8dd7d242f03b3a524666c793929d5bb4aa9b Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Sat, 6 Feb 2021 20:11:58 +0100 Subject: [PATCH 1/2] feat: Add Acceleration Bands (ABANDS) indicator --- .eslintrc.json | 2 +- README.md | 3 +- src/BANDS/AccelerationBands.test.ts | 73 ++++++++++++++++++++++++ src/BANDS/AccelerationBands.ts | 63 ++++++++++++++++++++ src/BANDS/BandsResult.ts | 7 +++ src/{BB => BANDS}/BollingerBands.test.ts | 0 src/{BB => BANDS}/BollingerBands.ts | 11 +--- src/BANDS/index.ts | 2 + src/MACD/MACD.test.ts | 21 ++++++- src/index.ts | 2 +- 10 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 src/BANDS/AccelerationBands.test.ts create mode 100644 src/BANDS/AccelerationBands.ts create mode 100644 src/BANDS/BandsResult.ts rename src/{BB => BANDS}/BollingerBands.test.ts (100%) rename src/{BB => BANDS}/BollingerBands.ts (88%) create mode 100644 src/BANDS/index.ts diff --git a/.eslintrc.json b/.eslintrc.json index d18438608..667777401 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -35,7 +35,7 @@ "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-this-alias": "error", - "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-unused-vars": ["error", {"argsIgnorePattern": "^_"}], "@typescript-eslint/prefer-for-of": "off", "@typescript-eslint/prefer-readonly": "error", "@typescript-eslint/typedef": "off", diff --git a/README.md b/README.md index 8fb124ee1..19377fbae 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,10 @@ Provide a TypeScript implementation for common technical indicators with arbitra ## Supported Indicators +1. Acceleration Bands (ABANDS) 1. Average Directional Index (ADX) 1. Average True Range (ATR) -1. Bollinger Bands (BB) +1. Bollinger Bands (BBANDS) 1. Double Exponential Moving Average (DEMA) 1. Double Moving Average (DMA) 1. Exponential Moving Average (EMA) diff --git a/src/BANDS/AccelerationBands.test.ts b/src/BANDS/AccelerationBands.test.ts new file mode 100644 index 000000000..34d4f5da7 --- /dev/null +++ b/src/BANDS/AccelerationBands.test.ts @@ -0,0 +1,73 @@ +import {AccelerationBands} from './AccelerationBands'; +import {NotEnoughDataError} from '../error'; +import {SMA} from '../SMA/SMA'; +import {EMA} from '../EMA/EMA'; + +describe('AccelerationBands', () => { + describe('constructor', () => { + it('works with different kinds of indicators', () => { + const accBandsWithSMA = new AccelerationBands(20, 2, SMA); + const accBandsWithEMA = new AccelerationBands(20, 2, EMA); + expect(accBandsWithSMA).toBeDefined(); + expect(accBandsWithEMA).toBeDefined(); + }); + }); + + describe('getResult', () => { + it('returns upper, middle and lower bands', () => { + const accBands = new AccelerationBands(20, 4); + // Test data from: https://github.com/QuantConnect/Lean/blob/master/Tests/TestData/spy_acceleration_bands_20_4.txt + const candles: number[][] = [ + [196.25, 198.05, 194.96, 195.55], + [192.88, 193.86, 191.61, 192.59], + [195.97, 197.61, 195.17, 197.43], + [199.32, 199.47, 194.35, 194.79], + [194.5, 197.22, 194.25, 195.85], + [195.32, 196.82, 194.53, 196.74], + [196.95, 197.01, 195.43, 196.01], + [196.59, 198.99, 195.96, 198.46], + [198.82, 200.41, 198.41, 200.18], + [199.96, 202.89, 199.28, 199.73], + [195.74, 198.68, 194.96, 195.45], + [196.45, 197.68, 195.21, 196.46], + [193.9, 194.46, 192.56, 193.91], + [194.13, 194.67, 192.91, 193.6], + [192.13, 193.45, 190.56, 192.9], + [194.61, 195, 191.81, 192.85], + [191.75, 191.91, 187.64, 188.01], + [188.24, 189.74, 186.93, 188.12], + [190.4, 191.83, 189.44, 191.63], + [192.03, 192.49, 189.82, 192.13], + ]; + + candles.forEach(([_, high, low, close]) => { + accBands.update(high, low, close); + }); + + let result = accBands.getResult(); + + expect(result.lower.toFixed(4)).toBe('187.6891'); + expect(result.middle.toFixed(4)).toBe('194.6195'); + expect(result.upper.toFixed(4)).toBe('201.8016'); + + accBands.update(195.03, 189.12, 195); + + result = accBands.getResult(); + + expect(result.lower.toFixed(4)).toBe('187.1217'); + expect(result.middle.toFixed(4)).toBe('194.5920'); + expect(result.upper.toFixed(4)).toBe('201.9392'); + }); + + it('throws an error when there is not enough input data', () => { + const accBands = new AccelerationBands(20, 2); + + try { + accBands.getResult(); + fail('Expected error'); + } catch (error) { + expect(error).toBeInstanceOf(NotEnoughDataError); + } + }); + }); +}); diff --git a/src/BANDS/AccelerationBands.ts b/src/BANDS/AccelerationBands.ts new file mode 100644 index 000000000..08fb0909c --- /dev/null +++ b/src/BANDS/AccelerationBands.ts @@ -0,0 +1,63 @@ +import {SMA} from '../SMA/SMA'; +import {EMA} from '../EMA/EMA'; +import Big, {BigSource} from 'big.js'; +import {NotEnoughDataError} from '../error'; +import {BandsResult} from './BandsResult'; + +export class AccelerationBands { + private readonly lowerBand: EMA | SMA; + private readonly middleBand: EMA | SMA; + private readonly upperBand: EMA | SMA; + private readonly width: Big; + + /** + * Acceleration Bands + * + * @param period The period of the three moving average (middle, upper and lower band) + * @param width A coefficient specifying the distance between the middle band and upper or lower bands + * @param Indicator Which indicator (EMA, SMA) to use + * + * @see https://www.tradingtechnologies.com/xtrader-help/x-study/technical-indicator-definitions/acceleration-bands-abands/ + * @see https://www.motivewave.com/studies/acceleration_bands.htm + * @see https://github.com/QuantConnect/Lean/blob/master/Indicators/AccelerationBands.cs + * @see https://github.com/twopirllc/pandas-ta/blob/master/pandas_ta/volatility/accbands.py + */ + constructor(period: number, width: number, Indicator: typeof EMA | typeof SMA = SMA) { + this.lowerBand = new Indicator(period); + this.middleBand = new Indicator(period); + this.upperBand = new Indicator(period); + this.width = new Big(width); + } + + get isStable(): boolean { + try { + this.middleBand.getResult(); + return true; + } catch (error) { + return false; + } + } + + update(high: BigSource, low: BigSource, close: BigSource): void { + const coefficient = this.width.mul(new Big(high).minus(low).div(new Big(high).plus(low))); + + // (Low * (1 - 4 * (High - Low)/ (High + Low))) + this.lowerBand.update(new Big(low).mul(new Big(1).minus(coefficient))); + // Close + this.middleBand.update(close); + // (High * ( 1 + 4 * (High - Low) / (High + Low))) + this.upperBand.update(new Big(high).mul(new Big(1).plus(coefficient))); + } + + getResult(): BandsResult { + if (!this.isStable) { + throw new NotEnoughDataError(); + } + + return { + lower: this.lowerBand.getResult(), + middle: this.middleBand.getResult(), + upper: this.upperBand.getResult(), + }; + } +} diff --git a/src/BANDS/BandsResult.ts b/src/BANDS/BandsResult.ts new file mode 100644 index 000000000..e40ef4d93 --- /dev/null +++ b/src/BANDS/BandsResult.ts @@ -0,0 +1,7 @@ +import Big from 'big.js'; + +export interface BandsResult { + lower: Big; + middle: Big; + upper: Big; +} diff --git a/src/BB/BollingerBands.test.ts b/src/BANDS/BollingerBands.test.ts similarity index 100% rename from src/BB/BollingerBands.test.ts rename to src/BANDS/BollingerBands.test.ts diff --git a/src/BB/BollingerBands.ts b/src/BANDS/BollingerBands.ts similarity index 88% rename from src/BB/BollingerBands.ts rename to src/BANDS/BollingerBands.ts index 5bcc743be..2bda20fcc 100644 --- a/src/BB/BollingerBands.ts +++ b/src/BANDS/BollingerBands.ts @@ -2,19 +2,14 @@ import Big, {BigSource} from 'big.js'; import {SMA} from '../SMA/SMA'; import {NotEnoughDataError} from '../error'; import {MathAnalysis} from '../util'; - -export type BollingerBandsResult = { - lower: Big; - middle: Big; - upper: Big; -}; +import {BandsResult} from './BandsResult'; export class BollingerBands { public readonly interval: number; public readonly deviationMultiplier: number; private readonly middleSMA: SMA; private readonly prices: Big[] = []; - private result: BollingerBandsResult | undefined; + private result: BandsResult | undefined; constructor(interval: number = 0, deviationMultiplier: number = 2) { this.interval = interval; @@ -51,7 +46,7 @@ export class BollingerBands { }; } - getResult(): BollingerBandsResult { + getResult(): BandsResult { if (!this.result) { throw new NotEnoughDataError(); } diff --git a/src/BANDS/index.ts b/src/BANDS/index.ts new file mode 100644 index 000000000..7a2f4bf54 --- /dev/null +++ b/src/BANDS/index.ts @@ -0,0 +1,2 @@ +export * from './AccelerationBands'; +export * from './BollingerBands'; diff --git a/src/MACD/MACD.test.ts b/src/MACD/MACD.test.ts index 24dce891c..710b91f0a 100644 --- a/src/MACD/MACD.test.ts +++ b/src/MACD/MACD.test.ts @@ -40,7 +40,12 @@ describe('MACD', () => { '0.98', '0.62', ]; - const indicator = new MACD({longInterval: 5, shortInterval: 2, signalInterval: 9, indicator: EMA}); + const indicator = new MACD({ + indicator: EMA, + longInterval: 5, + shortInterval: 2, + signalInterval: 9, + }); for (const [index, input] of Object.entries(inputs)) { indicator.update(input); @@ -54,7 +59,12 @@ describe('MACD', () => { }); it('throws an error when there is not enough input data', () => { - const macd = new MACD({longInterval: 26, shortInterval: 12, signalInterval: 9, indicator: DEMA}); + const macd = new MACD({ + indicator: DEMA, + longInterval: 26, + shortInterval: 12, + signalInterval: 9, + }); try { macd.getResult(); @@ -68,7 +78,12 @@ describe('MACD', () => { describe('isStable', () => { it('knows when it can return reliable data', () => { const longInterval = 18; - const macd = new MACD({longInterval, shortInterval: 9, signalInterval: 9, indicator: EMA}); + const macd = new MACD({ + indicator: EMA, + longInterval, + shortInterval: 9, + signalInterval: 9, + }); const mockedPrices = [ new Big('0.00019040'), diff --git a/src/index.ts b/src/index.ts index 9e96433aa..085148ab2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export * from './ADX/ADX'; export * from './ATR/ATR'; -export * from './BB/BollingerBands'; +export * from './BANDS'; export * from './DEMA/DEMA'; export * from './DMA/DMA'; export * from './EMA/EMA'; From 528930f885e6fbebe2f4decfb61e411e72c936dd Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Sat, 6 Feb 2021 20:13:05 +0100 Subject: [PATCH 2/2] sort --- src/BANDS/AccelerationBands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BANDS/AccelerationBands.ts b/src/BANDS/AccelerationBands.ts index 08fb0909c..d829603da 100644 --- a/src/BANDS/AccelerationBands.ts +++ b/src/BANDS/AccelerationBands.ts @@ -43,7 +43,7 @@ export class AccelerationBands { // (Low * (1 - 4 * (High - Low)/ (High + Low))) this.lowerBand.update(new Big(low).mul(new Big(1).minus(coefficient))); - // Close + // (Close) this.middleBand.update(close); // (High * ( 1 + 4 * (High - Low) / (High + Low))) this.upperBand.update(new Big(high).mul(new Big(1).plus(coefficient)));