Skip to content

Commit

Permalink
feat(STOCH): Add faster implementation (#386)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored Dec 27, 2021
1 parent 1064ebb commit c6c38d2
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 84 deletions.
2 changes: 1 addition & 1 deletion src/BBW/BollingerBandsWidth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 71 additions & 60 deletions src/STOCH/StochasticOscillator.test.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,107 @@
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});
stoch.update({close: 100, high: 100, low: 100});
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');
});
});
});
110 changes: 87 additions & 23 deletions src/STOCH/StochasticOscillator.ts
Original file line number Diff line number Diff line change
@@ -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;
}

/**
Expand All @@ -19,32 +26,34 @@ 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<StochasticResult, HighLowClose> {
public readonly d: MovingAverage;
private readonly periodM: SMA;
private readonly periodP: SMA;

private readonly candles: HighLowClose[] = [];
private result?: StochasticResult;

/**
* Constructs a Stochastic Oscillator.
*
* @param periodK Typical intervals for the %k period are 5, 9, or 14
* @param periodD The standard interval for the %d period is 3
* @param [Indicator] Moving average type to smooth values (%d period)
* @param n The %k period
* @param m The %k slowing period
* @param p The %d period
*/
constructor(public readonly periodK: number, public readonly periodD: number, Indicator: MovingAverageTypes = SMA) {
this.d = new Indicator(periodD);
constructor(public readonly n: number, public readonly m: number, public readonly p: number) {
this.periodM = new SMA(m);
this.periodP = new SMA(p);
}

getResult(): StochasticResult {
Expand All @@ -55,25 +64,26 @@ export class StochasticOscillator implements Indicator<StochasticResult, HighLow
return this.result;
}

update(candle: HighLowClose): StochasticResult | void {
update(candle: HighLowClose): void | StochasticResult {
this.candles.push(candle);

if (this.candles.length > 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,
});
}
}
Expand All @@ -83,3 +93,57 @@ export class StochasticOscillator implements Indicator<StochasticResult, HighLow
return this.result !== undefined;
}
}

export class FasterStochasticOscillator implements Indicator<FasterStochasticResult, HighLowCloseNumber> {
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,
});
}
}
}
}
14 changes: 14 additions & 0 deletions src/start/startBenchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
FasterROC,
FasterRSI,
FasterSMA,
FasterStochasticOscillator,
FasterStochasticRSI,
FasterTR,
FasterWSMA,
Expand All @@ -49,6 +50,7 @@ import {
ROC,
RSI,
SMA,
StochasticOscillator,
StochasticRSI,
TR,
WSMA,
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit c6c38d2

Please sign in to comment.