Files
2026-10-29 11:57:29 +01:00

334 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});
});