Skip to content
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 @@ -155,6 +155,7 @@ Most of the time, the minimum amount of data depends on the interval / time peri
1. Volume-Weighted Average Price (VWAP)
1. Weighted Moving Average (WMA)
1. Wilder's Smoothed Moving Average (WSMA / WWS / SMMA / MEMA)
1. Williams %R (WILLR)
1. Zig Zag Indicator (ZigZag)

Utility Methods:
Expand Down
6 changes: 3 additions & 3 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

192 changes: 144 additions & 48 deletions docs/pages/indicators/momentum.tsx

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions src/fixtures/STOCH/candles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{"close": 81.59, "high": 82.15, "low": 81.29},
{"close": 81.06, "high": 81.89, "low": 80.64},
{"close": 82.87, "high": 83.03, "low": 81.31},
{"close": 83.0, "high": 83.3, "low": 82.65},
{"close": 83.61, "high": 83.85, "low": 83.07},
{"close": 83.15, "high": 83.9, "low": 83.11},
{"close": 82.84, "high": 83.33, "low": 82.49},
{"close": 83.99, "high": 84.3, "low": 82.3},
{"close": 84.55, "high": 84.84, "low": 84.15},
{"close": 84.36, "high": 85.0, "low": 84.11},
{"close": 85.53, "high": 85.9, "low": 84.03},
{"close": 86.54, "high": 86.58, "low": 85.39},
{"close": 86.89, "high": 86.98, "low": 85.76},
{"close": 87.77, "high": 88.0, "low": 87.17},
{"close": 87.29, "high": 87.87, "low": 87.01}
]
20 changes: 1 addition & 19 deletions src/momentum/STOCH/StochasticOscillator.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
import {StochasticOscillator} from './StochasticOscillator.js';
import {NotEnoughDataError} from '../../error/index.js';
import candles from '../../fixtures/STOCH/candles.json' with {type: 'json'};

