Versie 0.2.0

This commit is contained in:
Atreides Assets committed 2026-10-29 11:57:29 +01:00
commit e77d8fd373
495 files changed
+81026

No files matched your search

+676
View File
@@ -0,0 +1,676 @@
// Generated, do not edit. sha256:f793f1998c617a8dfc8fce95de7c88d4b4d4d1a08ecaa3c19643f70bcb8ebab4
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import { toSearchName } from '../../shared/search-name.js';
import { createDbHarness } from '../helpers/db-node.mjs';
import { CONFIG, ENUMS } from '../../shared/config.js';
const harness = createDbHarness();
mock.module('../../server/db.js', { namedExports: harness.exports });
const {
addManualPricePoint, getPriceAt, getPricePoint, latestPricePoint, latestSpotPoint, listAllPriceHistory,
listPriceGaps, listPriceHistory, priceHistoryCount, recordCurrentSpot, upsertPriceHistory,
ignorePriceGap, listIgnoredPriceGaps, unignorePriceGap, recordNoPriceDays, listNoPriceDays,
listPriceAbsences, deletePriceAbsences,
} = await import('../../server/data/price-history.js');
const DREMPEL = CONFIG.PRICE_HISTORY.NEAREST_MAX_SECONDS;
let btc, eth;
beforeEach(() => {
harness.recreate();
providerIds.clear();
providerId('mempool'); providerId('bitvavo');
btc = asset('BTC');
eth = asset('ETH');
dekking('mempool', 'BTC', 'ETH');
dekking('bitvavo', 'BTC', 'ETH');
});
function asset(symbol) {
harness.exports.run(
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES (?,?,?,'crypto',?,NULL,8,'unit')`, [symbol, symbol, toSearchName(symbol), symbol]);
return harness.exports.lastId();
}
function provider(key) {
harness.exports.run(
`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired,
isActive, isSeed, isSystem, createdAt)
VALUES (?,?,?,'','none',0,1,1,0,'2024-01-01T00:00:00Z')`, [key, key, toSearchName(key)]);
return harness.exports.lastId();
}
const providerIds = new Map();
const providerId = key => {
if (!providerIds.has(key)) providerIds.set(key, provider(key));
return providerIds.get(key);
};
function dekking(key, ...symbolen) {
for (const s of symbolen)
harness.exports.run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?,?)`, [providerId(key), s]);
}
function systeemProvider(key) {
harness.exports.run(
`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired,
isActive, isSeed, isSystem, createdAt)
VALUES (?,?,?,'','none',0,1,1,1,'2024-01-01T00:00:00Z')`, [key, key, toSearchName(key)]);
providerIds.set(key, harness.exports.lastId());
return providerIds.get(key);
}
function punt({ assetId = null, observedAt, resolution = 'day', fiatPrice = 40000, source = 'mempool' }) {
harness.exports.run(
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
VALUES (?,?,?,?,?,?)`, [assetId ?? btc, observedAt, resolution, fiatPrice, providerId(source), observedAt]);
return harness.exports.lastId();
}
const unix = iso => Math.floor(Date.parse(iso) / 1000);
const vanaf = (momentIso, sec) => new Date((unix(momentIso) + sec) * 1000).toISOString();
describe('upsertPriceHistory', () => {
it('valideert de resolutie tegen de enum', async () => {
assert.deepEqual([...ENUMS.PRICE_RESOLUTION], ['day', 'timestamp']);
await assert.rejects(
() => upsertPriceHistory(btc, [{ time: unix('2024-01-01T12:00:00Z'), price: 1 }], 'mempool', 'uurtje'),
/resolution/);
});
it('normaliseert een dag-punt naar middernacht UTC', async () => {
await upsertPriceHistory(btc, [{ time: unix('2024-03-05T14:37:11Z'), price: 1000 }], 'mempool', 'day');
assert.equal(harness.exports.one(`SELECT observedAt FROM PriceHistory`).observedAt,
'2024-03-05T00:00:00Z');
});
it('bewaart bij een timestamp-punt het exacte moment', async () => {
await upsertPriceHistory(btc, [{ time: unix('2024-03-05T14:37:11Z'), price: 1000 }], 'mempool', 'timestamp');
assert.equal(harness.exports.one(`SELECT observedAt FROM PriceHistory`).observedAt,
'2024-03-05T14:37:11.000Z');
});
it('werkt een bestaand punt bij in plaats van te dupliceren', async () => {
const t = unix('2024-03-05T00:00:00Z');
await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day');
await upsertPriceHistory(btc, [{ time: t, price: 2000 }], 'mempool', 'day');
const rijen = harness.exports.all(`SELECT fiatPrice FROM PriceHistory`);
assert.equal(rijen.length, 1, 'ON CONFLICT werkt bij, dus geen tweede rij');
assert.equal(rijen[0].fiatPrice, 2000, 'de nieuwste waarde wint');
});
it('bewaart de bron achter een koers van de dienst, en werkt hem bij met de koers', async () => {
const t = unix('2024-03-05T00:00:00Z');
const bron = () => harness.exports.one(`SELECT sourceName FROM PriceHistory`).sourceName;
await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day');
assert.equal(bron(), null, 'zonder bron blijft hij leeg');
await upsertPriceHistory(btc, [{ time: t, price: 1001, sourceName: 'Bitvavo' }], 'mempool', 'day');
assert.equal(bron(), 'Bitvavo');
await upsertPriceHistory(btc, [{ time: t, price: 1002, sourceName: 'ECB' }], 'mempool', 'day');
assert.equal(bron(), 'ECB', 'een nieuwere koers brengt zijn eigen bron mee');
});
it('zet createdAt bij het aanmaken en laat hem staan bij bijwerken', async () => {
const t = unix('2024-03-05T00:00:00Z');
await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day');
const eerst = harness.exports.one(`SELECT createdAt FROM PriceHistory`).createdAt;
assert.ok(eerst, 'een nieuwe rij heeft een createdAt');
harness.exports.run(`UPDATE PriceHistory SET createdAt = '2024-03-06T08:00:00.000Z'`);
await upsertPriceHistory(btc, [{ time: t, price: 2000 }], 'mempool', 'day');
const r = harness.exports.one(`SELECT createdAt, fiatPrice FROM PriceHistory`);
assert.equal(r.fiatPrice, 2000);
assert.equal(r.createdAt, '2024-03-06T08:00:00.000Z', 'bijwerken raakt createdAt niet');
});
it('houdt reeksen van verschillende bronnen naast elkaar', async () => {
const t = unix('2024-03-05T00:00:00Z');
await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day');
await upsertPriceHistory(btc, [{ time: t, price: 1010 }], 'bitvavo', 'day');
assert.equal(priceHistoryCount(btc), 2, 'de bron zit in de unieke sleutel');
});
it('slaat een punt zonder koers over in plaats van te weigeren', async () => {
await upsertPriceHistory(btc, [
{ time: unix('2024-03-05T00:00:00Z') },
{ time: unix('2024-03-06T00:00:00Z'), price: 1100 },
], 'mempool', 'day');
const rijen = harness.exports.all(`SELECT observedAt, fiatPrice FROM PriceHistory`);
assert.deepEqual(rijen.map(r => [r.observedAt, r.fiatPrice]), [['2024-03-06T00:00:00Z', 1100]]);
});
it('slaat een koers van nul of lager over', async () => {
await upsertPriceHistory(btc, [{ time: unix('2012-03-05T00:00:00Z'), price: 0 }], 'mempool', 'day');
await upsertPriceHistory(btc, [{ time: unix('2012-03-06T00:00:00Z'), price: 4 }], 'mempool', 'day');
const rijen = harness.exports.all(`SELECT fiatPrice FROM PriceHistory ORDER BY observedAt`);
assert.deepEqual(rijen.map(r => r.fiatPrice), [4]);
});
it('slaat een punt zonder positieve koers over, ook een lege string', async () => {
await upsertPriceHistory(btc, [
{ time: unix('2012-03-05T00:00:00Z'), price: 0 },
{ time: unix('2012-03-06T00:00:00Z'), price: '' },
{ time: unix('2012-03-07T00:00:00Z'), price: -3 },
], 'mempool', 'day');
assert.equal(priceHistoryCount(btc), 0);
});
it('laat een opgeslagen koers staan als dezelfde dag later met nul terugkomt', async () => {
const t = unix('2024-03-05T00:00:00Z');
await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day');
await upsertPriceHistory(btc, [{ time: t, price: 0 }], 'mempool', 'day');
const r = harness.exports.one(`SELECT fiatPrice FROM PriceHistory`);
assert.equal(r.fiatPrice, 1000);
});
});
describe('listPriceHistory / priceHistoryCount / getPricePoint', () => {
it('sorteert nieuwste eerst en filtert op asset', () => {
punt({ observedAt: '2024-01-01T00:00:00Z' });
punt({ observedAt: '2024-03-01T00:00:00Z' });
punt({ assetId: eth, observedAt: '2024-02-01T00:00:00Z' });
assert.deepEqual(listPriceHistory(btc).map(r => r.observedAt),
['2024-03-01T00:00:00Z', '2024-01-01T00:00:00Z']);
});
it('filtert vanaf een datum, grens inbegrepen', () => {
punt({ observedAt: '2024-01-01T00:00:00Z' });
punt({ observedAt: '2024-02-01T00:00:00Z' });
assert.equal(listPriceHistory(btc, '2024-02-01').length, 1);
assert.equal(listPriceHistory(btc, '2024-01-01').length, 2, 'de begindag telt mee');
});
it('telt per asset', () => {
punt({ observedAt: '2024-01-01T00:00:00Z' });
punt({ assetId: eth, observedAt: '2024-01-01T00:00:00Z' });
assert.equal(priceHistoryCount(btc), 1);
assert.equal(priceHistoryCount(eth), 1);
});
it('getPricePoint vindt een dagpunt op datum', () => {
punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 42000 });
assert.equal(getPricePoint(btc, '2024-01-01').fiatPrice, 42000);
assert.equal(getPricePoint(btc, '2024-01-02'), null);
});
it('getPricePoint valt zonder dagpunt terug op de laatste spot van die dag, gemarkeerd', () => {
punt({ observedAt: '2024-01-01T09:00:00Z', resolution: 'timestamp', fiatPrice: 1 });
punt({ observedAt: '2024-01-01T21:00:00Z', resolution: 'timestamp', fiatPrice: 2 });
const p = getPricePoint(btc, '2024-01-01');
assert.equal(p.fiatPrice, 2, 'de LAATSTE spot van de dag');
assert.equal(p.derivedFrom, 'spot');
});
it('getPricePoint laat een echt dagpunt voorgaan, en geeft met derived:false alleen dat', () => {
punt({ observedAt: '2024-01-01T21:00:00Z', resolution: 'timestamp', fiatPrice: 2 });
assert.equal(getPricePoint(btc, '2024-01-01', { derived: false }), null);
punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 5 });
const p = getPricePoint(btc, '2024-01-01');
assert.equal(p.fiatPrice, 5);
assert.equal(p.derivedFrom, undefined);
});
it('getPricePoint neemt bij meerdere bronnen de laatst toegevoegde rij', () => {
punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 100, source: 'mempool' });
punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 200, source: 'bitvavo' });
assert.equal(getPricePoint(btc, '2024-01-01').fiatPrice, 200);
});
});
describe('getPriceAt', () => {
const MOMENT = '2024-06-15T12:00:00Z';
it('geeft null zonder enig punt', () => {
assert.equal(getPriceAt(btc, unix(MOMENT)), null);
});
it('kiest een timestamp-punt binnen de drempel', () => {
punt({ observedAt: vanaf(MOMENT, -30), resolution: 'timestamp', fiatPrice: 111 });
const r = getPriceAt(btc, unix(MOMENT));
assert.equal(r.fiatPrice, 111);
assert.equal(r.resolution, 'timestamp');
assert.equal(r.dist_seconds, 30, 'de afstand wordt meegegeven');
});
it('neemt een timestamp-punt buiten de drempel niet als moment-koers, wel als koers van die dag', () => {
punt({ observedAt: new Date((unix(MOMENT) - DREMPEL - 60) * 1000).toISOString(), resolution: 'timestamp', fiatPrice: 111 });
const p = getPriceAt(btc, unix(MOMENT));
assert.equal(p.fiatPrice, 111);
assert.equal(p.derivedFrom, 'spot');
assert.equal(p.dist_seconds, undefined, 'geen moment-treffer, dus geen afstand');
});
it('geeft niets als er op die dag geen enkele koers is', () => {
punt({ observedAt: '2020-01-01T12:00:00Z', resolution: 'timestamp', fiatPrice: 111 });
assert.equal(getPriceAt(btc, unix(MOMENT)), null);
});
it('kiest van twee timestamp-punten het naaste, ook als dat later ligt', () => {
punt({ observedAt: vanaf(MOMENT, -Math.floor(DREMPEL * 0.8)), resolution: 'timestamp', fiatPrice: 100 });
punt({ observedAt: vanaf(MOMENT, Math.floor(DREMPEL * 0.3)), resolution: 'timestamp', fiatPrice: 200 });
assert.equal(getPriceAt(btc, unix(MOMENT)).fiatPrice, 200, 'dichterbij weegt zwaarder dan eerder');
});
it('valt terug op een dagpunt van dezelfde kalenderdag', () => {
punt({ observedAt: '2024-06-15T00:00:00Z', fiatPrice: 300 });
const r = getPriceAt(btc, unix(MOMENT));
assert.equal(r.fiatPrice, 300);
assert.equal(r.resolution, 'day');
assert.equal(r.dist_seconds, null, 'een dagpunt heeft geen afstand');
});
it('gebruikt geen dagpunt van een andere dag', () => {
punt({ observedAt: '2024-06-14T00:00:00Z', fiatPrice: 300 });
assert.equal(getPriceAt(btc, unix(MOMENT)), null);
});
it('geeft een timestamp-punt voorrang boven een dagpunt', () => {
punt({ observedAt: '2024-06-15T00:00:00Z', fiatPrice: 300 });
punt({ observedAt: vanaf(MOMENT, -30), resolution: 'timestamp', fiatPrice: 111 });
assert.equal(getPriceAt(btc, unix(MOMENT)).fiatPrice, 111);
});
describe('voorkeursbron', () => {
it('kiest de voorkeursbron ook als een andere bron naderbij ligt', () => {
punt({ observedAt: vanaf(MOMENT, Math.floor(DREMPEL * 0.3)), resolution: 'timestamp', fiatPrice: 100, source: 'mempool' });
punt({ observedAt: vanaf(MOMENT, -Math.floor(DREMPEL * 0.8)), resolution: 'timestamp', fiatPrice: 200, source: 'bitvavo' });
assert.equal(getPriceAt(btc, unix(MOMENT), 'bitvavo').fiatPrice, 200,
'de bron-matrix bepaalt welke provider leidend is');
});
it('valt binnen dezelfde resolutie terug op een andere bron', () => {
punt({ observedAt: vanaf(MOMENT, -30), resolution: 'timestamp', fiatPrice: 100, source: 'mempool' });
assert.equal(getPriceAt(btc, unix(MOMENT), 'bitvavo').fiatPrice, 100,
'liever een punt van een andere bron dan geen koers');
});
it('gaat niet van resolutie af om de voorkeursbron te volgen', () => {
punt({ observedAt: vanaf(MOMENT, -30), resolution: 'timestamp', fiatPrice: 100, source: 'mempool' });
punt({ observedAt: '2024-06-15T00:00:00Z', fiatPrice: 999, source: 'bitvavo' });
assert.equal(getPriceAt(btc, unix(MOMENT), 'bitvavo').fiatPrice, 100);
});
});
});
describe('latestPricePoint', () => {
it('geeft het nieuwste punt, ongeacht de resolutie', () => {
punt({ observedAt: '2026-01-01T00:00:00Z', fiatPrice: 100 });
punt({ observedAt: '2026-03-01T12:00:00Z', resolution: 'timestamp', fiatPrice: 300 });
punt({ observedAt: '2026-02-01T00:00:00Z', fiatPrice: 200 });
assert.equal(latestPricePoint(btc).fiatPrice, 300);
});
it('geeft null als er niets staat, en niet nul', () => {
assert.equal(latestPricePoint(eth), null);
});
it('kijkt niet over de asset-grens heen', () => {
punt({ assetId: eth, observedAt: '2027-01-01T00:00:00Z', fiatPrice: 999 });
punt({ observedAt: '2026-01-01T00:00:00Z', fiatPrice: 100 });
assert.equal(latestPricePoint(btc).fiatPrice, 100, 'het nieuwere punt van een ANDER asset hoort niet mee');
});
it('kiest bij twee punten op hetzelfde moment de laatst geschreven rij', () => {
punt({ observedAt: '2026-01-01T00:00:00Z', fiatPrice: 100, source: 'mempool' });
punt({ observedAt: '2026-01-01T00:00:00Z', fiatPrice: 111, source: 'bitvavo' });
assert.equal(latestPricePoint(btc).fiatPrice, 111);
});
});
describe('latestSpotPoint', () => {
it('geeft de nieuwste spot van die ene bron', () => {
punt({ observedAt: '2026-09-25T08:00:00.000Z', resolution: 'timestamp', fiatPrice: 1 });
punt({ observedAt: '2026-09-25T09:00:00.000Z', resolution: 'timestamp', fiatPrice: 2 });
assert.equal(latestSpotPoint(btc, 'mempool').fiatPrice, 2);
});
it('neemt de spot van een andere bron niet, ook niet als die nieuwer is', () => {
punt({ observedAt: '2026-09-25T08:00:00.000Z', resolution: 'timestamp', fiatPrice: 1 });
punt({ observedAt: '2026-09-25T09:00:00.000Z', resolution: 'timestamp', fiatPrice: 2, source: 'bitvavo' });
assert.equal(latestSpotPoint(btc, 'mempool').fiatPrice, 1);
});
it('neemt geen dagrij, geen ander asset en geen koers van nul', () => {
punt({ observedAt: '2026-09-25T00:00:00Z', fiatPrice: 5 });
punt({ assetId: eth, observedAt: '2026-09-25T09:00:00.000Z', resolution: 'timestamp', fiatPrice: 6 });
punt({ observedAt: '2026-09-25T10:00:00.000Z', resolution: 'timestamp', fiatPrice: 0 });
assert.equal(latestSpotPoint(btc, 'mempool'), null);
});
it('geeft null bij een onbekende bron in plaats van te gooien', () => {
assert.equal(latestSpotPoint(btc, 'bestaatniet'), null);
});
});
describe('listAllPriceHistory', () => {
beforeEach(() => {
punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 1 });
punt({ observedAt: '2024-03-01T00:00:00Z', fiatPrice: 2 });
punt({ assetId: eth, observedAt: '2024-02-01T00:00:00Z', fiatPrice: 3 });
});
it('geeft zonder filter alles, nieuwste eerst, met assetsymbool', () => {
const rijen = listAllPriceHistory();
assert.equal(rijen.length, 3);
assert.deepEqual(rijen.map(r => r.fiatPrice), [2, 3, 1]);
assert.equal(rijen[0].asset_symbol, 'BTC');
});
it('filtert op asset', () => {
assert.deepEqual(listAllPriceHistory({ assetId: eth }).map(r => r.fiatPrice), [3]);
});
it('filtert vanaf een datum', () => {
assert.deepEqual(listAllPriceHistory({ fromDate: '2024-02-01' }).map(r => r.fiatPrice), [2, 3]);
});
it('combineert beide filters', () => {
assert.deepEqual(listAllPriceHistory({ assetId: btc, fromDate: '2024-02-01' }).map(r => r.fiatPrice), [2]);
});
});
describe('dekkingsguard', () => {
const vreemdeRij = (extra = {}) => {
harness.exports.run(`DELETE FROM ProviderCoverage WHERE providerId = ? AND symbol = 'ETH'`,
[providerId('mempool')]);
return punt({ assetId: eth, observedAt: '2024-06-15T00:00:00Z', fiatPrice: 74500, source: 'mempool', ...extra });
};
it('laat een rij van een niet-dekkende bron geen dagpunt worden', () => {
vreemdeRij();
assert.equal(getPricePoint(eth, '2024-06-15'), null,
'mempool dekt geen ETH, dus deze rij is geen ETH-koers');
});
it('laat zo n rij ook getPriceAt niet winnen, op geen van beide resoluties', () => {
vreemdeRij();
vreemdeRij({ observedAt: '2024-06-15T12:00:00Z', resolution: 'timestamp' });
assert.equal(getPriceAt(eth, unix('2024-06-15T12:00:00Z')), null);
});
it('houdt hem uit listPriceHistory, en daarmee uit het daggrid van signalen en de spot-widget', () => {
vreemdeRij();
assert.deepEqual(listPriceHistory(eth), []);
assert.deepEqual(listPriceHistory(eth, '2024-01-01'), [], 'ook met een vanaf-datum');
});
it('houdt hem uit latestPricePoint', () => {
vreemdeRij();
assert.equal(latestPricePoint(eth), null);
});
it('laat een dekkende bron er wél doorheen, ook als de niet-dekkende nieuwer is', () => {
vreemdeRij({ observedAt: '2024-06-16T00:00:00Z' });
punt({ assetId: eth, observedAt: '2024-06-15T00:00:00Z', fiatPrice: 3000, source: 'bitvavo' });
assert.equal(latestPricePoint(eth).fiatPrice, 3000, 'de nieuwere rij van de niet-dekkende bron telt niet');
});
it('LAAT EEN HANDMATIG PUNT STAAN, ook al heeft de systeembron geen enkele dekkingsrij', () => {
systeemProvider('manual');
punt({ assetId: eth, observedAt: '2024-06-15T00:00:00Z', fiatPrice: 3000, source: 'manual' });
assert.equal(getPricePoint(eth, '2024-06-15').fiatPrice, 3000);
assert.equal(latestPricePoint(eth).fiatPrice, 3000);
assert.equal(listPriceHistory(eth).length, 1);
assert.equal(getPriceAt(eth, unix('2024-06-15T12:00:00Z')).fiatPrice, 3000);
});
it('toont de foute rij wél in de beheertabel, want daar wordt hij verwijderd', () => {
vreemdeRij();
assert.deepEqual(listAllPriceHistory().map(r => r.fiatPrice), [74500]);
assert.equal(priceHistoryCount(eth), 1, 'de telling gaat over wat er staat, niet over wat meetelt');
});
});
describe('listPriceGaps', () => {
const VANDAAG = '2026-09-24';
const boeking = (assetId, dag) => harness.exports.run(
`INSERT INTO Event (assetId, amount, type, source, occurredAt, createdAt)
VALUES (?, 100, 'transfer', 'manual', ?, ?)`, [assetId, `${dag}T10:00:00Z`, `${dag}T10:00:00Z`]);
const dagen = (assetId, van, tot, source = 'mempool') => {
for (let d = van; d <= tot; d = new Date(Date.parse(`${d}T00:00:00Z`) + 86400000).toISOString().slice(0, 10)) {
punt({ assetId, observedAt: `${d}T00:00:00Z`, source });
}
};
it('vindt een gat in het midden, met de juiste eerste en laatste dag', () => {
boeking(btc, '2026-09-01');
dagen(btc, '2026-09-01', '2026-09-05');
dagen(btc, '2026-09-15', '2026-09-23');
assert.deepEqual(listPriceGaps({ today: VANDAAG }).rows,
[{ assetId: btc, symbol: 'BTC', from: '2026-09-06', until: '2026-09-14', days: 9 }]);
});
it('vindt de kop (koersen beginnen na de eerste boeking) en de staart (tot en met gisteren)', () => {
boeking(btc, '2026-08-01');
dagen(btc, '2026-09-01', '2026-09-10');
assert.deepEqual(listPriceGaps({ today: VANDAAG }).rows.map(g => [g.from, g.until, g.days]),
[['2026-08-01', '2026-08-31', 31], ['2026-09-11', '2026-09-23', 13]]);
});
it('telt een gat tot de drempel niet, zoals een weekend met een feestdag', () => {
boeking(btc, '2026-09-01');
dagen(btc, '2026-09-01', '2026-09-10');
dagen(btc, '2026-09-17', '2026-09-23');
assert.equal(listPriceGaps({ today: VANDAAG }).total, 0);
});
it('laat een bron zonder dekking geen gat dichten', () => {
harness.exports.run(`DELETE FROM ProviderCoverage WHERE providerId = ? AND symbol = 'BTC'`, [providerId('bitvavo')]);
boeking(btc, '2026-09-01');
dagen(btc, '2026-09-01', '2026-09-05');
dagen(btc, '2026-09-06', '2026-09-14', 'bitvavo');
dagen(btc, '2026-09-15', '2026-09-23');
assert.equal(listPriceGaps({ today: VANDAAG }).rows[0].from, '2026-09-06');
});
describe('een gat negeren', () => {
it('laat een genegeerd gat weg, en terugzetten brengt het terug', async () => {
boeking(btc, '2026-09-01');
dagen(btc, '2026-09-01', '2026-09-05');
dagen(btc, '2026-09-15', '2026-09-23');
const id = await ignorePriceGap({ assetId: btc, from: '2026-09-06', until: '2026-09-14' });
assert.equal(listPriceGaps({ today: VANDAAG }).total, 0);
assert.equal(listIgnoredPriceGaps()[0].symbol, 'BTC');
await unignorePriceGap(id);
assert.equal(listPriceGaps({ today: VANDAAG }).total, 1);
});
it('toont bij een staart die doorgroeit alleen het nieuwe stuk, en pas als dat de drempel haalt', async () => {
boeking(btc, '2026-09-01');
dagen(btc, '2026-09-01', '2026-09-05');
await ignorePriceGap({ assetId: btc, from: '2026-09-06', until: '2026-09-20' });
assert.equal(listPriceGaps({ today: '2026-09-24' }).total, 0, 'drie nieuwe dagen haalt de drempel niet');
assert.deepEqual(listPriceGaps({ today: '2026-10-01' }).rows.map(g => [g.from, g.until, g.days]),
[['2026-09-21', '2026-09-30', 10]], 'tien nieuwe dagen wel, en alleen die');
});
it('weigert een onbekend asset en een bereik dat geen bereik is', async () => {
await assert.rejects(() => ignorePriceGap({ assetId: 9999, from: '2026-09-01', until: '2026-09-02' }), /does not exist/);
await assert.rejects(() => ignorePriceGap({ assetId: btc, from: '2026-09-05', until: '2026-09-01' }), /not a range/);
await assert.rejects(() => ignorePriceGap({ assetId: btc, from: 'gisteren', until: '2026-09-01' }), /not a range/);
assert.deepEqual(listIgnoredPriceGaps(), []);
});
});
describe('dagen zonder koers per bron', () => {
it('onthoudt een dag één keer per bron, en levert hem vanaf een startdag terug', async () => {
await recordNoPriceDays(btc, 'mempool', ['2025-12-25', '2025-12-26']);
await recordNoPriceDays(btc, 'mempool', ['2025-12-26']);
await recordNoPriceDays(btc, 'bitvavo', ['2025-12-26']);
assert.deepEqual(listNoPriceDays(btc).map(r => r.day), ['2025-12-25', '2025-12-26', '2025-12-26'],
'twee bronnen op dezelfde dag zijn twee rijen, dezelfde bron twee keer is er één');
assert.deepEqual(listNoPriceDays(btc, '2025-12-26'),
[{ providerId: providerId('mempool'), day: '2025-12-26' }, { providerId: providerId('bitvavo'), day: '2025-12-26' }]
.sort((a, b) => a.providerId - b.providerId));
assert.deepEqual(listNoPriceDays(eth), [], 'per asset');
});
it('weigert een onbekend asset, een onbekende bron en iets wat geen dag is', async () => {
await assert.rejects(() => recordNoPriceDays(9999, 'mempool', ['2025-12-25']), /does not exist/);
await assert.rejects(() => recordNoPriceDays(btc, 'bestaat-niet', ['2025-12-25']), /Unknown price source/);
await assert.rejects(() => recordNoPriceDays(btc, 'mempool', ['25-12-2025']), /Not a list of days/);
await assert.rejects(() => recordNoPriceDays(btc, 'mempool', '2025-12-25'), /Not a list of days/);
assert.deepEqual(listNoPriceDays(btc), []);
});
it('voegt opeenvolgende dagen per asset en bron samen tot een periode', async () => {
await recordNoPriceDays(btc, 'mempool', ['2025-12-24', '2025-12-25', '2025-12-26', '2026-01-01']);
await recordNoPriceDays(btc, 'bitvavo', ['2025-12-25']);
await recordNoPriceDays(eth, 'mempool', ['2025-12-26']);
const r = listPriceAbsences().map(a => [a.assetId, a.providerName, a.fromDay, a.untilDay, a.days]);
assert.deepEqual(r, [
[btc, 'bitvavo', '2025-12-25', '2025-12-25', 1],
[btc, 'mempool', '2025-12-24', '2025-12-26', 3],
[btc, 'mempool', '2026-01-01', '2026-01-01', 1],
[eth, 'mempool', '2025-12-26', '2025-12-26', 1],
]);
});
it('vergeet alleen de periode van die bron, en weigert iets wat geen periode is', async () => {
await recordNoPriceDays(btc, 'mempool', ['2025-12-24', '2025-12-25', '2026-01-01']);
await recordNoPriceDays(btc, 'bitvavo', ['2025-12-25']);
await deletePriceAbsences({ assetId: btc, providerId: providerId('mempool'), from: '2025-12-24', until: '2025-12-25' });
assert.deepEqual(listNoPriceDays(btc).map(r => [r.providerId, r.day]),
[[providerId('bitvavo'), '2025-12-25'], [providerId('mempool'), '2026-01-01']]);
await assert.rejects(() => deletePriceAbsences({ assetId: btc, providerId: providerId('mempool'), from: '2026-01-02', until: '2026-01-01' }), /not a range/);
await assert.rejects(() => deletePriceAbsences({ assetId: btc, from: '2026-01-01', until: '2026-01-01' }), /required/);
assert.equal(listNoPriceDays(btc).length, 2, 'een geweigerde aanroep wist niets');
});
});
it('negeert een gat vóór de eerste boeking, een asset zonder boeking, en fiat', () => {
dagen(btc, '2026-06-01', '2026-06-02');
boeking(btc, '2026-09-01');
dagen(btc, '2026-09-01', '2026-09-23');
dagen(eth, '2026-09-01', '2026-09-02');
harness.exports.run(`UPDATE Asset SET kind = 'fiat' WHERE id = ?`, [eth]);
boeking(eth, '2026-01-01');
assert.equal(listPriceGaps({ today: VANDAAG }).total, 0);
});
});
describe('recordCurrentSpot', () => {
it('bewaart de spot als timestamp-punt', async () => {
await recordCurrentSpot(btc, 'mempool', { price: 55000 });
const r = harness.exports.one(`SELECT resolution, fiatPrice FROM PriceHistory`);
assert.equal(r.resolution, 'timestamp');
assert.equal(r.fiatPrice, 55000);
});
it('houdt één punt per asset+bron per dag en werkt dat BIJ', async () => {
await recordCurrentSpot(btc, 'mempool', { price: 55000 });
await recordCurrentSpot(btc, 'mempool', { price: 56000 });
assert.equal(priceHistoryCount(btc), 1, 'nog steeds één rij');
assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory`).fiatPrice, 56000,
'maar met de LAATST geziene koers, want een slotkoers staat pas vast als de dag om is');
});
it('schuift het moment mee met de koers', async () => {
await recordCurrentSpot(btc, 'mempool', { price: 55000 });
const eerst = harness.exports.one(`SELECT observedAt FROM PriceHistory`).observedAt;
await recordCurrentSpot(btc, 'mempool', { price: 56000 });
const daarna = harness.exports.one(`SELECT observedAt FROM PriceHistory`).observedAt;
assert.ok(daarna >= eerst, 'het moment loopt niet terug');
assert.equal(daarna.slice(0, 10), new Date().toISOString().slice(0, 10), 'en blijft op vandaag');
});
it('bewaart wél een punt per bron', async () => {
await recordCurrentSpot(btc, 'mempool', { price: 55000 });
await recordCurrentSpot(btc, 'bitvavo', { price: 55100 });
assert.equal(priceHistoryCount(btc), 2);
});
it('doet niets zonder bruikbare koers', async () => {
await recordCurrentSpot(btc, 'mempool', null);
await recordCurrentSpot(btc, 'mempool', {});
await recordCurrentSpot(btc, 'mempool', { price: NaN });
await recordCurrentSpot(btc, 'mempool', { price: Infinity });
assert.equal(priceHistoryCount(btc), 0);
});
});
describe('addManualPricePoint', () => {
const morgen = () => new Date(Date.now() + 36 * 3600 * 1000).toISOString().slice(0, 10);
beforeEach(() => { systeemProvider('manual'); });
it('bewaart een dagpunt op de systeembron Handmatig', async () => {
const id = await addManualPricePoint({ assetId: btc, observedAt: '1994-06-15', fiatPrice: 315.5 });
assert.ok(id, 'het id komt terug, zodat de pagina de nieuwe rij kan aanwijzen');
const r = harness.exports.one(`SELECT ph.*, p.key AS bron FROM PriceHistory ph
JOIN Provider p ON p.id = ph.providerId WHERE ph.id = ?`, [id]);
assert.equal(r.fiatPrice, 315.5);
assert.equal(r.resolution, 'day');
assert.equal(r.observedAt, '1994-06-15T00:00:00Z', 'een dagpunt staat op middernacht UTC');
assert.equal(r.bron, 'manual', 'geen nieuwe systeembron: deze betekent al "met de hand"');
});
it('weigert een moment in de TOEKOMST', async () => {
await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: morgen(), fiatPrice: 100 }),
/future/i);
assert.equal(priceHistoryCount(btc), 0);
});
it('weigert een koers van nul of lager', async () => {
await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 0 }), /above zero/);
await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: -5 }), /above zero/);
await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 'veel' }), /above zero/);
assert.equal(priceHistoryCount(btc), 0);
});
it('weigert een onleesbare datum en een onbekend asset', async () => {
await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: 'ooit', fiatPrice: 100 }), /valid moment/);
await assert.rejects(() => addManualPricePoint({ assetId: 99999, observedAt: '2024-01-01', fiatPrice: 100 }), /does not exist/);
assert.equal(priceHistoryCount(btc), 0);
});
it('overschrijft hetzelfde punt in plaats van er een tweede naast te zetten', async () => {
const a = await addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 100 });
const b = await addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 120 });
assert.equal(a, b, 'dezelfde rij');
assert.equal(priceHistoryCount(btc), 1);
assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory`).fiatPrice, 120);
});
it('weigert een resolutie die niet bestaat', async () => {
await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 100, resolution: 'uur' }));
});
});
describe('addManualPricePoint: de eenheid', () => {
let goud;
beforeEach(() => {
systeemProvider('manual');
harness.exports.run(
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
VALUES ('XAU','Goud','goud','metal','ozt','g',4,'unit')`);
goud = harness.exports.lastId();
});
it('rekent een ouncerprijs om naar de gramprijs van de opslag', async () => {
const id = await addManualPricePoint({ assetId: goud, observedAt: '1994-06-15', fiatPrice: 315.5, unit: 'ozt' });
const r = harness.exports.one(`SELECT fiatPrice FROM PriceHistory WHERE id = ?`, [id]);
assert.equal(r.fiatPrice, 315.5 / CONFIG.METALS.UNITS.ozt);
assert.ok(r.fiatPrice > 10 && r.fiatPrice < 11, 'ruim tien euro per gram, en niet ruim driehonderd');
});
it('laat een gramprijs staan zoals hij is', async () => {
const id = await addManualPricePoint({ assetId: goud, observedAt: '1994-06-15', fiatPrice: 10.14, unit: 'g' });
assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory WHERE id = ?`, [id]).fiatPrice, 10.14);
});
it('neemt zonder eenheid de HOOFDEENHEID van het asset en niet de weergave-eenheid', async () => {
harness.exports.run(`UPDATE Asset SET displayUnit = 'subunit' WHERE id = ?`, [goud]);
const id = await addManualPricePoint({ assetId: goud, observedAt: '1994-06-15', fiatPrice: 315.5 });
assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory WHERE id = ?`, [id]).fiatPrice,
315.5 / CONFIG.METALS.UNITS.ozt, 'ozt, ook al staat de weergave op gram');
});
it('laat CRYPTO ongemoeid, wat er ook als eenheid meekomt', async () => {
const id = await addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 40000, unit: 'ozt' });
assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory WHERE id = ?`, [id]).fiatPrice, 40000);
});
it('weigert een eenheid die de config niet kent', async () => {
await assert.rejects(
() => addManualPricePoint({ assetId: goud, observedAt: '2024-01-01', fiatPrice: 100, unit: 'pond' }),
/Unknown metal unit/);
});
});