From 718acfd35b21df55c1940c7a62a03ef0169645b5 Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Mon, 27 Dec 2021 22:52:26 +0100 Subject: [PATCH] feat(STOCH): Add faster implementation --- src/BBW/BollingerBandsWidth.test.ts | 2 +- src/STOCH/StochasticOscillator.test.ts | 131 ++++++++++++++----------- src/STOCH/StochasticOscillator.ts | 110 ++++++++++++++++----- src/start/startBenchmark.ts | 14 +++ 4 files changed, 173 insertions(+), 84 deletions(-) diff --git a/src/BBW/BollingerBandsWidth.test.ts b/src/BBW/BollingerBandsWidth.test.ts index 67d84e7fe..d50694ec0 100644 --- a/src/BBW/BollingerBandsWidth.test.ts +++ b/src/BBW/BollingerBandsWidth.test.ts @@ -3,7 +3,7 @@ import {BollingerBandsWidth, FasterBollingerBandsWidth} from './BollingerBandsWi describe('BollingerBandsWidth', () => { describe('getResult', () => { - it('calculates the Bollinger Bands Width', () => { + it('calculates the Bollinger Bands Width (BBW)', () => { // eBay Inc. (EBAY) daily stock prices in USD on NASDAQ with CBOE BZX exchange const candles = [ {open: 72.06, high: 72.07, low: 68.08, close: 68.21}, // 2021/07/30 diff --git a/src/STOCH/StochasticOscillator.test.ts b/src/STOCH/StochasticOscillator.test.ts index e4d2cfd98..d978a5117 100644 --- a/src/STOCH/StochasticOscillator.test.ts +++ b/src/STOCH/StochasticOscillator.test.ts @@ -1,87 +1,91 @@ -import {StochasticOscillator} from './StochasticOscillator'; +import {FasterStochasticOscillator, StochasticOscillator} from './StochasticOscillator'; import {NotEnoughDataError} from '../error'; describe('StochasticOscillator', () => { describe('update', () => { - it('is stable when the amount of inputs is bigger than the required interval', () => { + it('calculates the StochasticOscillator', () => { // Test data verified with: - // https://runkit.com/anandaravindan/stochastic - const highs = [ - 127.009, 127.616, 126.591, 127.347, 128.173, 128.432, 127.367, 126.422, 126.9, 126.85, 125.646, 125.716, - 127.158, 127.715, 127.686, 128.223, 128.273, 128.093, 128.273, 127.735, 128.77, 129.287, 130.063, 129.118, - 129.287, 128.472, 128.093, 128.651, 129.138, 128.641, - ]; - const lows = [ - 125.357, 126.163, 124.93, 126.094, 126.82, 126.482, 126.034, 124.83, 126.392, 125.716, 124.562, 124.572, - 125.069, 126.86, 126.631, 126.8, 126.711, 126.8, 126.134, 125.925, 126.989, 127.815, 128.472, 128.064, 127.606, - 127.596, 126.999, 126.9, 127.487, 127.397, - ]; - const closes = [ - 125.357, 126.163, 124.93, 126.094, 126.82, 126.482, 126.034, 124.83, 126.392, 125.716, 124.562, 124.572, - 125.069, 127.288, 127.178, 128.014, 127.109, 127.725, 127.059, 127.327, 128.71, 127.875, 128.581, 128.601, - 127.934, 128.113, 127.596, 127.596, 128.69, 128.273, + // https://tulipindicators.org/stoch + const candles = [ + {high: 82.15, low: 81.29, close: 81.59}, + {high: 81.89, low: 80.64, close: 81.06}, + {high: 83.03, low: 81.31, close: 82.87}, + {high: 83.3, low: 82.65, close: 83.0}, + {high: 83.85, low: 83.07, close: 83.61}, + {high: 83.9, low: 83.11, close: 83.15}, + {high: 83.33, low: 82.49, close: 82.84}, + {high: 84.3, low: 82.3, close: 83.99}, + {high: 84.84, low: 84.15, close: 84.55}, + {high: 85.0, low: 84.11, close: 84.36}, + {high: 85.9, low: 84.03, close: 85.53}, + {high: 86.58, low: 85.39, close: 86.54}, + {high: 86.98, low: 85.76, close: 86.89}, + {high: 88.0, low: 87.17, close: 87.77}, + {high: 87.87, low: 87.01, close: 87.29}, ]; - const expectations = [ - {d: '75.75', k: '89.20'}, - {d: '74.20', k: '65.81'}, - {d: '78.91', k: '81.73'}, - {d: '70.69', k: '64.52'}, - {d: '73.59', k: '74.51'}, - {d: '79.20', k: '98.57'}, - {d: '81.07', k: '70.12'}, - {d: '80.58', k: '73.06'}, - {d: '72.20', k: '73.42'}, - {d: '69.24', k: '61.23'}, - {d: '65.20', k: '60.95'}, - {d: '54.19', k: '40.38'}, - {d: '47.24', k: '40.38'}, - {d: '49.19', k: '66.82'}, - {d: '54.65', k: '56.74'}, - ]; + const stochKs = ['77.39', '83.13', '84.87', '88.36', '95.25', '96.74', '91.09']; + + const stochDs = ['75.70', '78.01', '81.79', '85.45', '89.49', '93.45', '94.36']; + + const stoch = new StochasticOscillator(5, 3, 3); + const fasterStoch = new FasterStochasticOscillator(5, 3, 3); - const stoch = new StochasticOscillator(14, 3); - - for (let i = 0; i < highs.length; i++) { - stoch.update({ - close: closes[i], - high: highs[i], - low: lows[i], - }); - if (stoch.isStable) { - const {k, d} = stoch.getResult(); - const expected = expectations.shift()!; - expect(k.toFixed(2)).toBe(expected.k); - expect(d.toFixed(2)).toBe(expected.d); + for (const candle of candles) { + const stochResult = stoch.update(candle); + const fasterStochResult = fasterStoch.update(candle); + if (fasterStoch.isStable && fasterStochResult && stoch.isStable && stochResult) { + const stochD = stochDs.shift()!; + const stochK = stochKs.shift()!; + + expect(stochResult.stochD.toFixed(2)).toEqual(stochD); + expect(fasterStochResult.stochD.toFixed(2)).toEqual(stochD); + + expect(stochResult.stochD.toFixed(2)).toEqual(stochD); + expect(fasterStochResult.stochK.toFixed(2)).toEqual(stochK); } } - const {k, d} = stoch.getResult(); - expect(k.toFixed(2)).toBe('56.74'); - expect(d.toFixed(2)).toBe('54.65'); + expect(stoch.isStable).toBeTrue(); + expect(fasterStoch.isStable).toBeTrue(); + + expect(stoch.getResult().stochK.toFixed(2)).toBe('91.09'); + expect(fasterStoch.getResult().stochK.toFixed(2)).toBe('91.09'); }); }); describe('getResult', () => { it('throws an error when there is not enough input data', () => { - const stoch = new StochasticOscillator(5, 3); + const stoch = new StochasticOscillator(5, 3, 3); + + stoch.update({close: 1, high: 1, low: 1}); + stoch.update({close: 1, high: 2, low: 1}); + stoch.update({close: 1, high: 3, low: 1}); + stoch.update({close: 1, high: 4, low: 1}); + stoch.update({close: 1, high: 5, low: 1}); // Emits 1st of 3 required values for %d period + stoch.update({close: 1, high: 6, low: 1}); // Emits 2nd of 3 required values for %d period try { - stoch.update({close: 1, high: 1, low: 1}); - stoch.update({close: 1, high: 2, low: 1}); - stoch.update({close: 1, high: 3, low: 1}); - stoch.update({close: 1, high: 4, low: 1}); - stoch.update({close: 1, high: 5, low: 1}); // Emits 1st of 3 required values for %d period - stoch.update({close: 1, high: 6, low: 1}); // Emits 2nd of 3 required values for %d period stoch.getResult(); fail('Expected error'); } catch (error) { expect(error).toBeInstanceOf(NotEnoughDataError); } + + const fasterStoch = new FasterStochasticOscillator(5, 3, 3); + + try { + fasterStoch.getResult(); + fail('Expected error'); + } catch (error) { + expect(error).toBeInstanceOf(NotEnoughDataError); + } }); it('prevents division by zero errors when highest high and lowest low have the same value', () => { - const stoch = new StochasticOscillator(5, 3); + const stoch = new StochasticOscillator(5, 3, 3); + stoch.update({close: 100, high: 100, low: 100}); + stoch.update({close: 100, high: 100, low: 100}); stoch.update({close: 100, high: 100, low: 100}); stoch.update({close: 100, high: 100, low: 100}); stoch.update({close: 100, high: 100, low: 100}); @@ -89,8 +93,15 @@ describe('StochasticOscillator', () => { stoch.update({close: 100, high: 100, low: 100}); stoch.update({close: 100, high: 100, low: 100}); const result = stoch.update({close: 100, high: 100, low: 100})!; - expect(result.k.toFixed(2)).toBe('0.00'); - expect(result.d.toFixed(2)).toBe('0.00'); + expect(result.stochK.toFixed(2)).toBe('0.00'); + expect(result.stochD.toFixed(2)).toBe('0.00'); + + const fasterStoch = new FasterStochasticOscillator(1, 2, 2); + fasterStoch.update({close: 100, high: 100, low: 100}); + fasterStoch.update({close: 100, high: 100, low: 100}); + const {stochK, stochD} = fasterStoch.getResult(); + expect(stochK.toFixed(2)).toBe('0.00'); + expect(stochD.toFixed(2)).toBe('0.00'); }); }); }); diff --git a/src/STOCH/StochasticOscillator.ts b/src/STOCH/StochasticOscillator.ts index 8697439d1..b05d78f7d 100644 --- a/src/STOCH/StochasticOscillator.ts +++ b/src/STOCH/StochasticOscillator.ts @@ -1,16 +1,23 @@ import {Indicator} from '../Indicator'; import Big from 'big.js'; -import {SMA} from '../SMA/SMA'; -import {MovingAverageTypes} from '../MA/MovingAverageTypes'; -import {MovingAverage} from '../MA/MovingAverage'; +import {FasterSMA, SMA} from '../SMA/SMA'; import {getMaximum} from '../util/getMaximum'; import {getMinimum} from '../util/getMinimum'; import {NotEnoughDataError} from '../error'; -import {HighLowClose} from '../util'; +import {HighLowClose, HighLowCloseNumber} from '../util'; export interface StochasticResult { - d: Big; - k: Big; + /** Slow stochastic indicator (%D) */ + stochD: Big; + /** Fast stochastic indicator (%K) */ + stochK: Big; +} + +export interface FasterStochasticResult { + /** Slow stochastic indicator (%D) */ + stochD: number; + /** Fast stochastic indicator (%K) */ + stochK: number; } /** @@ -19,19 +26,20 @@ export interface StochasticResult { * * The Stochastic Oscillator was developed by George Lane and is range-bound between 0 and 100. The Stochastic * Oscillator attempts to predict price turning points. A value of 80 indicates that the asset is on the verge of being - * overbought. By default a Simple Moving Average (SMA) is used. When the momentum starts to slow down, the Stochastic + * overbought. By default, a Simple Moving Average (SMA) is used. When the momentum starts to slow down, the Stochastic * Oscillator values start to turn down. In the case of an uptrend, prices tend to make higher highs, and the * settlement price usually tends to be in the upper end of that time period's trading range. * - * The %k values represent the relation between current close to the period's price range (high/low). It is sometimes - * referred as the "fast" stochastic period (fastk). The %d values represent a Moving Average of the %k values. It - * is sometimes referred as the "slow" period. + * The stochastic k (%k) values represent the relation between current close to the period's price range (high/low). It + * is sometimes referred as the "fast" stochastic period (fastk). The stochastic d (%d) values represent a Moving + * Average of the %k values. It is sometimes referred as the "slow" period. * * @see https://en.wikipedia.org/wiki/Stochastic_oscillator * @see https://www.investopedia.com/terms/s/stochasticoscillator.asp */ export class StochasticOscillator implements Indicator { - public readonly d: MovingAverage; + private readonly periodM: SMA; + private readonly periodP: SMA; private readonly candles: HighLowClose[] = []; private result?: StochasticResult; @@ -39,12 +47,13 @@ export class StochasticOscillator implements Indicator this.periodK) { + if (this.candles.length > this.n) { this.candles.shift(); } - if (this.candles.length === this.periodK) { + if (this.candles.length === this.n) { const highest = getMaximum(this.candles.map(candle => candle.high)); 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) { + const stochK = this.periodM.update(fastK); // (stoch_k, %k) + const stochD = stochK && this.periodP.update(stochK); // (stoch_d, %d) + if (stochK && stochD) { return (this.result = { - d: dResult, - k: fastK, + stochD, + stochK, }); } } @@ -83,3 +93,57 @@ export class StochasticOscillator implements Indicator { + public readonly candles: HighLowCloseNumber[] = []; + private result: FasterStochasticResult | undefined; + private readonly periodM: FasterSMA; + private readonly periodP: FasterSMA; + + /** + * @param n The %k period + * @param m The %k slowing period + * @param p The %d period + */ + constructor(public n: number, public m: number, public p: number) { + this.periodM = new FasterSMA(m); + this.periodP = new FasterSMA(p); + } + + getResult(): FasterStochasticResult { + if (this.result === undefined) { + throw new NotEnoughDataError(); + } + + return this.result; + } + + get isStable(): boolean { + return this.result !== undefined; + } + + update(candle: HighLowCloseNumber): void | FasterStochasticResult { + this.candles.push(candle); + + if (this.candles.length > this.n) { + this.candles.shift(); + } + + if (this.candles.length === this.n) { + const highest = Math.max(...this.candles.map(candle => candle.high)); + const lowest = Math.min(...this.candles.map(candle => candle.low)); + const divisor = highest - lowest; + let fastK = (candle.close - lowest) * 100; + // Prevent division by zero + fastK = fastK / (divisor === 0 ? 1 : divisor); + const stochK = this.periodM.update(fastK); // (stoch_k, %k) + const stochD = stochK && this.periodP.update(stochK); // (stoch_d, %d) + if (stochK !== undefined && stochD !== undefined) { + return (this.result = { + stochD, + stochK, + }); + } + } + } +} diff --git a/src/start/startBenchmark.ts b/src/start/startBenchmark.ts index cf13a239d..685911bef 100644 --- a/src/start/startBenchmark.ts +++ b/src/start/startBenchmark.ts @@ -34,6 +34,7 @@ import { FasterROC, FasterRSI, FasterSMA, + FasterStochasticOscillator, FasterStochasticRSI, FasterTR, FasterWSMA, @@ -49,6 +50,7 @@ import { ROC, RSI, SMA, + StochasticOscillator, StochasticRSI, TR, WSMA, @@ -310,6 +312,18 @@ new Benchmark.Suite('Technical Indicators') fasterSMA.update(price); } }) + .add('StochasticOscillator', () => { + const stoch = new StochasticOscillator(shortInterval, interval, interval); + for (const candle of candles) { + stoch.update(candle); + } + }) + .add('FasterStochasticOscillator', () => { + const fasterStoch = new FasterStochasticOscillator(shortInterval, interval, interval); + for (const candle of highLowCloses) { + fasterStoch.update(candle); + } + }) .add('StochasticRSI', () => { const stochRSI = new StochasticRSI(interval); for (const price of prices) {