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