333 lines
12 KiB
JavaScript
333 lines
12 KiB
JavaScript
// Generated, do not edit. sha256:0f8ce48674a0340e99f3a0f3bfdbe9b7b81059732682e20db22979eeeb8e67f2
|
||||
|
|
import { describe, it } from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
daysSinceGenesis, genesisMs, toDailyGrid, dailyGridSources, powerLawModel, powerLawValue, powerLawFor,
|
|||
|
|
weeklyAggregate, movingAverage, mayerSeries, powerLawSignal, wmaSignal, mayerSignal,
|
|||
|
|
} from '../../client/signals/data.js';
|
|||
|
|
import { MS_PER_DAY, WMA, MAYER } from '../../client/signals/constants.js';
|
|||
|
|
|
|||
|
|
const nabij = (werkelijk, verwacht, eps = 1e-9) =>
|
|||
|
|
assert.ok(Math.abs(werkelijk - verwacht) < eps,
|
|||
|
|
`verwacht ~${verwacht}, kreeg ${werkelijk}`);
|
|||
|
|
|
|||
|
|
const dag = iso => Date.parse(`${iso}T00:00:00Z`);
|
|||
|
|
|
|||
|
|
describe('daysSinceGenesis', () => {
|
|||
|
|
it('is 0 op het genesis-moment zelf', () => {
|
|||
|
|
assert.equal(daysSinceGenesis(genesisMs()), 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt hele dagen vanaf 2009-01-03', () => {
|
|||
|
|
assert.equal(daysSinceGenesis(dag('2009-01-04')), 1);
|
|||
|
|
assert.equal(daysSinceGenesis(dag('2009-02-02')), 30);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('levert een fractie voor een deel van een dag', () => {
|
|||
|
|
nabij(daysSinceGenesis(genesisMs() + MS_PER_DAY / 2), 0.5);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('powerLawValue', () => {
|
|||
|
|
const fit = powerLawModel();
|
|||
|
|
|
|||
|
|
it('gebruikt de coëfficiënten uit de SSOT', () => {
|
|||
|
|
assert.deepEqual(fit, { A: 1e-17, n: 5.8 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('is exact A op dag 1 (want 1^n = 1)', () => {
|
|||
|
|
nabij(powerLawValue(fit, dag('2009-01-04')), fit.A, 1e-25);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('schaalt als een machtsfunctie: dagen verdubbelen ⇒ ×2^n', () => {
|
|||
|
|
const t1 = genesisMs() + 1000 * MS_PER_DAY;
|
|||
|
|
const t2 = genesisMs() + 2000 * MS_PER_DAY;
|
|||
|
|
nabij(powerLawValue(fit, t2) / powerLawValue(fit, t1), Math.pow(2, fit.n), 1e-6);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft een plausibele orde van grootte voor een recente datum', () => {
|
|||
|
|
const waarde = powerLawValue(fit, dag('2025-01-01'));
|
|||
|
|
assert.ok(waarde > 10_000 && waarde < 1_000_000, `kreeg ${waarde}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('loopt monotoon op in de tijd', () => {
|
|||
|
|
const a = powerLawValue(fit, dag('2020-01-01'));
|
|||
|
|
const b = powerLawValue(fit, dag('2021-01-01'));
|
|||
|
|
assert.ok(b > a);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('toDailyGrid', () => {
|
|||
|
|
const rijen = [
|
|||
|
|
{ observedAt: '2024-03-03T09:00:00Z', fiatPrice: 300 },
|
|||
|
|
{ observedAt: '2024-03-01T10:00:00Z', fiatPrice: 100 },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
it('weigert lege invoer', () => {
|
|||
|
|
assert.throws(() => toDailyGrid([]), /Geen koershistorie/);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('sorteert oplopend en zet elke stempel op 00:00Z', () => {
|
|||
|
|
const grid = toDailyGrid(rijen);
|
|||
|
|
assert.equal(grid[0][0], dag('2024-03-01'));
|
|||
|
|
assert.ok(grid.every(([ts]) => ts % MS_PER_DAY === 0));
|
|||
|
|
assert.ok(grid.every(([ts], i) => i === 0 || ts > grid[i - 1][0]));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('vult ontbrekende dagen vooruit met de laatst bekende koers', () => {
|
|||
|
|
const grid = toDailyGrid(rijen);
|
|||
|
|
assert.deepEqual(grid, [
|
|||
|
|
[dag('2024-03-01'), 100],
|
|||
|
|
[dag('2024-03-02'), 100],
|
|||
|
|
[dag('2024-03-03'), 300],
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('negeert null en niet-positieve koersen en vult het gat vooruit', () => {
|
|||
|
|
const metGaten = [
|
|||
|
|
{ observedAt: '2024-03-01T10:00:00Z', fiatPrice: 100 },
|
|||
|
|
{ observedAt: '2024-03-02T10:00:00Z', fiatPrice: null },
|
|||
|
|
{ observedAt: '2024-03-03T10:00:00Z', fiatPrice: 0 },
|
|||
|
|
{ observedAt: '2024-03-04T10:00:00Z', fiatPrice: 400 },
|
|||
|
|
];
|
|||
|
|
assert.deepEqual(toDailyGrid(metGaten).map(([, v]) => v), [100, 100, 100, 400]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rekt het grid niet op tot een dag zonder geldige koers', () => {
|
|||
|
|
const eindigtOpNull = [
|
|||
|
|
{ observedAt: '2024-03-01T10:00:00Z', fiatPrice: 100 },
|
|||
|
|
{ observedAt: '2024-03-02T10:00:00Z', fiatPrice: null },
|
|||
|
|
];
|
|||
|
|
assert.deepEqual(toDailyGrid(eindigtOpNull), [[dag('2024-03-01'), 100]]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('houdt bij twee rijen op dezelfde dag de laatste uit de invoer aan', () => {
|
|||
|
|
const zelfdeDag = [
|
|||
|
|
{ observedAt: '2024-03-01T09:00:00Z', fiatPrice: 100 },
|
|||
|
|
{ observedAt: '2024-03-01T08:00:00Z', fiatPrice: 200 },
|
|||
|
|
];
|
|||
|
|
assert.equal(toDailyGrid(zelfdeDag)[0][1], 200);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('dailyGridSources', () => {
|
|||
|
|
it('kiest per dag dezelfde rij als toDailyGrid, ook bij twee bronnen op één dag', () => {
|
|||
|
|
const rijen = [
|
|||
|
|
{ observedAt: '2024-03-01T00:00:00Z', fiatPrice: 100, providerId: 7 },
|
|||
|
|
{ observedAt: '2024-03-01T00:00:00Z', fiatPrice: 101, providerId: 9 },
|
|||
|
|
{ observedAt: '2024-03-03T00:00:00Z', fiatPrice: 300, providerId: 7 },
|
|||
|
|
];
|
|||
|
|
const bronnen = dailyGridSources(rijen);
|
|||
|
|
const grid = new Map(toDailyGrid(rijen));
|
|||
|
|
assert.equal(bronnen.get(dag('2024-03-01')).providerId, 9);
|
|||
|
|
assert.equal(grid.get(dag('2024-03-01')), 101);
|
|||
|
|
const om = [...rijen].reverse();
|
|||
|
|
assert.equal(dailyGridSources(om).get(dag('2024-03-01')).providerId, 7);
|
|||
|
|
assert.equal(new Map(toDailyGrid(om)).get(dag('2024-03-01')), 100);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een doorgetrokken dag en een rij zonder geldige koers weg', () => {
|
|||
|
|
const rijen = [
|
|||
|
|
{ observedAt: '2024-03-01T00:00:00Z', fiatPrice: 100, providerId: 7 },
|
|||
|
|
{ observedAt: '2024-03-02T00:00:00Z', fiatPrice: 0, providerId: 9 },
|
|||
|
|
{ observedAt: '2024-03-03T00:00:00Z', fiatPrice: null, providerId: 9 },
|
|||
|
|
{ observedAt: '2024-03-04T00:00:00Z', fiatPrice: 400, providerId: 7 },
|
|||
|
|
];
|
|||
|
|
assert.deepEqual([...dailyGridSources(rijen)].map(([ms, b]) => [ms, b.providerId]),
|
|||
|
|
[[dag('2024-03-01'), 7], [dag('2024-03-04'), 7]]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('neemt de bron achter een koers van de dienst mee, en null waar die er niet is', () => {
|
|||
|
|
const rijen = [
|
|||
|
|
{ observedAt: '2024-03-01T00:00:00Z', fiatPrice: 100, providerId: 7, sourceName: 'Bitvavo' },
|
|||
|
|
{ observedAt: '2024-03-02T00:00:00Z', fiatPrice: 101, providerId: 9 },
|
|||
|
|
];
|
|||
|
|
assert.deepEqual([...dailyGridSources(rijen)].map(([, b]) => b.sourceName), ['Bitvavo', null]);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('movingAverage', () => {
|
|||
|
|
const reeks = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]];
|
|||
|
|
|
|||
|
|
it('geeft null tot het venster gevuld is', () => {
|
|||
|
|
const ma = movingAverage(reeks, 3);
|
|||
|
|
assert.deepEqual(ma.map(d => d.y), [null, null, 2, 3, 4]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('behoudt de x-waarden van de invoer', () => {
|
|||
|
|
assert.deepEqual(movingAverage(reeks, 2).map(d => d.x), [0, 1, 2, 3, 4]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('is met venster 1 gelijk aan de reeks zelf', () => {
|
|||
|
|
assert.deepEqual(movingAverage(reeks, 1).map(d => d.y), [1, 2, 3, 4, 5]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('schuift het venster echt mee (geen cumulatieve som)', () => {
|
|||
|
|
assert.equal(movingAverage(reeks, 3).at(-1).y, 4);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('weeklyAggregate', () => {
|
|||
|
|
it('geeft een lege reeks terug voor lege invoer', () => {
|
|||
|
|
assert.deepEqual(weeklyAggregate([]), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('bundelt zeven dagen tot één weekgemiddelde', () => {
|
|||
|
|
const daily = [];
|
|||
|
|
for (let i = 0; i < 7; i++) daily.push([dag('2024-01-04') + i * MS_PER_DAY, i + 1]);
|
|||
|
|
const weekly = weeklyAggregate(daily);
|
|||
|
|
assert.equal(weekly.length, 1);
|
|||
|
|
assert.equal(weekly[0][0], dag('2024-01-04'));
|
|||
|
|
nabij(weekly[0][1], 4);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('begint een nieuwe bucket op de volgende donderdag', () => {
|
|||
|
|
const daily = [];
|
|||
|
|
for (let i = 0; i < 8; i++) daily.push([dag('2024-01-04') + i * MS_PER_DAY, 10]);
|
|||
|
|
const weekly = weeklyAggregate(daily);
|
|||
|
|
assert.equal(weekly.length, 2);
|
|||
|
|
assert.equal(weekly[1][0], dag('2024-01-11'));
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('mayerSeries', () => {
|
|||
|
|
it('deelt de koers door het voortschrijdend gemiddelde', () => {
|
|||
|
|
const daily = [[0, 1], [1, 2], [2, 3], [3, 4]];
|
|||
|
|
const mayer = mayerSeries(daily, 2);
|
|||
|
|
assert.equal(mayer[0].y, null);
|
|||
|
|
nabij(mayer[1].y, 2 / 1.5);
|
|||
|
|
nabij(mayer[2].y, 3 / 2.5);
|
|||
|
|
nabij(mayer[3].y, 4 / 3.5);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('powerLawSignal', () => {
|
|||
|
|
const platteFit = { A: 1, n: 0 };
|
|||
|
|
const laatste = prijs => [[dag('2024-01-01'), prijs]];
|
|||
|
|
|
|||
|
|
it('sterk koopsignaal onder de ondergrens', () => {
|
|||
|
|
assert.deepEqual(powerLawSignal(laatste(0.4), platteFit, 0.5, 2),
|
|||
|
|
{ color: 'green', key: 'plStrong' });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('koopsignaal tussen ondergrens en fit-lijn', () => {
|
|||
|
|
assert.deepEqual(powerLawSignal(laatste(0.9), platteFit, 0.5, 2),
|
|||
|
|
{ color: 'green', key: 'plBuy' });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geel tussen fit-lijn en bovengrens', () => {
|
|||
|
|
assert.deepEqual(powerLawSignal(laatste(1.5), platteFit, 0.5, 2),
|
|||
|
|
{ color: 'yellow', key: 'plAbove' });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rood boven de bovengrens', () => {
|
|||
|
|
assert.deepEqual(powerLawSignal(laatste(3), platteFit, 0.5, 2),
|
|||
|
|
{ color: 'red', key: 'plFar' });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('de grenzen zelf vallen in de gunstigste categorie (<=)', () => {
|
|||
|
|
assert.equal(powerLawSignal(laatste(0.5), platteFit, 0.5, 2).key, 'plStrong');
|
|||
|
|
assert.equal(powerLawSignal(laatste(1), platteFit, 0.5, 2).key, 'plBuy');
|
|||
|
|
assert.equal(powerLawSignal(laatste(2), platteFit, 0.5, 2).key, 'plAbove');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('powerLawFor', () => {
|
|||
|
|
const yen = [[dag('2024-01-01'), 100], [dag('2024-01-02'), 100], [dag('2024-01-03'), 150]];
|
|||
|
|
|
|||
|
|
it('laat EUR en USD ongewijzigd: het model zelf, met hun eigen bovengrens', () => {
|
|||
|
|
assert.deepEqual(powerLawFor('EUR'), { fit: powerLawModel(), upper: 4.5 });
|
|||
|
|
assert.deepEqual(powerLawFor('usd'), { fit: powerLawModel(), upper: 5.0 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft geen model in een andere munt zonder dollarkoersen', () => {
|
|||
|
|
assert.equal(powerLawFor('JPY'), null);
|
|||
|
|
assert.equal(powerLawFor('JPY', []), null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rekent een andere munt via dollars: het dollarmodel maal de koers van die dag', () => {
|
|||
|
|
const { fit, upper } = powerLawFor('JPY', yen);
|
|||
|
|
const usd = powerLawModel();
|
|||
|
|
assert.equal(upper, 5.0, 'de bovengrens van de dollar');
|
|||
|
|
nabij(powerLawValue(fit, dag('2024-01-02')) / powerLawValue(usd, dag('2024-01-02')), 100, 1e-9);
|
|||
|
|
nabij(powerLawValue(fit, dag('2024-01-03')) / powerLawValue(usd, dag('2024-01-03')), 150, 1e-9);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('neemt vóór de eerste koers de eerste en ná de laatste de laatste', () => {
|
|||
|
|
const { fit } = powerLawFor('JPY', yen);
|
|||
|
|
const usd = powerLawModel();
|
|||
|
|
nabij(powerLawValue(fit, dag('2015-06-01')) / powerLawValue(usd, dag('2015-06-01')), 100, 1e-9);
|
|||
|
|
nabij(powerLawValue(fit, dag('2025-06-01')) / powerLawValue(usd, dag('2025-06-01')), 150, 1e-9);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft geel en geen rood voor een koers anderhalf keer boven de lijn, ook in yen', () => {
|
|||
|
|
const { fit, upper } = powerLawFor('JPY', yen);
|
|||
|
|
const ts = dag('2024-01-03');
|
|||
|
|
const prijs = 1.5 * powerLawValue(fit, ts);
|
|||
|
|
assert.deepEqual(powerLawSignal([[ts, prijs]], fit, 0.47, upper), { color: 'yellow', key: 'plAbove' });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('wmaSignal', () => {
|
|||
|
|
const weken = (aantal, prijs) =>
|
|||
|
|
Array.from({ length: aantal }, (_, i) => [i * 7 * MS_PER_DAY, prijs]);
|
|||
|
|
|
|||
|
|
it('geeft null zolang er geen 200 weken historie is', () => {
|
|||
|
|
assert.equal(wmaSignal(weken(WMA.WEEKS - 1, 100)), null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft null bij lege invoer', () => {
|
|||
|
|
assert.equal(wmaSignal([]), null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('groen als de koers op of onder de signaallijn ligt', () => {
|
|||
|
|
const sig = wmaSignal(weken(WMA.WEEKS, 100));
|
|||
|
|
assert.equal(sig.color, 'green');
|
|||
|
|
assert.equal(sig.key, 'wmaBuy');
|
|||
|
|
nabij(sig.mult, 1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geel tussen de signaallijn en de rode drempel', () => {
|
|||
|
|
const reeks = [...weken(WMA.WEEKS, 100), [WMA.WEEKS * 7 * MS_PER_DAY, 130]];
|
|||
|
|
const sig = wmaSignal(reeks);
|
|||
|
|
assert.equal(sig.color, 'yellow');
|
|||
|
|
assert.equal(sig.key, 'wmaAbove');
|
|||
|
|
assert.ok(sig.mult > WMA.SIGNAL_MULT && sig.mult < WMA.RED_MULT, `mult=${sig.mult}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rood boven de rode drempel', () => {
|
|||
|
|
const reeks = [...weken(WMA.WEEKS, 100), [WMA.WEEKS * 7 * MS_PER_DAY, 200]];
|
|||
|
|
const sig = wmaSignal(reeks);
|
|||
|
|
assert.equal(sig.color, 'red');
|
|||
|
|
assert.equal(sig.key, 'wmaFar');
|
|||
|
|
assert.ok(sig.mult >= WMA.RED_MULT);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('mayerSignal', () => {
|
|||
|
|
const punt = y => [{ x: 0, y: null }, { x: 1, y }];
|
|||
|
|
|
|||
|
|
it('groen onder de groene drempel', () => {
|
|||
|
|
const sig = mayerSignal(punt(MAYER.GREEN_BELOW - 0.1));
|
|||
|
|
assert.equal(sig.color, 'green');
|
|||
|
|
assert.equal(sig.key, 'mayerLow');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geel tussen de drempels', () => {
|
|||
|
|
assert.equal(mayerSignal(punt(1.5)).key, 'mayerMid');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rood op of boven de rode drempel', () => {
|
|||
|
|
assert.equal(mayerSignal(punt(MAYER.RED_ABOVE)).key, 'mayerHigh');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('negeert trailing nulls en pakt het laatste gevulde punt', () => {
|
|||
|
|
const reeks = [{ x: 0, y: 0.5 }, { x: 1, y: null }];
|
|||
|
|
assert.equal(mayerSignal(reeks).key, 'mayerLow');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft null als er geen enkel gevuld punt is', () => {
|
|||
|
|
assert.equal(mayerSignal([{ x: 0, y: null }]), null);
|
|||
|
|
});
|
|||
|
|
});
|