// Generated, do not edit. sha256:d59f0912a4578c0ab6f39878716960d471f5cbde7d6f6d3686e3e3fe87378fc2 import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { weightOf, averageCapital, modifiedDietz, irr, dailyFlows, lastDayHeld, timeWeighted, windowTwr, dailyFactors, windowFactors } from '../../client/wealth/performance.js'; const JAAR = { fromIso: '2025-01-01T00:00:00Z', untilIso: '2026-01-01T00:00:00Z' }; const HALF = '2025-07-01T00:00:00Z'; const rond = (v, n = 4) => (v == null ? v : Math.round(v * 10 ** n) / 10 ** n); describe('weightOf', () => { it('geeft één op de eerste dag en nul op de laatste', () => { assert.equal(weightOf(JAAR.fromIso, JAAR.fromIso, JAAR.untilIso), 1); assert.equal(weightOf(JAAR.untilIso, JAAR.fromIso, JAAR.untilIso), 0); }); it('geeft ongeveer de helft halverwege', () => { assert.equal(rond(weightOf(HALF, JAAR.fromIso, JAAR.untilIso), 2), 0.5); }); it('klemt een stroom van buiten het venster op de rand', () => { assert.equal(weightOf('2024-06-01T00:00:00Z', JAAR.fromIso, JAAR.untilIso), 1); assert.equal(weightOf('2026-06-01T00:00:00Z', JAAR.fromIso, JAAR.untilIso), 0); }); it('geeft één bij een venster zonder lengte in plaats van te delen door nul', () => { assert.equal(weightOf(JAAR.fromIso, JAAR.fromIso, JAAR.fromIso), 1); }); it('geeft null bij een datum die niet te lezen is', () => { assert.equal(weightOf('geen datum', JAAR.fromIso, JAAR.untilIso), null); assert.equal(weightOf(JAAR.fromIso, null, JAAR.untilIso), null); }); }); describe('averageCapital', () => { it('geeft de beginwaarde als er niets stroomt', () => { assert.equal(averageCapital(1000, [], JAAR), 1000); }); it('telt een storting halverwege voor ongeveer de helft mee', () => { assert.equal(rond(averageCapital(1000, [{ fiat: 1000, atIso: HALF }], JAAR), 1), 1504.1); }); it('haalt een opname er weer af, gewogen', () => { assert.equal(rond(averageCapital(1000, [{ fiat: -1000, atIso: HALF }], JAAR), 1), 495.9); }); it('slaat een stroom zonder leesbare datum over in plaats van hem vol mee te tellen', () => { assert.equal(averageCapital(1000, [{ fiat: 500, atIso: null }], JAAR), 1000); }); it('geeft null zonder bruikbare beginwaarde', () => { assert.equal(averageCapital(null, [], JAAR), null); assert.equal(averageCapital(undefined, [], JAAR), null); }); }); describe('modifiedDietz', () => { it('deelt door de beginwaarde als er niets stroomt, net als de oude som', () => { assert.equal(modifiedDietz({ gain: 200, begin: 1000, flows: [], ...JAAR }), 0.2); }); it('verlaagt het percentage als je halverwege bijstort', () => { const oud = 200 / 1000; const nieuw = modifiedDietz({ gain: 200, begin: 1000, flows: [{ fiat: 1000, atIso: HALF }], ...JAAR }); assert.equal(rond(nieuw, 4), 0.133); assert.ok(nieuw < oud, 'meer geld aan het werk voor hetzelfde resultaat is een lager rendement'); }); it('geeft null bij een noemer van nul of lager', () => { assert.equal(modifiedDietz({ gain: 50, begin: 0, flows: [], ...JAAR }), null); assert.equal(modifiedDietz({ gain: 50, begin: 100, flows: [{ fiat: -1000, atIso: JAAR.fromIso }], ...JAAR }), null); }); it('geeft null zonder resultaat om te delen', () => { assert.equal(modifiedDietz({ gain: null, begin: 1000, flows: [], ...JAAR }), null); }); }); describe('irr', () => { const reeks = (...paren) => paren.map(([fiat, atIso]) => ({ fiat, atIso })); it('vindt de rente die de stromen verklaart', () => { const r = irr(reeks([-1000, JAAR.fromIso], [1100, JAAR.untilIso]), { fromIso: JAAR.fromIso }); assert.equal(rond(r, 3), 0.1); }); it('rekent per JAAR en niet per periode', () => { const r = irr(reeks([-1000, JAAR.fromIso], [1100, HALF]), { fromIso: JAAR.fromIso }); assert.ok(r > 0.2, `verwacht meer dan 20% op jaarbasis, kreeg ${r}`); }); it('verwerkt een storting halverwege', () => { const r = irr(reeks([-1000, JAAR.fromIso], [-1000, HALF], [2100, JAAR.untilIso]), { fromIso: JAAR.fromIso }); assert.ok(r > 0 && r < 0.2, `verwacht een bescheiden positief rendement, kreeg ${r}`); }); it('geeft null als er geen tekenwisseling is', () => { assert.equal(irr(reeks([-1000, JAAR.fromIso], [-500, HALF]), { fromIso: JAAR.fromIso }), null); assert.equal(irr(reeks([1000, JAAR.fromIso], [500, HALF]), { fromIso: JAAR.fromIso }), null); }); it('geeft null bij een verlies dat buiten het zoekbereik valt', () => { assert.equal(irr(reeks([-1000, JAAR.fromIso], [0, JAAR.untilIso]), { fromIso: JAAR.fromIso }), null); }); it('geeft null bij een onleesbare datum of een leeg bedrag', () => { assert.equal(irr(reeks([-1000, JAAR.fromIso], [1100, 'ooit']), { fromIso: JAAR.fromIso }), null); assert.equal(irr([], { fromIso: JAAR.fromIso }), null); }); it('komt op hetzelfde uit als Modified Dietz zolang er niets stroomt', () => { const md = modifiedDietz({ gain: 100, begin: 1000, flows: [], ...JAAR }); const i = irr(reeks([-1000, JAAR.fromIso], [1100, JAAR.untilIso]), { fromIso: JAAR.fromIso }); assert.equal(rond(md, 3), rond(i, 3)); }); }); describe('dailyFlows', () => { it('maakt van een cumulatieve reeks de stap per dag', () => { assert.deepEqual(dailyFlows([0, 100, 100, 250]), [0, 100, 0, 150]); }); it('geeft een negatieve stap bij een verkoop', () => { assert.deepEqual(dailyFlows([500, 500, 300]), [0, 0, -200]); }); it('begint op nul, want de eerste dag heeft geen vorige', () => { assert.deepEqual(dailyFlows([1000]), [0]); assert.deepEqual(dailyFlows([]), []); }); }); describe('timeWeighted', () => { it('geeft de koersverandering als er niets stroomt', () => { assert.equal(rond(timeWeighted([100, 110, 120], [0, 0, 0]), 4), 0.2); }); it('HAALT EEN STORTING ERUIT, en dat is het hele punt', () => { assert.equal(rond(timeWeighted([100, 110, 1110], [0, 0, 1000]), 4), 0.1); }); it('ketent twee deelperiodes in plaats van ze te middelen', () => { assert.equal(rond(timeWeighted([100, 110, 121], [0, 0, 0]), 4), 0.21); }); it('telt een dag zonder basis als niets in plaats van de keten te breken', () => { const r = timeWeighted([100, 0, 0, 220], [0, -110, 0, 200]); assert.equal(rond(r, 4), 0.21); }); it('geeft null als er niets te meten valt', () => { assert.equal(timeWeighted([100], [0]), null); assert.equal(timeWeighted([], []), null); assert.equal(timeWeighted([0, 0, 0], [0, 0, 0]), null, 'nooit een basis gehad'); }); it('overleeft een reeks zonder stromen-array', () => { assert.equal(rond(timeWeighted([100, 150]), 4), 0.5); }); }); describe('lastDayHeld', () => { it('vindt de laatste dag met een positie', () => { assert.equal(lastDayHeld([0, 100, 100, 0, 0], 0, 4), 2); }); it('geeft de laatste dag van het venster als de positie er dan nog is', () => { assert.equal(lastDayHeld([0, 100, 100], 0, 2), 2); }); it('kijkt niet buiten het venster', () => { assert.equal(lastDayHeld([0, 100, 100, 0, 50], 0, 3), 2); }); it('gaat niet onder de ondergrens van het venster', () => { assert.equal(lastDayHeld([100, 100, 0, 0], 2, 3), null); }); it('geeft null zonder enige positie in het venster', () => { assert.equal(lastDayHeld([0, 0, 0], 0, 2), null); assert.equal(lastDayHeld([], 0, 5), null); }); it('klemt op de lengte van de reeks', () => { assert.equal(lastDayHeld([0, 100], 0, 99), 1); }); describe('een verkochte positie', () => { const waarden = [0, 1000, 1200, 0]; it('telt de verkoopdag als wat de verkoop opbracht', () => { assert.equal(rond(timeWeighted(waarden, [0, 1000, 0, -1180])), 0.18); }); it('en een verkoop boven de slotkoers telt net zo, zonder sprong', () => { assert.equal(rond(timeWeighted(waarden, [0, 1000, 0, -1220])), 0.22); }); }); }); describe('timeWeighted: dust', () => { const waarden = [0, 50, 51, 0.02, 0.03, 0]; const stromen = [0, 50, 0, -51.15, 0, -0.01]; it('stopt de meting bij de verkoop die alleen dust overlaat', () => { assert.equal(rond(timeWeighted(waarden, stromen)), rond((0.02 + 51.15) / 50 - 1)); }); it('meet weer zodra er geld in komt', () => { const r = timeWeighted([0, 50, 51, 0.02, 100.02, 110.02], [0, 50, 0, -51.15, 100, 0]); assert.equal(rond(r), rond((51.17 / 50) * (100.02 / 100.02) * (110.02 / 100.02) - 1)); }); it('sluit niet bij een gewone gedeeltelijke verkoop', () => { const r = timeWeighted([0, 100, 100, 50, 55], [0, 100, 0, -50, 0]); assert.equal(rond(r), 0.1); }); }); describe('windowTwr: aankoop en verkoop op één dag', () => { it('telt de aankoop aan het begin en de verkoop aan het eind', () => { const r = windowTwr({ waarden: [1, 0.95, 0], inleg: [1, 1, 1 + 11.56 - 12.65], uit: [0, 0, 12.65], eenheden: [5, 5, 0], }, { van: 1, tot: 2 }); assert.equal(rond(r), rond(12.65 / (0.95 + 11.56) - 1)); }); }); describe('windowTwr', () => { const BETAALD = 99.84, SLOT = 104.77, EIND_2025 = 53.58, NU = 63.25; it('meet vanaf het betaalde bedrag en niet vanaf de slotkoers van de aankoopdag', () => { const r = windowTwr({ waarden: [0, 0, SLOT, EIND_2025], inleg: [0, 0, BETAALD, BETAALD], eenheden: [0, 0, 5, 5], eersteDag: 2, }, { van: 0, tot: 3 }); assert.equal(rond(r), rond(EIND_2025 / BETAALD - 1)); }); it('doet dat ook als de aankoop op de eerste dag van de reeks valt', () => { const r = windowTwr({ waarden: [SLOT, EIND_2025], inleg: [BETAALD, BETAALD], eenheden: [5, 5], eersteDag: 0, }, { van: 0, tot: 1 }); assert.equal(rond(r), rond(EIND_2025 / BETAALD - 1)); }); it('de keten van twee jaren is gelijk aan eindwaarde gedeeld door betaald', () => { const reeks = { waarden: [0, SLOT, EIND_2025, NU], inleg: [0, BETAALD, BETAALD, BETAALD], eenheden: [0, 5, 5, 5], eersteDag: 1, }; const r1 = windowTwr(reeks, { van: 0, tot: 2 }); const r2 = windowTwr(reeks, { van: 2, tot: 3 }); assert.equal(rond(r2), rond(NU / EIND_2025 - 1), 'het tweede jaar meet vanaf de jaargrens'); assert.equal(rond((1 + r1) * (1 + r2) - 1), rond(NU / BETAALD - 1)); }); it('telt een aankoop OP de jaargrens precies één keer', () => { const reeks = { waarden: [0, 0, SLOT, NU], inleg: [0, 0, BETAALD, BETAALD], eenheden: [0, 0, 5, 5], eersteDag: 2, }; const r1 = windowTwr(reeks, { van: 0, tot: 2 }); const r2 = windowTwr(reeks, { van: 2, tot: 3 }); assert.equal(rond(r2), rond(NU / SLOT - 1), 'het nieuwe jaar begint bij de slotkoers'); assert.equal(rond((1 + r1) * (1 + r2) - 1), rond(NU / BETAALD - 1)); }); it('begint zonder eersteDag gewoon bij het venster', () => { assert.equal(rond(windowTwr({ waarden: [SLOT, EIND_2025], inleg: [BETAALD, BETAALD] }, { van: 0, tot: 1 })), rond(EIND_2025 / SLOT - 1)); }); }); const product = factoren => { const gemeten = factoren.filter(f => f != null); return gemeten.length ? gemeten.reduce((p, f) => p * f, 1) - 1 : null; }; describe('dailyFactors', () => { it('geeft per dag de factor, en null op dag nul', () => { assert.deepEqual(dailyFactors([100, 110, 121], [0, 0, 0]).map(f => rond(f)), [null, 1.1, 1.1]); }); it('meet de dagen van een restje stof NIET', () => { const f = dailyFactors([0, 50, 51, 0.02, 0.03, 0], [0, 50, 0, -51.15, 0, -0.01]); assert.equal(f[4], null); assert.equal(f[5], null); }); it('geeft null op een dag zonder basis, en niet een factor van één', () => { const f = dailyFactors([0, 0, 100], [0, 0, 100]); assert.equal(f[1], null); assert.equal(f[2], 1); }); it('heeft als product precies timeWeighted, ook als een gesloten positie heropent', () => { const waarden = [0, 50, 51, 0.02, 100.02, 110.02]; const stromen = [0, 50, 0, -51.15, 100, 0]; assert.equal(rond(product(dailyFactors(waarden, stromen))), rond(timeWeighted(waarden, stromen))); }); }); describe('windowFactors', () => { const BETAALD = 99.84, SLOT = 104.77, EIND_2025 = 53.58, NU = 63.25; const reeks = { waarden: [0, SLOT, EIND_2025, NU], inleg: [0, BETAALD, BETAALD, BETAALD], eenheden: [0, 5, 5, 5], eersteDag: 1, }; it('heeft als product over elk venster precies windowTwr', () => { for (const venster of [{ van: 0, tot: 3 }, { van: 2, tot: 3 }, { van: 1, tot: 2 }]) { assert.equal(rond(product(windowFactors(reeks, venster))), rond(windowTwr(reeks, venster)), `venster ${venster.van} t/m ${venster.tot}`); } }); it('zet de factor op de dag van de reeks, ook als er een lege dag voor gezet is', () => { const f = windowFactors({ waarden: [SLOT, EIND_2025], inleg: [BETAALD, BETAALD], eenheden: [5, 5], eersteDag: 0 }, { van: 0, tot: 1 }); assert.deepEqual(f.map(x => rond(x)), [rond(SLOT / BETAALD), rond(EIND_2025 / SLOT)]); }); it('is null buiten het venster en even lang als de reeks', () => { const f = windowFactors(reeks, { van: 2, tot: 3 }); assert.equal(f.length, 4); assert.equal(f[1], null); assert.equal(f[2], null, 'het venster meet de stap NA zijn begin'); assert.equal(rond(f[3]), rond(NU / EIND_2025)); }); it('geeft alleen null als het venster nooit een positie had', () => { const f = windowFactors({ ...reeks, eenheden: [0, 0, 0, 0] }, { van: 0, tot: 3 }); assert.deepEqual(f, [null, null, null, null]); }); });