describe('StochasticOscillator', () => {
describe('update', () => {
it('calculates the StochasticOscillator', () => {
// Test data verified with:
// https://tulipindicators.org/stoch
const candles = [
{close: 81.59, high: 82.15, low: 81.29},
{close: 81.06, high: 81.89, low: 80.64},
{close: 82.87, high: 83.03, low: 81.31},
{close: 83.0, high: 83.3, low: 82.65},
{close: 83.61, high: 83.85, low: 83.07},
{close: 83.15, high: 83.9, low: 83.11},
{close: 82.84, high: 83.33, low: 82.49},
{close: 83.99, high: 84.3, low: 82.3},
{close: 84.55, high: 84.84, low: 84.15},
{close: 84.36, high: 85.0, low: 84.11},
{close: 85.53, high: 85.9, low: 84.03},
{close: 86.54, high: 86.58, low: 85.39},
{close: 86.89, high: 86.98, low: 85.76},
{close: 87.77, high: 88.0, low: 87.17},
{close: 87.29, high: 87.87, low: 87.01},
] as const;

const stochKs = ['77.39', '83.13', '84.87', '88.36', '95.25', '96.74', '91.09'] as const;

const stochDs = ['75.70', '78.01', '81.79', '85.45', '89.49', '93.45', '94.36'] as const;

const stoch = new StochasticOscillator(5, 3, 3);
Expand Down
123 changes: 123 additions & 0 deletions src/momentum/WILLR/WilliamsR.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {WilliamsR} from './WilliamsR.js';
import {StochasticOscillator} from '../STOCH/StochasticOscillator.js';
import {NotEnoughDataError} from '../../error/index.js';
import candles from '../../fixtures/STOCH/candles.json' with {type: 'json'};

describe('WilliamsR', () => {
describe('update', () => {
it('calculates the Williams %R indicator', () => {
const expectations = [
'-7.48',
'-23.01',
'-40.93',
'-15.50',
'-11.42',
'-23.70',
'-10.28',
'-0.93',
'-3.05',
'-5.79',
'-17.88',
] as const;

const willR = new WilliamsR(5);
const offset = willR.getRequiredInputs() - 1;

candles.forEach((candle, i) => {
const result = willR.add(candle);

if (willR.isStable && result !== null) {
const expected = expectations[i - offset];
expect(result.toFixed(2)).toBe(expected);
}
});

expect(willR.isStable).toBe(true);
expect(willR.getRequiredInputs()).toBe(5);
expect(willR.getResultOrThrow().toFixed(2)).toBe('-17.88');
});

it('returns null until enough values are provided', () => {
const willR = new WilliamsR(5);

for (let i = 0; i < 4; i++) {
const result = willR.add({close: i, high: i + 1, low: i - 1});
expect(result).toBeNull();
}

expect(willR.isStable).toBe(false);
});

it('prevents division by zero when highest high and lowest low have the same value', () => {
const willR = new WilliamsR(5);

for (let i = 0; i < 4; i++) {
willR.add({close: 100, high: 100, low: 100});
}

const result = willR.add({close: 100, high: 100, low: 100});
expect(result).toBe(-100);
expect(willR.getResultOrThrow()).toBe(-100);
});

it('verifies Williams %R is equivalent to inverted Stochastic %K', () => {
// Williams %R = -100 * (Highest High - Close) / (Highest High - Lowest Low)
// Stochastic %K = 100 * (Close - Lowest Low) / (Highest High - Lowest Low)
// Therefore: Williams %R = Stochastic %K - 100
const willR = new WilliamsR(5);
const stoch = new StochasticOscillator(5, 1, 1);

candles.forEach(candle => {
const willRResult = willR.add(candle);
const stochResult = stoch.add(candle);

if (willRResult !== null && stochResult !== null) {
const {stochK} = stochResult;
expect(willRResult.toFixed(2)).toBe((stochK - 100).toFixed(2));
}
});

expect(willR.isStable).toBe(true);
expect(stoch.isStable).toBe(true);

const willRFinal = willR.getResultOrThrow();
const stochKFinal = stoch.getResultOrThrow().stochK;
expect(willRFinal.toFixed(2)).toBe((stochKFinal - 100).toFixed(2));
});

it('handles the replace parameter correctly', () => {
const willR = new WilliamsR(3);
const latestValue = {close: 12, high: 13, low: 11};
const someOtherValue = {close: 11.5, high: 12.5, low: 10.5};

willR.add({close: 10, high: 11, low: 9});
willR.add({close: 11, high: 12, low: 10});
const result = willR.add(latestValue);

const replacedResult = willR.replace(someOtherValue);
expect(result).not.toBe(replacedResult);
expect(willR.candles.length).toBe(3);

const revertedResult = willR.replace(latestValue);
expect(result).toBe(revertedResult);
expect(willR.candles.length).toBe(3);
});
});

describe('getResultOrThrow', () => {
it('throws an error when there is not enough input data', () => {
const willR = new WilliamsR(14);

for (let i = 0; i < 13; i++) {
willR.add({close: i, high: i + 1, low: i - 1});
}

try {
willR.getResultOrThrow();
throw new Error('Expected error');
} catch (error) {
expect(error).toBeInstanceOf(NotEnoughDataError);
}
});
});
});
61 changes: 61 additions & 0 deletions src/momentum/WILLR/WilliamsR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {TechnicalIndicator} from '../../types/Indicator.js';
import type {HighLowClose} from '../../types/HighLowClose.js';
import {pushUpdate} from '../../util/pushUpdate.js';

/**
* Williams %R (Williams Percent Range)
* Type: Momentum
*
* The Williams %R indicator, developed by Larry Williams, is a momentum indicator that measures overbought
* and oversold levels. It is similar to the Stochastic Oscillator but is plotted on an inverted scale,
* ranging from 0 to -100. Readings from 0 to -20 are considered overbought, while readings from -80 to -100
* are considered oversold.
*
* The Williams %R is arithmetically exactly equivalent to the %K stochastic oscillator, mirrored at the 0%-line.
*
* Formula: %R = (Highest High - Close) / (Highest High - Lowest Low) × -100
*
* @see https://en.wikipedia.org/wiki/Williams_%25R
* @see https://www.investopedia.com/terms/w/williamsr.asp
*/
export class WilliamsR extends TechnicalIndicator<number, HighLowClose<number>> {
public readonly candles: HighLowClose<number>[] = [];

constructor(public readonly interval: number) {
super();
}

override getRequiredInputs() {
return this.interval;
}

override update(candle: HighLowClose<number>, replace: boolean) {
pushUpdate(this.candles, replace, candle, this.interval);

if (this.candles.length === this.interval) {
let highest = this.candles[0].high;
let lowest = this.candles[0].low;

for (let i = 1; i < this.candles.length; i++) {
if (this.candles[i].high > highest) {
highest = this.candles[i].high;
}

if (this.candles[i].low < lowest) {
lowest = this.candles[i].low;
}
}

const divisor = highest - lowest;

if (divisor === 0) {
return (this.result = -100);
}

const willR = ((highest - candle.close) / divisor) * -100;
return (this.result = willR);
}

return null;
}
}
1 change: 1 addition & 0 deletions src/momentum/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './ROC/ROC.js';
export * from './RSI/RSI.js';
export * from './STOCH/StochasticOscillator.js';
export * from './STOCHRSI/StochasticRSI.js';
export * from './WILLR/WilliamsR.js';