Skip to content

Commit 3ab4afc

Browse files
authored
feat(WILLR): Add Williams %R indicator
1 parent 9209957 commit 3ab4afc

File tree

8 files changed

+351
-70
lines changed

8 files changed

+351
-70
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Most of the time, the minimum amount of data depends on the interval / time peri
155155
1. Volume-Weighted Average Price (VWAP)
156156
1. Weighted Moving Average (WMA)
157157
1. Wilder's Smoothed Moving Average (WSMA / WWS / SMMA / MEMA)
158+
1. Williams %R (WILLR)
158159
1. Zig Zag Indicator (ZigZag)
159160

160161
Utility Methods:

docs/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/pages/indicators/momentum.tsx

Lines changed: 144 additions & 48 deletions
Large diffs are not rendered by default.

src/fixtures/STOCH/candles.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{"close": 81.59, "high": 82.15, "low": 81.29},
3+
{"close": 81.06, "high": 81.89, "low": 80.64},
4+
{"close": 82.87, "high": 83.03, "low": 81.31},
5+
{"close": 83.0, "high": 83.3, "low": 82.65},
6+
{"close": 83.61, "high": 83.85, "low": 83.07},
7+
{"close": 83.15, "high": 83.9, "low": 83.11},
8+
{"close": 82.84, "high": 83.33, "low": 82.49},
9+
{"close": 83.99, "high": 84.3, "low": 82.3},
10+
{"close": 84.55, "high": 84.84, "low": 84.15},
11+
{"close": 84.36, "high": 85.0, "low": 84.11},
12+
{"close": 85.53, "high": 85.9, "low": 84.03},
13+
{"close": 86.54, "high": 86.58, "low": 85.39},
14+
{"close": 86.89, "high": 86.98, "low": 85.76},
15+
{"close": 87.77, "high": 88.0, "low": 87.17},
16+
{"close": 87.29, "high": 87.87, "low": 87.01}
17+
]

src/momentum/STOCH/StochasticOscillator.test.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,13 @@
11
import {StochasticOscillator} from './StochasticOscillator.js';
22
import {NotEnoughDataError} from '../../error/index.js';
3+
import candles from '../../fixtures/STOCH/candles.json' with {type: 'json'};
34

