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

fix(STOCH,SMA,MAD,ABANDS): Prevent division by zero mistakes #380

Merged
merged 1 commit into from
Dec 20, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ All indicators can be updated over time by streaming data (prices or candles) to
- **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].
- **Robust.** Checked against common division by zero mistakes.
- **Tested.** Code coverage is 100%. No surprises when using it.
- **Typed.** Source code is 100% TypeScript. No need to install external typings.
- **Verified.** All results are verified with [other libraries](#alternatives) to guarantee correctness.
Expand Down
22 changes: 22 additions & 0 deletions src/ABANDS/AccelerationBands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,26 @@ describe('AccelerationBands', () => {
}
});
});

describe('update', () => {
it("doesn't crash when supplying zeroes", () => {
const accBands = new AccelerationBands(20, 2);
return accBands.update({
high: 0,
low: 0,
close: 0,
});
});
});
});

describe('FaserAccelerationBands', () => {
it("doesn't crash when supplying zeroes", () => {
const accBands = new FasterAccelerationBands(20, 2);
return accBands.update({
high: 0,
low: 0,
close: 0,
});
});
});
11 changes: 7 additions & 4 deletions src/ABANDS/AccelerationBands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export class AccelerationBands implements Indicator<BandsResult, HighLowClose> {
* Acceleration Bands (ABANDS)
* Type: Volatility
*
* Acceleration bands created by Price Headley are set as an envelope around a moving average. The upper and lower bands are of equal distance
* from the middle band.
* Acceleration bands created by Price Headley are set as an envelope around a moving average. The upper and lower
* bands are of equal distance from the middle band.
*
* Two consecutive closes outside Acceleration Bands suggest an entry point in the direction of the breakout (either
* bullish or bearish). A long position is usually kept till the first close back inside the bands.
Expand Down Expand Up @@ -47,7 +47,8 @@ export class AccelerationBands implements Indicator<BandsResult, HighLowClose> {
}

update({high, low, close}: HighLowClose): void {
const coefficient = new Big(high).minus(low).div(new Big(high).plus(low)).mul(this.width);
const highPlusLow = new Big(high).plus(low);
const coefficient = highPlusLow.eq(0) ? new Big(0) : new Big(high).minus(low).div(highPlusLow).mul(this.width);

// (Low * (1 - width * (High - Low)/ (High + Low)))
this.lowerBand.update(new Big(low).mul(new Big(1).minus(coefficient)));
Expand Down Expand Up @@ -86,7 +87,9 @@ export class FasterAccelerationBands implements Indicator<FasterBandsResult, Hig
}

update({high, low, close}: HighLowCloseNumber): void {
const coefficient = ((high - low) / (high + low)) * this.width;
const highPlusLow = high + low;
const coefficient = highPlusLow === 0 ? 0 : ((high - low) / highPlusLow) * this.width;

this.lowerBand.update(low * (1 - coefficient));
this.middleBand.update(close);
this.upperBand.update(high * (1 + coefficient));
Expand Down
5 changes: 5 additions & 0 deletions src/MAD/MAD.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ describe('MAD', () => {
});

describe('getResultFromBatch', () => {
it("doesn't crash when the array is empty", () => {
const result = MAD.getResultFromBatch([]);
expect(result.valueOf()).toBe('0');
});

it('calculates the mean when no mean is given', () => {
// Test data verified with:
// https://en.wikipedia.org/wiki/Average_absolute_deviation#Mean_absolute_deviation_around_a_central_point
Expand Down
2 changes: 1 addition & 1 deletion src/MAD/MAD.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class MAD extends BigIndicatorSeries {
const deviation = new Big(prices[i]).minus(mean).abs();
sum = sum.plus(deviation);
}
return sum.div(prices.length);
return sum.div(prices.length || 1);
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/SMA/SMA.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ describe('SMA', () => {
}
});
});

describe('getResultFromBatch', () => {
it("doesn't crash when the array is empty", () => {
const result = SMA.getResultFromBatch([]);
expect(result.valueOf()).toBe('0');
});
});
});

describe('FasterSMA', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/SMA/SMA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class SMA extends MovingAverage {

static getResultFromBatch(prices: BigSource[]): Big {
const sum = prices.reduce((a: Big, b: BigSource) => a.plus(b), new Big('0'));
return sum.div(prices.length);
return sum.div(prices.length || 1);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/STOCH/StochasticOscillator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export class StochasticOscillator implements Indicator<StochasticResult, HighLow
const lowest = getMinimum(this.candles.map(candle => candle.low));
const divisor = new Big(highest).minus(lowest);
let fastK = new Big(100).mul(new Big(candle.close).minus(lowest));
// Prevent division by zero
fastK = fastK.div(divisor.eq(0) ? 1 : divisor);
const dResult = this.d.update(fastK);
if (dResult) {
Expand Down
5 changes: 5 additions & 0 deletions src/util/getAverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ describe('getAverage', () => {
});

describe('getFasterAverage', () => {
it('does not fail when entering an empty array', () => {
const average = getFasterAverage([]);
expect(average).toBe(0);
});

it('only works with the primitive data type number', () => {
const prices = [20, 30, 40];
const average = getFasterAverage(prices);
Expand Down
8 changes: 2 additions & 6 deletions src/util/getAverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ import Big, {BigSource} from 'big.js';
* Return the mean / average value.
*/
export function getAverage(values: BigSource[]): Big {
if (values.length === 0) {
return new Big(0);
}

const sum = values.reduce((prev: Big, current: BigSource) => {
return prev.add(current);
}, new Big(0));

return sum.div(values.length);
return sum.div(values.length || 1);
}

export function getFasterAverage(values: number[]): number {
return values.reduce((sum: number, x: number) => sum + x, 0) / values.length;
return values.length ? values.reduce((sum: number, x: number) => sum + x, 0) / values.length : 0;
}