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

Adding weighted moving average #677

Merged
merged 3 commits into from
May 8, 2024
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 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)
bennycode marked this conversation as resolved.
Show resolved Hide resolved

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
Loading