Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -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/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user