Skip to content

Commit

Permalink
fix(ATR,AO,CG,SMA,TR): Fix caching of highest and lowest result (#679)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored May 8, 2024
1 parent 4a1f221 commit bcb73a6
Show file tree
Hide file tree
Showing 14 changed files with 571 additions and 193 deletions.
139 changes: 109 additions & 30 deletions src/AO/AO.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,40 @@ import {NotEnoughDataError} from '../error/index.js';
import type {HighLowNumber} from '../util/index.js';

describe('AO', () => {
// Test data verified with:
// https://github.com/TulipCharts/tulipindicators/blob/v0.8.0/tests/extra.txt#L17-L20
const highs = [
32.11, 27.62, 28.26, 28.02, 26.93, 26.65, 27.25, 27.58, 27.9, 28.9, 29.34, 29.82, 29.54, 29.3, 29.5, 29.5, 29.7,
29.14, 27.17, 30.34, 30.26, 30.14, 29.98, 30.55, 32.11, 34.16, 39.5, 50.78, 51.38, 51.34, 50.7, 44.23, 42.71, 39.82,
42.35, 44.71, 44.27, 43.67, 44.83, 44.55, 46.8, 46.24, 45.52, 44.55, 46.12, 44.71, 44.47, 45.28, 44.63, 43.43,
46.24, 49.13, 49.93, 49.93, 51.38, 52.78, 50.53, 50.61, 49.33, 49.41, 53.3, 52.58, 62.3, 61.57, 62.54, 64.54, 74.14,
73.17, 70.12, 68.64, 71.77, 70.64, 71.77, 104.04, 103.4, 97.62, 98.9, 99.46, 89.23, 87.34, 82.28, 78.99, 81.56,
78.03, 73.86, 77.67, 79.47, 77.71, 76.75, 78.31, 77.71, 72.25, 68.08, 66.31, 65.75, 64.14, 67.43, 80.28, 78.35,
] as const;
const lows = [
25.69, 25.57, 25.73, 25.69, 25.69, 26.17, 26.05, 26.29, 26.89, 27.74, 28.46, 29.02, 28.46, 28.78, 29.06, 28.74,
28.5, 27.01, 26.33, 26.97, 29.18, 29.62, 29.54, 29.74, 30.47, 31.83, 34.64, 40.42, 47.68, 48.97, 43.27, 41.58,
38.73, 37.33, 37.89, 43.39, 42.67, 41.14, 42.39, 43.03, 43.75, 44.83, 43.35, 42.67, 43.67, 42.67, 43.59, 43.19,
43.31, 41.58, 42.55, 45.96, 47.4, 48.17, 48.97, 50.25, 48.61, 48.37, 48.05, 48.05, 49.17, 50.98, 49.85, 56.56, 57.8,
58.84, 64.54, 66.31, 63.1, 65.06, 66.95, 65.83, 67.39, 72.25, 84.13, 89.91, 93.12, 89.91, 83.17, 76.34, 72.73,
71.93, 75.86, 72.57, 67.91, 70.04, 75.14, 74.66, 72.69, 72.89, 70.64, 63.66, 57.8, 58.92, 52.66, 57.88, 62.34,
68.76, 67.67,
] as const;
const aos = [
11.6905, 9.3535, 8.2531, 7.8816, 7.7612, 8.2594, 8.4822, 8.1794, 8.0454, 7.9502, 7.5005, 7.251, 6.5143, 5.7713,
5.2844, 4.9243, 4.0526, 3.7438, 3.8741, 4.1156, 4.5317, 5.4641, 6.2518, 6.0741, 5.6701, 5.0864, 4.3346, 3.862,
4.1222, 5.2467, 7.0596, 8.9599, 10.4984, 13.1686, 14.985, 15.7149, 16.3803, 17.1528, 16.1721, 15.3763, 18.3787,
22.3355, 25.798, 29.8361, 33.3549, 31.751, 28.244, 24.0074, 18.979, 14.7623, 11.6177, 8.6476, 7.1438, 6.6704,
5.3673, 4.5294, 4.764, 4.1044, 1.6913, -1.3769, -4.2062, -7.7196, -10.6241, -11.4972, -9.6358, -7.9344,
] as const;

describe('getResult', () => {
it('works with an interval setting of 5/34', () => {
// Test data verified with:
// https://github.com/TulipCharts/tulipindicators/blob/v0.8.0/tests/extra.txt#L17-L20
const highs = [
32.11, 27.62, 28.26, 28.02, 26.93, 26.65, 27.25, 27.58, 27.9, 28.9, 29.34, 29.82, 29.54, 29.3, 29.5, 29.5, 29.7,
29.14, 27.17, 30.34, 30.26, 30.14, 29.98, 30.55, 32.11, 34.16, 39.5, 50.78, 51.38, 51.34, 50.7, 44.23, 42.71,
39.82, 42.35, 44.71, 44.27, 43.67, 44.83, 44.55, 46.8, 46.24, 45.52, 44.55, 46.12, 44.71, 44.47, 45.28, 44.63,
43.43, 46.24, 49.13, 49.93, 49.93, 51.38, 52.78, 50.53, 50.61, 49.33, 49.41, 53.3, 52.58, 62.3, 61.57, 62.54,
64.54, 74.14, 73.17, 70.12, 68.64, 71.77, 70.64, 71.77, 104.04, 103.4, 97.62, 98.9, 99.46, 89.23, 87.34, 82.28,
78.99, 81.56, 78.03, 73.86, 77.67, 79.47, 77.71, 76.75, 78.31, 77.71, 72.25, 68.08, 66.31, 65.75, 64.14, 67.43,
80.28, 78.35,
];
const lows = [
25.69, 25.57, 25.73, 25.69, 25.69, 26.17, 26.05, 26.29, 26.89, 27.74, 28.46, 29.02, 28.46, 28.78, 29.06, 28.74,
28.5, 27.01, 26.33, 26.97, 29.18, 29.62, 29.54, 29.74, 30.47, 31.83, 34.64, 40.42, 47.68, 48.97, 43.27, 41.58,
38.73, 37.33, 37.89, 43.39, 42.67, 41.14, 42.39, 43.03, 43.75, 44.83, 43.35, 42.67, 43.67, 42.67, 43.59, 43.19,
43.31, 41.58, 42.55, 45.96, 47.4, 48.17, 48.97, 50.25, 48.61, 48.37, 48.05, 48.05, 49.17, 50.98, 49.85, 56.56,
57.8, 58.84, 64.54, 66.31, 63.1, 65.06, 66.95, 65.83, 67.39, 72.25, 84.13, 89.91, 93.12, 89.91, 83.17, 76.34,
72.73, 71.93, 75.86, 72.57, 67.91, 70.04, 75.14, 74.66, 72.69, 72.89, 70.64, 63.66, 57.8, 58.92, 52.66, 57.88,
62.34, 68.76, 67.67,
];
const aos = [
11.6905, 9.3535, 8.2531, 7.8816, 7.7612, 8.2594, 8.4822, 8.1794, 8.0454, 7.9502, 7.5005, 7.251, 6.5143, 5.7713,
5.2844, 4.9243, 4.0526, 3.7438, 3.8741, 4.1156, 4.5317, 5.4641, 6.2518, 6.0741, 5.6701, 5.0864, 4.3346, 3.862,
4.1222, 5.2467, 7.0596, 8.9599, 10.4984, 13.1686, 14.985, 15.7149, 16.3803, 17.1528, 16.1721, 15.3763, 18.3787,
22.3355, 25.798, 29.8361, 33.3549, 31.751, 28.244, 24.0074, 18.979, 14.7623, 11.6177, 8.6476, 7.1438, 6.6704,
5.3673, 4.5294, 4.764, 4.1044, 1.6913, -1.3769, -4.2062, -7.7196, -10.6241, -11.4972, -9.6358, -7.9344,
];
const ao = new AO(5, 34);
const fasterAO = new FasterAO(5, 34);
const shortInterval = 5;
const longInterval = 34;

const ao = new AO(shortInterval, longInterval);
const fasterAO = new FasterAO(shortInterval, longInterval);

for (let i = 0; i < lows.length; i++) {
const candle: HighLowNumber = {
Expand All @@ -46,7 +49,7 @@ describe('AO', () => {
expect(result).not.toBeUndefined();
expect(fasterResult).not.toBeUndefined();
const actual = ao.getResult().toFixed(4);
const expected = aos.shift()!;
const expected = aos[i - (longInterval - 1)];
expect(parseFloat(actual)).toBe(expected);
expect(fasterResult?.toFixed(4)).toBe(expected.toFixed(4));
}
Expand All @@ -70,4 +73,80 @@ describe('AO', () => {
}
});
});

describe('replace', () => {
it('replaces recently added values', () => {
const shortInterval = 5;
const longInterval = 34;

const ao = new AO(shortInterval, longInterval);
const fasterAO = new FasterAO(shortInterval, longInterval);

lows.forEach((low, index) => {
ao.update({
high: highs[index],
low,
});
fasterAO.update({
high: highs[index],
low,
});
});

expect(ao.lowest?.toFixed(2)).toBe('-11.50');
expect(ao.highest?.toFixed(2)).toBe('33.35');

expect(fasterAO.lowest?.toFixed(2)).toBe('-11.50');
expect(fasterAO.highest?.toFixed(2)).toBe('33.35');

// Add the latest value
const latestValue = {
high: 9000,
low: 0,
};
const latestResult = '749.69';
const latestLow = '-11.50';
const latestHigh = '749.69';

ao.update(latestValue);
expect(ao.getResult()?.toFixed(2)).toBe(latestResult);
expect(ao.lowest?.toFixed(2)).toBe(latestLow);
expect(ao.highest?.toFixed(2), 'new record high').toBe(latestHigh);

fasterAO.update(latestValue);
expect(fasterAO.getResult()?.toFixed(2)).toBe(latestResult);
expect(fasterAO.lowest?.toFixed(2)).toBe(latestLow);
expect(fasterAO.highest?.toFixed(2), 'new record high').toBe(latestHigh);

// Replace the latest value with some other value
const someOtherValue = {
high: 2000,
low: -2000,
};
const otherResult = '-17.96';
const otherLow = '-17.96';
const otherHigh = '33.35';

ao.replace(someOtherValue);
expect(ao.getResult()?.toFixed(2)).toBe(otherResult);
expect(ao.lowest?.toFixed(2), 'new record low').toBe(otherLow);
expect(ao.highest?.toFixed(2)).toBe(otherHigh);

fasterAO.replace(someOtherValue);
expect(fasterAO.getResult()?.toFixed(2)).toBe(otherResult);
expect(fasterAO.lowest?.toFixed(2), 'new record low').toBe(otherLow);
expect(fasterAO.highest?.toFixed(2)).toBe(otherHigh);

// Replace the other value with the latest value
ao.replace(latestValue);
expect(ao.getResult()?.toFixed(2)).toBe(latestResult);
expect(ao.lowest?.toFixed(2), 'lowest reset').toBe(latestLow);
expect(ao.highest?.toFixed(2), 'highest reset').toBe(latestHigh);

fasterAO.replace(latestValue);
expect(fasterAO.getResult()?.toFixed(2)).toBe(latestResult);
expect(fasterAO.lowest?.toFixed(2), 'lowest reset').toBe(latestLow);
expect(fasterAO.highest?.toFixed(2), 'highest reset').toBe(latestHigh);
});
});
});
16 changes: 8 additions & 8 deletions src/AO/AO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ export class AO extends BigIndicatorSeries<HighLow> {
this.long = new SmoothingIndicator(longInterval);
}

override update({low, high}: HighLow): void | Big {
override update({low, high}: HighLow, replace: boolean = false): void | Big {
const candleSum = new Big(low).add(high);
const medianPrice = candleSum.div(2);

this.short.update(medianPrice);
this.long.update(medianPrice);
this.short.update(medianPrice, replace);
this.long.update(medianPrice, replace);

if (this.long.isStable) {
return this.setResult(this.short.getResult().sub(this.long.getResult()));
return this.setResult(this.short.getResult().sub(this.long.getResult()), replace);
}
}
}
Expand All @@ -60,14 +60,14 @@ export class FasterAO extends NumberIndicatorSeries<HighLowNumber> {
this.long = new SmoothingIndicator(longInterval);
}

override update({low, high}: HighLowNumber): void | number {
override update({low, high}: HighLowNumber, replace: boolean = false): void | number {
const medianPrice = (low + high) / 2;

this.short.update(medianPrice);
this.long.update(medianPrice);
this.short.update(medianPrice, replace);
this.long.update(medianPrice, replace);

if (this.long.isStable) {
return this.setResult(this.short.getResult() - this.long.getResult());
return this.setResult(this.short.getResult() - this.long.getResult(), replace);
}
}
}
139 changes: 110 additions & 29 deletions src/ATR/ATR.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,51 @@ import {ATR, FasterATR} from './ATR.js';
import {NotEnoughDataError} from '../index.js';

describe('ATR', () => {
// Test data verified with:
// https://tulipindicators.org/atr
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 expectations = [
'1.12',
'1.05',
'1.01',
'1.21',
'1.14',
'1.09',
'1.24',
'1.23',
'1.23',
'1.21',
'1.14',
] as const;

describe('getResult', () => {
it('calculates the Average True Range (ATR)', () => {
// Test data verified with:
// https://tulipindicators.org/atr
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},
];
const expectations = ['1.12', '1.05', '1.01', '1.21', '1.14', '1.09', '1.24', '1.23', '1.23', '1.21', '1.14'];

const atr = new ATR(5);
const fasterATR = new FasterATR(5);
const interval = 5;
const atr = new ATR(interval);
const fasterATR = new FasterATR(interval);

for (const candle of candles) {
for (let i = 0; i < candles.length; i++) {
const candle = candles[i];
atr.update(candle);
fasterATR.update(candle);
if (atr.isStable && fasterATR.isStable) {
const expected = expectations.shift();
const expected = expectations[i - (interval - 1)];
expect(atr.getResult().toFixed(2)).toBe(expected!);
expect(fasterATR.getResult().toFixed(2)).toBe(expected!);
}
Expand All @@ -42,12 +56,11 @@ describe('ATR', () => {
expect(fasterATR.isStable).toBe(true);

expect(atr.getResult().toFixed(2)).toBe('1.14');
expect(fasterATR.getResult().toFixed(2)).toBe('1.14');

expect(atr.lowest?.toFixed(2)).toBe('1.01');
expect(fasterATR.lowest?.toFixed(2)).toBe('1.01');

expect(atr.highest?.toFixed(2)).toBe('1.24');

expect(fasterATR.getResult().toFixed(2)).toBe('1.14');
expect(fasterATR.lowest?.toFixed(2)).toBe('1.01');
expect(fasterATR.highest?.toFixed(2)).toBe('1.24');
});

Expand All @@ -62,4 +75,72 @@ describe('ATR', () => {
}
});
});

describe('replace', () => {
it('replaces recently added values', () => {
const interval = 5;
const atr = new ATR(interval);
const fasterATR = new FasterATR(interval);

for (const candle of candles) {
atr.update(candle);
fasterATR.update(candle);
}

expect(atr.getResult().toFixed(2)).toBe('1.14');
expect(atr.lowest?.toFixed(2)).toBe('1.01');
expect(atr.highest?.toFixed(2)).toBe('1.24');

expect(fasterATR.getResult().toFixed(2)).toBe('1.14');
expect(fasterATR.lowest?.toFixed(2)).toBe('1.01');
expect(fasterATR.highest?.toFixed(2)).toBe('1.24');

// Add the latest value
const latestValue = {close: 1337, high: 1337, low: 1337};
const latestResult = '250.85';
const latestLow = '1.01';
const latestHigh = '250.85';

atr.update(latestValue);
expect(atr.getResult().toFixed(2)).toBe(latestResult);
expect(atr.lowest?.toFixed(2)).toBe(latestLow);
expect(atr.highest?.toFixed(2), 'new record high').toBe(latestHigh);

fasterATR.update(latestValue);
expect(fasterATR.getResult().toFixed(2)).toBe(latestResult);
expect(fasterATR.lowest?.toFixed(2)).toBe(latestLow);
expect(fasterATR.highest?.toFixed(2), 'new record high').toBe(latestHigh);

// Replace the latest value with some other value
const someOtherValue = {
close: 1,
high: 1,
low: 1,
};
const otherResult = '18.17';
const otherLow = '1.01';
const otherHigh = '18.17';

atr.replace(someOtherValue);
expect(atr.getResult().toFixed(2)).toBe(otherResult);
expect(atr.lowest?.toFixed(2)).toBe(otherLow);
expect(atr.highest?.toFixed(2)).toBe(otherHigh);

fasterATR.replace(someOtherValue);
expect(fasterATR.getResult().toFixed(2)).toBe(otherResult);
expect(fasterATR.lowest?.toFixed(2)).toBe(otherLow);
expect(fasterATR.highest?.toFixed(2)).toBe(otherHigh);

// Replace the other value with the latest value
atr.replace(latestValue);
expect(atr.getResult().toFixed(2)).toBe(latestResult);
expect(atr.lowest?.toFixed(2), 'lowest reset').toBe(latestLow);
expect(atr.highest?.toFixed(2), 'highest reset').toBe(latestHigh);

fasterATR.replace(latestValue);
expect(fasterATR.getResult().toFixed(2)).toBe(latestResult);
expect(fasterATR.lowest?.toFixed(2), 'lowest reset').toBe(latestLow);
expect(fasterATR.highest?.toFixed(2), 'highest reset').toBe(latestHigh);
});
});
});
16 changes: 8 additions & 8 deletions src/ATR/ATR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ export class ATR extends BigIndicatorSeries<HighLowClose> {
this.smoothing = new SmoothingIndicator(interval);
}

override update(candle: HighLowClose): Big | void {
const trueRange = this.tr.update(candle);
this.smoothing.update(trueRange);
override update(candle: HighLowClose, replace: boolean = false): Big | void {
const trueRange = this.tr.update(candle, replace);
this.smoothing.update(trueRange, replace);
if (this.smoothing.isStable) {
return this.setResult(this.smoothing.getResult());
return this.setResult(this.smoothing.getResult(), replace);
}
}
}
Expand All @@ -63,11 +63,11 @@ export class FasterATR extends NumberIndicatorSeries<HighLowCloseNumber> {
this.smoothing = new SmoothingIndicator(interval);
}

update(candle: HighLowCloseNumber): number | void {
const trueRange = this.tr.update(candle);
this.smoothing.update(trueRange);
update(candle: HighLowCloseNumber, replace: boolean = false): number | void {
const trueRange = this.tr.update(candle, replace);
this.smoothing.update(trueRange, replace);
if (this.smoothing.isStable) {
return this.setResult(this.smoothing.getResult());
return this.setResult(this.smoothing.getResult(), replace);
}
}
}
Loading

0 comments on commit bcb73a6

Please sign in to comment.