45
describe('StochasticOscillator', () => {
56
describe('update', () => {
67
it('calculates the StochasticOscillator', () => {
78
// Test data verified with:
89
// https://tulipindicators.org/stoch
9-
const candles = [
10-
{close: 81.59, high: 82.15, low: 81.29},
11-
{close: 81.06, high: 81.89, low: 80.64},
12-
{close: 82.87, high: 83.03, low: 81.31},
13-
{close: 83.0, high: 83.3, low: 82.65},
14-
{close: 83.61, high: 83.85, low: 83.07},
15-
{close: 83.15, high: 83.9, low: 83.11},
16-
{close: 82.84, high: 83.33, low: 82.49},
17-
{close: 83.99, high: 84.3, low: 82.3},
18-
{close: 84.55, high: 84.84, low: 84.15},
19-
{close: 84.36, high: 85.0, low: 84.11},
20-
{close: 85.53, high: 85.9, low: 84.03},
21-
{close: 86.54, high: 86.58, low: 85.39},
22-
{close: 86.89, high: 86.98, low: 85.76},
23-
{close: 87.77, high: 88.0, low: 87.17},
24-
{close: 87.29, high: 87.87, low: 87.01},
25-
] as const;
26-
2710
const stochKs = ['77.39', '83.13', '84.87', '88.36', '95.25', '96.74', '91.09'] as const;
28-
2911
const stochDs = ['75.70', '78.01', '81.79', '85.45', '89.49', '93.45', '94.36'] as const;
3012

3113
const stoch = new StochasticOscillator(5, 3, 3);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import {WilliamsR} from './WilliamsR.js';
2+
import {StochasticOscillator} from '../STOCH/StochasticOscillator.js';
3+
import {NotEnoughDataError} from '../../error/index.js';
4+
import candles from '../../fixtures/STOCH/candles.json' with {type: 'json'};
5+
6+
describe('WilliamsR', () => {
7+
describe('update', () => {
8+
it('calculates the Williams %R indicator', () => {
9+
const expectations = [
10+
'-7.48',
11+
'-23.01',
12+
'-40.93',
13+
'-15.50',
14+
'-11.42',
15+
'-23.70',
16+
'-10.28',
17+
'-0.93',
18+
'-3.05',
19+
'-5.79',
20+
'-17.88',
21+
] as const;
22+
23+
const willR = new WilliamsR(5);
24+
const offset = willR.getRequiredInputs() - 1;
25+
26+
candles.forEach((candle, i) => {
27+
const result = willR.add(candle);
28+
29+
if (willR.isStable && result !== null) {
30+
const expected = expectations[i - offset];
31+
expect(result.toFixed(2)).toBe(expected);
32+
}
33+
});
34+
35+
expect(willR.isStable).toBe(true);
36+
expect(willR.getRequiredInputs()).toBe(5);
37+
expect(willR.getResultOrThrow().toFixed(2)).toBe('-17.88');
38+
});
39+
40+
it('returns null until enough values are provided', () => {
41+
const willR = new WilliamsR(5);
42+
43+
for (let i = 0; i < 4; i++) {
44+
const result = willR.add({close: i, high: i + 1, low: i - 1});
45+
expect(result).toBeNull();
46+
}
47+
48+
expect(willR.isStable).toBe(false);
49+
});
50+
51+
it('prevents division by zero when highest high and lowest low have the same value', () => {
52+
const willR = new WilliamsR(5);
53+
54+
for (let i = 0; i < 4; i++) {
55+
willR.add({close: 100, high: 100, low: 100});
56+
}
57+
58+
const result = willR.add({close: 100, high: 100, low: 100});
59+
expect(result).toBe(-100);
60+
expect(willR.getResultOrThrow()).toBe(-100);
61+
});
62+
63+
it('verifies Williams %R is equivalent to inverted Stochastic %K', () => {
64+
// Williams %R = -100 * (Highest High - Close) / (Highest High - Lowest Low)
65+
// Stochastic %K = 100 * (Close - Lowest Low) / (Highest High - Lowest Low)
66+
// Therefore: Williams %R = Stochastic %K - 100
67+
const willR = new WilliamsR(5);
68+
const stoch = new StochasticOscillator(5, 1, 1);
69+
70+
candles.forEach(candle => {
71+
const willRResult = willR.add(candle);
72+
const stochResult = stoch.add(candle);
73+
74+
if (willRResult !== null && stochResult !== null) {
75+
const {stochK} = stochResult;
76+
expect(willRResult.toFixed(2)).toBe((stochK - 100).toFixed(2));
77+
}
78+
});
79+
80+
expect(willR.isStable).toBe(true);
81+
expect(stoch.isStable).toBe(true);
82+
83+
const willRFinal = willR.getResultOrThrow();
84+
const stochKFinal = stoch.getResultOrThrow().stochK;
85+
expect(willRFinal.toFixed(2)).toBe((stochKFinal - 100).toFixed(2));
86+
});
87+
88+
it('handles the replace parameter correctly', () => {
89+
const willR = new WilliamsR(3);
90+
const latestValue = {close: 12, high: 13, low: 11};
91+
const someOtherValue = {close: 11.5, high: 12.5, low: 10.5};
92+
93+
willR.add({close: 10, high: 11, low: 9});
94+
willR.add({close: 11, high: 12, low: 10});
95+
const result = willR.add(latestValue);
96+
97+
const replacedResult = willR.replace(someOtherValue);
98+
expect(result).not.toBe(replacedResult);
99+
expect(willR.candles.length).toBe(3);
100+
101+
const revertedResult = willR.replace(latestValue);
102+
expect(result).toBe(revertedResult);
103+
expect(willR.candles.length).toBe(3);
104+
});
105+
});
106+
107+
describe('getResultOrThrow', () => {
108+
it('throws an error when there is not enough input data', () => {
109+
const willR = new WilliamsR(14);
110+
111+
for (let i = 0; i < 13; i++) {
112+
willR.add({close: i, high: i + 1, low: i - 1});
113+
}
114+
115+
try {
116+
willR.getResultOrThrow();
117+
throw new Error('Expected error');
118+
} catch (error) {
119+
expect(error).toBeInstanceOf(NotEnoughDataError);
120+
}
121+
});
122+
});
123+
});

src/momentum/WILLR/WilliamsR.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {TechnicalIndicator} from '../../types/Indicator.js';
2+
import type {HighLowClose} from '../../types/HighLowClose.js';
3+
import {pushUpdate} from '../../util/pushUpdate.js';
4+
5+
/**
6+
* Williams %R (Williams Percent Range)
7+
* Type: Momentum
8+
*
9+
* The Williams %R indicator, developed by Larry Williams, is a momentum indicator that measures overbought
10+
* and oversold levels. It is similar to the Stochastic Oscillator but is plotted on an inverted scale,
11+
* ranging from 0 to -100. Readings from 0 to -20 are considered overbought, while readings from -80 to -100
12+
* are considered oversold.
13+
*
14+
* The Williams %R is arithmetically exactly equivalent to the %K stochastic oscillator, mirrored at the 0%-line.
15+
*
16+
* Formula: %R = (Highest High - Close) / (Highest High - Lowest Low) × -100
17+
*
18+
* @see https://en.wikipedia.org/wiki/Williams_%25R
19+
* @see https://www.investopedia.com/terms/w/williamsr.asp
20+
*/
21+
export class WilliamsR extends TechnicalIndicator<number, HighLowClose<number>> {
22+
public readonly candles: HighLowClose<number>[] = [];
23+
24+
constructor(public readonly interval: number) {
25+
super();
26+
}
27+
28+
override getRequiredInputs() {
29+
return this.interval;
30+
}
31+
32+
override update(candle: HighLowClose<number>, replace: boolean) {
33+
pushUpdate(this.candles, replace, candle, this.interval);
34+
35+
if (this.candles.length === this.interval) {
36+
let highest = this.candles[0].high;
37+
let lowest = this.candles[0].low;
38+
39+
for (let i = 1; i < this.candles.length; i++) {
40+
if (this.candles[i].high > highest) {
41+
highest = this.candles[i].high;
42+
}
43+
44+
if (this.candles[i].low < lowest) {
45+
lowest = this.candles[i].low;
46+
}
47+
}
48+
49+
const divisor = highest - lowest;
50+
51+
if (divisor === 0) {
52+
return (this.result = -100);
53+
}
54+
55+
const willR = ((highest - candle.close) / divisor) * -100;
56+
return (this.result = willR);
57+
}
58+
59+
return null;
60+
}
61+
}

src/momentum/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './ROC/ROC.js';
1010
export * from './RSI/RSI.js';
1111
export * from './STOCH/StochasticOscillator.js';
1212
export * from './STOCHRSI/StochasticRSI.js';
13+
export * from './WILLR/WilliamsR.js';

0 commit comments

Comments
 (0)