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(STOCH): Add faster implementation #386

Merged
merged 1 commit into from
Dec 27, 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 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