Skip to content

Commit

Permalink
feat(WMA): Add Weighted Moving Average (WMA) (#677)
Browse files Browse the repository at this point in the history
  • Loading branch information
AkshatGiri authored May 8, 2024
1 parent 8ee412b commit 87bb0fb
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ All indicators can be updated over time by streaming data (prices or candles) to
1. Stochastic RSI (STOCHRSI)
1. True Range (TR)
1. Wilder's Smoothed Moving Average (WSMA / WMA / WWS / SMMA / MEMA)
1. Weighted Moving Average (WMA)

Utility Methods:

Expand Down
134 changes: 134 additions & 0 deletions src/WMA/WMA.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {Big, FasterWMA, NotEnoughDataError, WMA} from '../index.js';
import {describe} from 'vitest';

describe('WMA', () => {
describe('prices', () => {
it('does not cache more prices than necessary to fill the interval', () => {
const wma = new WMA(3);
const fasterWMA = new FasterWMA(3);
wma.update(1);
fasterWMA.update(1);
wma.update(2);
fasterWMA.update(2);
expect(wma.prices.length).toBe(2);
expect(fasterWMA.prices.length).toBe(2);
wma.update(3);
fasterWMA.update(3);
expect(wma.prices.length).toBe(3);
expect(fasterWMA.prices.length).toBe(3);
wma.update(4);
fasterWMA.update(4);
expect(wma.prices.length).toBe(3);
expect(fasterWMA.prices.length).toBe(3);
wma.update(5);
fasterWMA.update(5);
expect(wma.prices.length).toBe(3);
expect(fasterWMA.prices.length).toBe(3);
wma.update(6);
fasterWMA.update(6);
expect(wma.prices.length).toBe(3);
expect(fasterWMA.prices.length).toBe(3);
});
});

describe('update', () => {
it('can replace recently added values', () => {
const interval = 3;

const wma = new WMA(interval);
const fasterWMA = new FasterWMA(interval);

wma.update(30);
fasterWMA.update(30);

wma.update(60);
fasterWMA.update(60);

wma.update(90);
fasterWMA.update(90);

expect(wma.getResult().toFixed()).toBe('70');
expect(fasterWMA.getResult().toFixed()).toBe('70');

wma.update(60, true);
fasterWMA.update(60, true);

expect(wma.getResult().toFixed()).toBe('55');
expect(fasterWMA.getResult().toFixed()).toBe('55');
});
});

describe('updates', () => {
it('supports multiple updates at once', () => {
const prices = [30, 60, 90, 60, 90];
const interval = 5;

const wma = new WMA(interval);
const fasterWMA = new FasterWMA(interval);
wma.updates(prices);
fasterWMA.updates(prices);

expect(wma.getResult().toFixed()).toBe('74');
expect(fasterWMA.getResult().toFixed()).toBe('74');
});
});

describe('isStable', () => {
it('knows when there is enough input data', () => {
const wma = new WMA(3);
wma.update(30);
wma.update(60);
expect(wma.isStable).toBe(false);
wma.update(90);
expect(wma.isStable).toBe(true);
wma.update('120');
wma.update(new Big(60));
expect(wma.getResult().valueOf()).toBe('85');
expect(wma.lowest?.toFixed(2)).toBe('70.00');
expect(wma.highest?.toFixed(2)).toBe('100.00');
});
});

describe('getResult', () => {
it('calculates the moving average based on the last 5 prices', () => {
const prices = [91, 90, 89, 88, 90];
const expectations = ['89.33'];
const wma = new WMA(5);
const fasterWMA = new FasterWMA(5);

for (const price of prices) {
const result = wma.update(price);
const fasterResult = fasterWMA.update(price);

if (result && fasterResult) {
const expected = expectations.shift()!;
expect(result.toFixed(2)).toBe(expected);
expect(fasterResult.toFixed(2)).toBe(expected);
}
}

expect(wma.isStable).toBe(true);
expect(fasterWMA.isStable).toBe(true);
});

it('throws an error when there is not enough input data', () => {
const wma = new WMA(26);

try {
wma.getResult();
throw new Error('Expected error');
} catch (error) {
expect(error).toBeInstanceOf(NotEnoughDataError);
}

const fasterWMA = new FasterWMA(5);

try {
fasterWMA.getResult();
throw new Error('Expected error');
} catch (error) {
expect(error).toBeInstanceOf(NotEnoughDataError);
}
});
});
});
78 changes: 78 additions & 0 deletions src/WMA/WMA.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {Big, type BigSource} from '../index.js';
import {FasterMovingAverage, MovingAverage} from '../MA/MovingAverage.js';

/**
* Weighted Moving Average (WMA)
* Type: Trend
*
* Compared to SMA, the WMA puts more emphasis on the recent prices to reduce lag. Due to its responsiveness to price
* changes, it rises faster and falls faster than the SMA when the price is inclining or declining.
*
* @see https://corporatefinanceinstitute.com/resources/career-map/sell-side/capital-markets/weighted-moving-average-wma/
*/
export class WMA extends MovingAverage {
public readonly prices: BigSource[] = [];

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

override update(price: BigSource, replace: boolean = false): Big | void {
if (this.prices.length && replace) {
this.prices[this.prices.length - 1] = price;
} else {
this.prices.push(price);
}

if (this.prices.length > this.interval) {
this.prices.shift();
}

if (this.prices.length === this.interval) {
const weightedPricesSum = this.prices.reduce((acc: Big, price: BigSource, index: number) => {
const weightedPrice = new Big(price).mul(index + 1);

return acc.add(weightedPrice);
}, new Big(0));

const weightBase = (this.interval * (this.interval + 1)) / 2; // the numerator will always be even and the value will be an int.
const weightedMa = weightedPricesSum.div(weightBase);

return this.setResult(weightedMa);
}
}
}

export class FasterWMA extends FasterMovingAverage {
public readonly prices: number[] = [];

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

override update(price: number, replace: boolean = false): number | void {
if (this.prices.length && replace) {
this.prices[this.prices.length - 1] = price;
} else {
this.prices.push(price);
}

if (this.prices.length > this.interval) {
this.prices.shift();
}

if (this.prices.length === this.interval) {
const weightedPricesSum = this.prices.reduce((acc: number, price: number, index: number) => {
const weightedPrice = price * (index + 1);

return acc + weightedPrice;
}, 0);

const weightBase = (this.interval * (this.interval + 1)) / 2; // the numerator will always be even and the value will be an int.

const weightedMa = weightedPricesSum / weightBase;

return this.setResult(weightedMa);
}
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export * from './STOCH/StochasticOscillator.js';
export * from './STOCH/StochasticRSI.js';
export * from './TR/TR.js';
export * from './util/index.js';
export * from './WMA/WMA.js';
export * from './WSMA/WSMA.js';
14 changes: 14 additions & 0 deletions src/start/startBenchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
FasterStochasticRSI,
FasterTR,
FasterWSMA,
FasterWMA,
getAverage,
getFasterAverage,
getFasterStandardDeviation,
Expand All @@ -55,6 +56,7 @@ import {
StochasticRSI,
TR,
WSMA,
WMA,
type OpenHighLowCloseVolumeNumber,
} from '../index.js';

Expand Down Expand Up @@ -376,6 +378,18 @@ new Benchmark.Suite('Technical Indicators')
fasterWSMA.update(price);
}
})
.add('WMA', () => {
const wma = new WMA(interval);
for (const price of prices) {
wma.update(price);
}
})
.add('FasterWMA', () => {
const fasterWMA = new FasterWMA(interval);
for (const price of prices) {
fasterWMA.update(price);
}
})
.add('getAverage', () => {
return getAverage(prices);
})
Expand Down

0 comments on commit 87bb0fb

Please sign in to comment.