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

feat: Add Acceleration Bands (ABANDS) indicator #175

Merged
merged 2 commits into from
Feb 6, 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
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
73 changes: 73 additions & 0 deletions src/BANDS/AccelerationBands.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
});
63 changes: 63 additions & 0 deletions src/BANDS/AccelerationBands.ts
Original file line number Diff line number Diff line change
@@ -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(),
};
}
}
7 changes: 7 additions & 0 deletions src/BANDS/BandsResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Big from 'big.js';

export interface BandsResult {
lower: Big;
middle: Big;
upper: Big;
}
File renamed without changes.
11 changes: 3 additions & 8 deletions src/BB/BollingerBands.ts → src/BANDS/BollingerBands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,7 +46,7 @@ export class BollingerBands {
};
}

getResult(): BollingerBandsResult {
getResult(): BandsResult {
if (!this.result) {
throw new NotEnoughDataError();
}
Expand Down
2 changes: 2 additions & 0 deletions src/BANDS/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './AccelerationBands';
export * from './BollingerBands';
21 changes: 18 additions & 3 deletions src/MACD/MACD.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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'),
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down