Skip to content

Commit

Permalink
feat(AC,AO,MOM): Add faster implementation (#373)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored Dec 5, 2021
1 parent 74841cb commit c821c64
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 40 deletions.
22 changes: 13 additions & 9 deletions src/ABANDS/AccelerationBands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,21 @@ export class AccelerationBands implements Indicator<BandsResult, HighLowClose> {
* @param interval The interval that is being used for the three moving averages which create lower, middle and upper
* bands
* @param width A coefficient specifying the distance between the middle band and upper/lower bands
* @param Indicator Which moving average (SMA, EMA, ...) to use
* @param SmoothingIndicator Which moving average (SMA, EMA, ...) 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(public readonly interval: number, public readonly width: number, Indicator: MovingAverageTypes = SMA) {
this.lowerBand = new Indicator(interval);
this.middleBand = new Indicator(interval);
this.upperBand = new Indicator(interval);
constructor(
public readonly interval: number,
public readonly width: number,
SmoothingIndicator: MovingAverageTypes = SMA
) {
this.lowerBand = new SmoothingIndicator(interval);
this.middleBand = new SmoothingIndicator(interval);
this.upperBand = new SmoothingIndicator(interval);
}

get isStable(): boolean {
Expand Down Expand Up @@ -74,11 +78,11 @@ export class FasterAccelerationBands implements Indicator<FasterBandsResult, Hig
constructor(
public readonly interval: number,
public readonly width: number,
Indicator: FasterMovingAverageTypes = FasterSMA
SmoothingIndicator: FasterMovingAverageTypes = FasterSMA
) {
this.lowerBand = new Indicator(interval);
this.middleBand = new Indicator(interval);
this.upperBand = new Indicator(interval);
this.lowerBand = new SmoothingIndicator(interval);
this.middleBand = new SmoothingIndicator(interval);
this.upperBand = new SmoothingIndicator(interval);
}

update({high, low, close}: HighLowCloseNumber): void {
Expand Down
18 changes: 16 additions & 2 deletions src/AC/AC.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {AC} from './AC';
import {AC, FasterAC} from './AC';
import {NotEnoughDataError} from '../error';
import {HighLowNumber} from '../util';

describe('AC', () => {
describe('getResult', () => {
it('works with a signal line of SMA(5)', () => {
const ac = new AC(5, 34, 5);
const fasterAC = new FasterAC(5, 34, 5);

// Test data verified with:
// https://github.com/jesse-ai/jesse/blob/8e502d070c24bed29db80e1d0938781d8cdb1046/tests/data/test_candles_indicators.py#L4351
const candles = [
Expand Down Expand Up @@ -251,16 +254,27 @@ describe('AC', () => {

for (const candle of candles) {
const [, , , high, low] = candle;
ac.update({high, low});
const input: HighLowNumber = {high, low};
ac.update(input);
fasterAC.update(input);
}

// Result verified with:
// https://github.com/jesse-ai/jesse/blob/53297462d48ebf43f9df46ab5005076d25073e5e/tests/test_indicators.py#L14
expect(ac.isStable).toBeTrue();
expect(fasterAC.isStable).toBeTrue();

expect(ac.getResult().toFixed(2)).toBe('-21.97');
expect(fasterAC.getResult().toFixed(2)).toBe('-21.97');

expect(ac.momentum.getResult().toFixed(2)).toBe('-9.22');
expect(fasterAC.momentum.getResult().toFixed(2)).toBe('-9.22');

expect(ac.lowest!.toFixed(2)).toBe('-21.97');
expect(fasterAC.lowest!.toFixed(2)).toBe('-21.97');

expect(ac.highest!.toFixed(2)).toBe('11.65');
expect(fasterAC.highest!.toFixed(2)).toBe('11.65');
});

it('throws an error when there is not enough input data', () => {
Expand Down
37 changes: 31 additions & 6 deletions src/AC/AC.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {BigIndicatorSeries} from '../Indicator';
import {BigIndicatorSeries, NumberIndicatorSeries} from '../Indicator';
import Big from 'big.js';
import {AO} from '../AO/AO';
import {SMA} from '../SMA/SMA';
import {MOM} from '../MOM/MOM';
import {HighLow} from '../util';
import {AO, FasterAO} from '../AO/AO';
import {FasterSMA, SMA} from '../SMA/SMA';
import {FasterMOM, MOM} from '../MOM/MOM';
import {HighLow, HighLowNumber} from '../util';

/**
* Accelerator Oscillator (AC)
Expand All @@ -25,8 +25,8 @@ export class AC extends BigIndicatorSeries<HighLow> {
constructor(public readonly shortAO: number, public readonly longAO: number, public readonly signalInterval: number) {
super();
this.ao = new AO(shortAO, longAO);
this.signal = new SMA(signalInterval);
this.momentum = new MOM(1);
this.signal = new SMA(signalInterval);
}

override update(input: HighLow): void | Big {
Expand All @@ -41,3 +41,28 @@ export class AC extends BigIndicatorSeries<HighLow> {
}
}
}

export class FasterAC extends NumberIndicatorSeries<HighLowNumber> {
public readonly ao: FasterAO;
public readonly momentum: FasterMOM;
public readonly signal: FasterSMA;

constructor(public readonly shortAO: number, public readonly longAO: number, public readonly signalInterval: number) {
super();
this.ao = new FasterAO(shortAO, longAO);
this.momentum = new FasterMOM(1);
this.signal = new FasterSMA(signalInterval);
}

override update(input: HighLowNumber): void | number {
const ao = this.ao.update(input);
if (ao) {
this.signal.update(ao);
if (this.signal.isStable) {
const result = this.setResult(ao - this.signal.getResult());
this.momentum.update(result);
return this.result;
}
}
}
}
23 changes: 16 additions & 7 deletions src/AO/AO.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {AO} from './AO';
import {AO, FasterAO} from './AO';
import {NotEnoughDataError} from '../error';
import {HighLowNumber} from '../util';

describe('AO', () => {
describe('getResult', () => {
Expand Down Expand Up @@ -32,22 +33,30 @@ describe('AO', () => {
5.3673, 4.5294, 4.764, 4.1044, 1.6913, -1.3769, -4.2062, -7.7196, -10.6241, -11.4972, -9.6358, -7.9344,
];
const ao = new AO(5, 34);
const fasterAO = new FasterAO(5, 34);

for (let i = 0; i < lows.length; i++) {
const newResult = ao.update({
const candle: HighLowNumber = {
high: highs[i],
low: lows[i],
});
if (ao.isStable) {
expect(newResult!).not.toBeUndefined();
};
const result = ao.update(candle);
const fasterResult = fasterAO.update(candle);
if (ao.isStable && fasterAO.isStable) {
expect(result).not.toBeUndefined();
expect(fasterResult).not.toBeUndefined();
const actual = ao.getResult().toFixed(4);
const expected = aos.shift();
expect(parseFloat(actual)).toBe(expected!);
const expected = aos.shift()!;
expect(parseFloat(actual)).toBe(expected);
expect(fasterResult!.toFixed(4)).toBe(expected.toFixed(4));
}
}

expect(ao.lowest!.toFixed(2)).toBe('-11.50');
expect(fasterAO.lowest!.toFixed(2)).toBe('-11.50');

expect(ao.highest!.toFixed(2)).toBe('33.35');
expect(fasterAO.highest!.toFixed(2)).toBe('33.35');
});

it('throws an error when there is not enough input data', () => {
Expand Down
50 changes: 41 additions & 9 deletions src/AO/AO.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {BigIndicatorSeries} from '../Indicator';
import {BigIndicatorSeries, NumberIndicatorSeries} from '../Indicator';
import Big from 'big.js';
import {SMA} from '../SMA/SMA';
import {HighLow} from '../util';
import {FasterSMA, SMA} from '../SMA/SMA';
import {HighLow, HighLowNumber} from '../util';
import {FasterMovingAverageTypes, MovingAverageTypes} from '../MA/MovingAverageTypes';
import {FasterMovingAverage, MovingAverage} from '../MA/MovingAverage';

/**
* Awesome Oscillator (AO)
Expand All @@ -18,13 +20,17 @@ import {HighLow} from '../util';
* @see https://tradingstrategyguides.com/bill-williams-awesome-oscillator-strategy/
*/
export class AO extends BigIndicatorSeries<HighLow> {
public readonly long: SMA;
public readonly short: SMA;
public readonly long: MovingAverage;
public readonly short: MovingAverage;

constructor(public readonly shortInterval: number, public readonly longInterval: number) {
constructor(
public readonly shortInterval: number,
public readonly longInterval: number,
SmoothingIndicator: MovingAverageTypes = SMA
) {
super();
this.short = new SMA(shortInterval);
this.long = new SMA(longInterval);
this.short = new SmoothingIndicator(shortInterval);
this.long = new SmoothingIndicator(longInterval);
}

override update({low, high}: HighLow): void | Big {
Expand All @@ -34,8 +40,34 @@ export class AO extends BigIndicatorSeries<HighLow> {
this.short.update(medianPrice);
this.long.update(medianPrice);

if (this.short.isStable && this.long.isStable) {
if (this.long.isStable) {
return this.setResult(this.short.getResult().sub(this.long.getResult()));
}
}
}

export class FasterAO extends NumberIndicatorSeries<HighLowNumber> {
public readonly long: FasterMovingAverage;
public readonly short: FasterMovingAverage;

constructor(
public readonly shortInterval: number,
public readonly longInterval: number,
SmoothingIndicator: FasterMovingAverageTypes = FasterSMA
) {
super();
this.short = new SmoothingIndicator(shortInterval);
this.long = new SmoothingIndicator(longInterval);
}

override update({low, high}: HighLowNumber): void | number {
const medianPrice = (low + high) / 2;

this.short.update(medianPrice);
this.long.update(medianPrice);

if (this.long.isStable) {
return this.setResult(this.short.getResult() - this.long.getResult());
}
}
}
17 changes: 13 additions & 4 deletions src/MOM/MOM.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {MOM} from './MOM';
import {FasterMOM, MOM} from './MOM';
import {NotEnoughDataError} from '../error';

describe('MOM', () => {
Expand All @@ -11,18 +11,27 @@ describe('MOM', () => {
];
const outputs = [1.56, 1.78, 1.12, 1.55, 0.75, 2.38, 3.7, 2.9, 3.22, 2.93];
const momentum = new MOM(5);
const fasterMomentum = new FasterMOM(5);

for (const input of inputs) {
momentum.update(input);
if (momentum.isStable) {
fasterMomentum.update(input);
if (momentum.isStable && fasterMomentum.isStable) {
const actual = momentum.getResult().toFixed(3);
const expected = outputs.shift();
expect(parseFloat(actual)).toBe(expected!);
const expected = outputs.shift()!;
expect(parseFloat(actual)).toBe(expected);
expect(fasterMomentum.getResult().toFixed(2)).toBe(expected.toFixed(2));
}
}

expect(momentum.isStable).toBeTrue();
expect(fasterMomentum.isStable).toBeTrue();

expect(momentum.lowest!.toFixed(2)).toBe('0.75');
expect(fasterMomentum.lowest!.toFixed(2)).toBe('0.75');

expect(momentum.highest!.toFixed(2)).toBe('3.70');
expect(fasterMomentum.highest!.toFixed(2)).toBe('3.70');
});

it('throws an error when there is not enough input data', () => {
Expand Down
24 changes: 21 additions & 3 deletions src/MOM/MOM.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {BigIndicatorSeries} from '../Indicator';
import {BigIndicatorSeries, NumberIndicatorSeries} from '../Indicator';
import Big, {BigSource} from 'big.js';
import {getFixedArray} from '../util/getFixedArray';

Expand All @@ -20,10 +20,28 @@ export class MOM extends BigIndicatorSeries {
this.history = getFixedArray<BigSource>(this.historyLength);
}

override update(value: BigSource): void {
override update(value: BigSource): void | Big {
this.history.push(value);
if (this.history.length === this.historyLength) {
this.setResult(new Big(value).minus(this.history[0]));
return this.setResult(new Big(value).minus(this.history[0]));
}
}
}

export class FasterMOM extends NumberIndicatorSeries {
private readonly history: number[];
private readonly historyLength: number;

constructor(public readonly interval: number) {
super();
this.historyLength = interval + 1;
this.history = getFixedArray<number>(this.historyLength);
}

override update(value: number): void | number {
this.history.push(value);
if (this.history.length === this.historyLength) {
return this.setResult(value - this.history[0]);
}
}
}
39 changes: 39 additions & 0 deletions src/start/startBenchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import {
import {FasterRSI, RSI} from '../RSI/RSI';
import {FasterStochasticRSI, StochasticRSI} from '../STOCH/StochasticRSI';
import {AccelerationBands, FasterAccelerationBands} from '../ABANDS/AccelerationBands';
import {FasterMOM, MOM} from '../MOM/MOM';
import {AO, FasterAO} from '../AO/AO';
import {AC, FasterAC} from '../AC/AC';

const interval = 20;
const prices: number[] = candles.map(candle => parseInt(candle.close, 10));
Expand All @@ -44,12 +47,36 @@ new Benchmark.Suite('Technical Indicators')
fasterAccBands.update(candle);
}
})
.add('AC', () => {
const ac = new AC(5, 34, 5);
for (const candle of highLowCloses) {
ac.update(candle);
}
})
.add('FasterAC', () => {
const fasterAC = new FasterAC(5, 34, 5);
for (const candle of highLowCloses) {
fasterAC.update(candle);
}
})
.add('ADX', () => {
const adx = new ADX(interval);
for (const candle of highLowCloses) {
adx.update(candle);
}
})
.add('AO', () => {
const ao = new AO(5, 34);
for (const candle of highLowCloses) {
ao.update(candle);
}
})
.add('FasterAO', () => {
const fasterAO = new FasterAO(5, 34);
for (const candle of highLowCloses) {
fasterAO.update(candle);
}
})
.add('ATR', () => {
const atr = new ATR(interval);
for (const candle of highLowCloses) {
Expand Down Expand Up @@ -116,6 +143,18 @@ new Benchmark.Suite('Technical Indicators')
fasterMad.update(price);
}
})
.add('MOM', () => {
const mom = new MOM(interval);
for (const price of prices) {
mom.update(price);
}
})
.add('FasterMOM', () => {
const fasterMOM = new FasterMOM(interval);
for (const price of prices) {
fasterMOM.update(price);
}
})
.add('Period', () => {
const period = new Period(interval);
for (const price of prices) {
Expand Down

0 comments on commit c821c64

Please sign in to comment.