Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,54 @@
|
||||
// Generated, do not edit. sha256:771d7bfccd806caf4ec8847da01e643d018dcebd8d547afd6fc2a566be8ad622
|
||||
import { ENUMS } from '../../shared/config.js';
|
||||
import { byKindAndLabel } from '../ui/asset-label.js';
|
||||
|
||||
export function accountCards({ kaarten = [], holdings = [], accounts = [], toon = new Set() } = {}) {
|
||||
const perAsset = new Map(kaarten.map(k => [k.asset.id, k]));
|
||||
const lijnen = new Map();
|
||||
const lijn = (accountId, k, saldo) => {
|
||||
if (!lijnen.has(accountId)) lijnen.set(accountId, []);
|
||||
lijnen.get(accountId).push({ k, saldo });
|
||||
};
|
||||
const opAccount = new Map();
|
||||
for (const h of holdings) {
|
||||
const k = perAsset.get(h.assetId);
|
||||
if (!k || !h.saldo) continue;
|
||||
lijn(h.accountId, k, h.saldo);
|
||||
opAccount.set(h.assetId, (opAccount.get(h.assetId) || 0) + h.saldo);
|
||||
}
|
||||
for (const k of kaarten) {
|
||||
const rest = (k.p.net || 0) - (opAccount.get(k.asset.id) || 0);
|
||||
if (rest) lijn(null, k, rest);
|
||||
}
|
||||
for (const id of toon) if (!lijnen.has(id)) lijnen.set(id, []);
|
||||
|
||||
const naam = new Map(accounts.map(a => [a.id, a]));
|
||||
const kaartVan = (accountId, rijen) => {
|
||||
let value = null, unrealized = null;
|
||||
const lines = rijen.map(({ k, saldo }) => {
|
||||
const units = saldo / Math.pow(10, k.asset.decimals);
|
||||
const v = k.m.fiatPrice != null ? units * k.m.fiatPrice : null;
|
||||
if (v != null) value = (value ?? 0) + v;
|
||||
const deel = ongerealiseerdVan(k, accountId, saldo);
|
||||
if (deel != null) unrealized = (unrealized ?? 0) + deel;
|
||||
return { asset: k.asset, saldo, value: v, origin: k.m.origin };
|
||||
}).sort((a, b) => byKindAndLabel(a.asset, b.asset));
|
||||
return {
|
||||
accountId, account: naam.get(accountId) || null, value, unrealized, lines,
|
||||
assetIds: lines.map(l => l.asset.id),
|
||||
kinds: [...new Set(lines.map(l => l.asset.kind))],
|
||||
};
|
||||
};
|
||||
const ongerealiseerdVan = (k, accountId, saldo) => {
|
||||
const eigenDeel = k.m.beheerd?.find(d => d.accountId === accountId);
|
||||
if (eigenDeel) return eigenDeel.unrealized;
|
||||
const basis = k.m.eigen ?? { net: k.p.net, unrealized: k.m.unrealized };
|
||||
return basis.unrealized != null && basis.net ? basis.unrealized * (saldo / basis.net) : null;
|
||||
};
|
||||
const soort = c => (c.accountId == null ? ENUMS.ACCOUNT_KIND.length + 1
|
||||
: c.account ? ENUMS.ACCOUNT_KIND.indexOf(c.account.kind) : ENUMS.ACCOUNT_KIND.length);
|
||||
return [...lijnen].map(([id, rijen]) => kaartVan(id, rijen))
|
||||
.filter(c => c.lines.length || c.account)
|
||||
.sort((a, b) => soort(a) - soort(b)
|
||||
|| String(a.account?.name ?? '').localeCompare(String(b.account?.name ?? ''), 'nl'));
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Generated, do not edit. sha256:f962f4bc88fe2f11615f93f44c35fe729fcb597398ce82af6f05055c78fd9a0a
|
||||
import { CONFIG } from '../../shared/config.js';
|
||||
|
||||
export function carriedPerAsset({ dagen = [], assets = [], rijenPerAsset = new Map() } = {}) {
|
||||
const uit = new Map();
|
||||
for (const a of assets) {
|
||||
if (a.kind === 'fiat') continue;
|
||||
const echt = new Set((rijenPerAsset.get(a.id) || []).filter(r => r.fiatPrice != null && r.fiatPrice > 0)
|
||||
.map(r => String(r.observedAt).slice(0, 10)));
|
||||
if (dagen.length) echt.add(dagen[dagen.length - 1]);
|
||||
const dicht = CONFIG.PRICE_HISTORY.CLOSED_MARKET_DAYS[a.kind] ?? 0;
|
||||
const vlag = dagen.map(() => false);
|
||||
let gezien = false;
|
||||
let begin = null;
|
||||
const sluit = eind => {
|
||||
if (begin != null && eind - begin > dicht) for (let k = begin; k < eind; k++) vlag[k] = true;
|
||||
begin = null;
|
||||
};
|
||||
dagen.forEach((dag, i) => {
|
||||
if (echt.has(dag)) { sluit(i); gezien = true; return; }
|
||||
if (gezien && begin == null) begin = i;
|
||||
});
|
||||
sluit(dagen.length);
|
||||
uit.set(a.id, vlag);
|
||||
}
|
||||
return uit;
|
||||
}
|
||||
|
||||
export function carriedDays(reeks = {}, ids = null, assets = []) {
|
||||
const kandidaten = assets.filter(a => ids == null || ids.includes(a.id));
|
||||
const vlaggen = carriedPerAsset({ dagen: reeks.dagen || [], assets: kandidaten, rijenPerAsset: reeks.rijenPerAsset || new Map() });
|
||||
const uit = new Set();
|
||||
for (const [id, vlag] of vlaggen) {
|
||||
const eenheden = reeks.eenhedenPerAsset?.get(id) || [];
|
||||
vlag.forEach((aan, i) => { if (aan && eenheden[i]) uit.add(i); });
|
||||
}
|
||||
return uit;
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
// Generated, do not edit. sha256:2f0f6627ca3113d46ca463586c34fddd9f180542b18f6825cae3c17d5a06bf19
|
||||
import { loadCharting, readPalette } from '../ui/chart-setup.js';
|
||||
import { tintSeries } from '../ui/charting.js';
|
||||
import { t, localeTag } from '../i18n/index.js';
|
||||
import { fmtBase } from '../ui/format.js';
|
||||
import { ENUMS } from '../../shared/config.js';
|
||||
|
||||
export function ringData(kaarten, { restVanaf = 0.02 } = {}) {
|
||||
const bruikbaar = kaarten.filter(k => typeof k.m.value === 'number' && k.m.value > 0);
|
||||
const alle = bruikbaar
|
||||
.map(k => ({ ids: [k.asset.id], kinds: [k.asset.kind], label: k.asset.symbol, value: k.m.value, kind: k.asset.kind }))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
const perSoort = new Map();
|
||||
for (const a of alle) {
|
||||
const vorig = perSoort.get(a.kind) || { value: 0, ids: [] };
|
||||
perSoort.set(a.kind, { value: vorig.value + a.value, ids: [...vorig.ids, ...a.ids] });
|
||||
}
|
||||
|
||||
const totaal = alle.reduce((s, a) => s + a.value, 0);
|
||||
const grens = totaal * restVanaf;
|
||||
const klein = alle.filter(a => a.value < grens);
|
||||
const assets = klein.length >= 2
|
||||
? [...alle.filter(a => a.value >= grens), {
|
||||
ids: klein.flatMap(a => a.ids),
|
||||
kinds: [...new Set(klein.flatMap(a => a.kinds))],
|
||||
label: `${t('wealth.ringOther')} (${klein.length})`,
|
||||
value: klein.reduce((s, a) => s + a.value, 0),
|
||||
kind: null,
|
||||
}]
|
||||
: alle;
|
||||
const soorten = ENUMS.ASSET_KIND
|
||||
.filter(kind => perSoort.has(kind))
|
||||
.map(kind => ({
|
||||
kind, label: t('enums.asset_kind.' + kind),
|
||||
value: perSoort.get(kind).value, ids: perSoort.get(kind).ids, kinds: [kind],
|
||||
}));
|
||||
|
||||
return { soorten, assets };
|
||||
}
|
||||
|
||||
export function segmentIndices(segmenten = [], sel) {
|
||||
if (!sel) return [];
|
||||
const uit = [];
|
||||
if (sel.assetIds?.length) {
|
||||
const wil = new Set(sel.assetIds);
|
||||
segmenten.forEach((s, i) => { if ((s.ids || []).some(id => wil.has(id))) uit.push(i); });
|
||||
return uit;
|
||||
}
|
||||
if (sel.kind) {
|
||||
segmenten.forEach((s, i) => { if ((s.kinds || []).includes(sel.kind)) uit.push(i); });
|
||||
}
|
||||
return uit;
|
||||
}
|
||||
|
||||
export function uitsteek(aandeel, max = UITSTEEK) {
|
||||
const a = Math.min(Math.max(Number(aandeel) || 0, 0), 1);
|
||||
return Math.round(max * (1 - a));
|
||||
}
|
||||
|
||||
export function nadrukKleuren(basis = [], aan = new Set()) {
|
||||
return basis.map((k, n) => (aan.size === 0 || aan.has(n) ? k : dim(k)));
|
||||
}
|
||||
|
||||
export function glansStops(rgba) {
|
||||
const m = /^rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$/.exec(String(rgba));
|
||||
if (!m) return [rgba, rgba];
|
||||
const [r, g, b, a] = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
||||
const licht = c => Math.round(c + (255 - c) * 0.18);
|
||||
const binnen = Math.round(a * 0.72 * 1000) / 1000;
|
||||
const buiten = Math.round(Math.min(1, a * 1.1) * 1000) / 1000;
|
||||
return [`rgba(${r},${g},${b},${binnen})`, `rgba(${licht(r)},${licht(g)},${licht(b)},${buiten})`];
|
||||
}
|
||||
|
||||
export function outerEdge(arc, { offset = 0, spacing = 0 } = {}) {
|
||||
const { x, y, startAngle, endAngle, innerRadius = 0, outerRadius } = arc;
|
||||
const g = endAngle - startAngle;
|
||||
const mid = (startAngle + endAngle) / 2;
|
||||
const shift = offset / 4;
|
||||
const grow = shift * (1 - Math.sin(Math.min(Math.PI, g || 0)));
|
||||
const half = spacing / 2;
|
||||
const r = Math.max(outerRadius + half + grow, 0);
|
||||
let trimSpacing = 0;
|
||||
if (half) {
|
||||
const avg = ((innerRadius > 0 ? innerRadius - half : 0) + (r > 0 ? r - half : 0)) / 2;
|
||||
trimSpacing = (g - (avg !== 0 ? (g * avg) / (avg + half) : g)) / 2;
|
||||
}
|
||||
const trimOffset = r > 0 ? (g - Math.max(0.001, g * r - grow / Math.PI) / r) / 2 : 0;
|
||||
return {
|
||||
cx: x + Math.cos(mid) * shift, cy: y + Math.sin(mid) * shift, r,
|
||||
start: startAngle + trimOffset + trimSpacing, end: endAngle - trimOffset - trimSpacing,
|
||||
};
|
||||
}
|
||||
|
||||
export function dim(rgba, factor = 0.35) {
|
||||
const m = /^rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$/.exec(String(rgba));
|
||||
if (!m) return rgba;
|
||||
const a = Math.round(Number(m[4]) * factor * 1000) / 1000;
|
||||
return `rgba(${m[1]},${m[2]},${m[3]},${a})`;
|
||||
}
|
||||
|
||||
const UITSTEEK = 14;
|
||||
|
||||
const REST_UITSTEEK = 6;
|
||||
const basisUitsteek = segmenten => segmenten.map(s => (s.kind === null ? REST_UITSTEEK : 0));
|
||||
|
||||
const PCT_IN_BOOG = 5;
|
||||
|
||||
const pctTekst = pct => `${pct.toLocaleString(localeTag(), { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`;
|
||||
|
||||
function pasNadrukToe(chart) {
|
||||
const ds = chart?.data?.datasets?.[0];
|
||||
if (!ds) return;
|
||||
const st = chart._vermogen || {};
|
||||
const aan = new Set([...(st.hover || []), ...(st.selectie || [])]);
|
||||
ds._kleuren = nadrukKleuren(ds._basisKleuren, aan);
|
||||
if (!ds._glans) ds.backgroundColor = ds._kleuren;
|
||||
const basis = ds._basisOffset || [];
|
||||
const totaal = (ds.data || []).reduce((s, v) => s + v, 0);
|
||||
ds.offset = basis.map((b, n) => (aan.has(n) ? b + uitsteek(totaal > 0 ? ds.data[n] / totaal : 0) : b));
|
||||
chart.update('none');
|
||||
}
|
||||
|
||||
export async function tekenVerdeling(canvasSoorten, canvasAssets, kaarten, { onHover = () => {}, onSelect = () => {} } = {}) {
|
||||
const { soorten, assets } = ringData(kaarten);
|
||||
if (!assets.length) return null;
|
||||
|
||||
const Chart = await loadCharting();
|
||||
const pal = readPalette();
|
||||
const grafieken = [
|
||||
canvasSoorten && bouwRing(Chart, pal, canvasSoorten, soorten, { onHover, onSelect, soortRing: true }),
|
||||
canvasAssets && bouwRing(Chart, pal, canvasAssets, assets, { onHover, onSelect, soortRing: false }),
|
||||
].filter(Boolean);
|
||||
if (!grafieken.length) return null;
|
||||
|
||||
const zet = (veld, sel) => {
|
||||
for (const g of grafieken) {
|
||||
g._vermogen = { ...g._vermogen, [veld]: segmentIndices(g.data.datasets[0]._segmenten, sel) };
|
||||
pasNadrukToe(g);
|
||||
}
|
||||
};
|
||||
return {
|
||||
markeer: sel => zet('hover', sel),
|
||||
selecteer: sel => zet('selectie', sel),
|
||||
destroy: () => { grafieken.forEach(g => g.destroy()); hideRingTip(); },
|
||||
};
|
||||
}
|
||||
|
||||
let ringTipEl = null;
|
||||
const hideRingTip = () => { if (ringTipEl) ringTipEl.classList.remove('is-visible'); };
|
||||
|
||||
function ringTip({ chart, tooltip }, deel) {
|
||||
if (!tooltip || tooltip.opacity === 0 || !tooltip.dataPoints?.length) { hideRingTip(); return; }
|
||||
if (!ringTipEl || !ringTipEl.isConnected) {
|
||||
ringTipEl = document.createElement('div');
|
||||
ringTipEl.className = 'ring-tip';
|
||||
ringTipEl.setAttribute('role', 'tooltip');
|
||||
document.body.appendChild(ringTipEl);
|
||||
window.addEventListener('scroll', hideRingTip, { capture: true, passive: true });
|
||||
}
|
||||
const punt = tooltip.dataPoints[0];
|
||||
const bedrag = document.createElement('span');
|
||||
bedrag.className = 'val';
|
||||
bedrag.textContent = fmtBase(punt.parsed);
|
||||
const naam = punt.dataset._segmenten?.[punt.dataIndex]?.label ?? '';
|
||||
const naamEl = document.createElement('span');
|
||||
naamEl.className = 'ring-tip-label';
|
||||
naamEl.textContent = naam;
|
||||
const pct = document.createElement('span');
|
||||
pct.className = 'ring-tip-pct';
|
||||
pct.textContent = pctTekst(deel(punt.parsed));
|
||||
const regel = document.createElement('span');
|
||||
regel.className = 'ring-tip-row';
|
||||
regel.append(bedrag, pct);
|
||||
ringTipEl.replaceChildren(naamEl, regel);
|
||||
const r = chart.canvas.getBoundingClientRect();
|
||||
ringTipEl.style.left = `${Math.round(r.left + tooltip.caretX)}px`;
|
||||
ringTipEl.style.top = `${Math.round(r.top + tooltip.caretY)}px`;
|
||||
ringTipEl.classList.add('is-visible');
|
||||
}
|
||||
|
||||
function bouwRing(Chart, pal, canvas, segmenten, { onHover, onSelect, soortRing }) {
|
||||
if (!segmenten.length) return null;
|
||||
const kleuren = tintSeries(pal.accent, segmenten.length);
|
||||
const totaal = segmenten.reduce((s, a) => s + a.value, 0);
|
||||
const deel = v => (totaal > 0 ? (v / totaal) * 100 : 0);
|
||||
const meldingVan = n => (soortRing
|
||||
? { kind: segmenten[n].kind }
|
||||
: { assetIds: segmenten[n].ids ?? [] });
|
||||
let laatst = null;
|
||||
|
||||
const grafiek = new Chart(canvas, {
|
||||
type: 'doughnut',
|
||||
plugins: [glasPlugin(pal), percentagePlugin(pal), middenLabelPlugin(pal, deel)],
|
||||
data: {
|
||||
datasets: [{
|
||||
data: segmenten.map(s => s.value),
|
||||
backgroundColor: pal.ringSheen ? glansVulling : kleuren,
|
||||
_kleuren: kleuren,
|
||||
_glans: pal.ringSheen,
|
||||
spacing: pal.ringSeam,
|
||||
borderWidth: pal.ringSeam > 0 ? 0 : 1,
|
||||
_segmenten: segmenten,
|
||||
_basisKleuren: kleuren,
|
||||
offset: basisUitsteek(segmenten), _basisOffset: basisUitsteek(segmenten),
|
||||
hoverOffset: 0,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: { padding: REST_UITSTEEK + UITSTEEK + 4 },
|
||||
onHover: (evt, actief) => {
|
||||
const el = actief[0];
|
||||
if (!el || el.index === laatst) return;
|
||||
laatst = el.index;
|
||||
onHover(meldingVan(el.index));
|
||||
},
|
||||
onClick: (evt, actief) => {
|
||||
const el = actief[0];
|
||||
return onSelect(el ? meldingVan(el.index) : null);
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
external: ctx => ringTip(ctx, deel),
|
||||
},
|
||||
},
|
||||
borderColor: pal.tooltipBg,
|
||||
borderWidth: 1,
|
||||
cutout: '58%',
|
||||
},
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => { laatst = null; onHover(null); });
|
||||
return grafiek;
|
||||
}
|
||||
|
||||
function glansVulling(context) {
|
||||
const kleur = context.dataset._kleuren?.[context.dataIndex];
|
||||
const { chart } = context;
|
||||
const area = chart?.chartArea;
|
||||
if (!kleur || !area) return kleur;
|
||||
const r = Math.min(area.right - area.left, area.bottom - area.top) / 2;
|
||||
if (!(r > 0)) return kleur;
|
||||
const cx = (area.left + area.right) / 2;
|
||||
const cy = (area.top + area.bottom) / 2;
|
||||
const [binnen, buiten] = glansStops(kleur);
|
||||
const g = chart.ctx.createRadialGradient(cx, cy, r * 0.58, cx, cy, r);
|
||||
g.addColorStop(0, binnen);
|
||||
g.addColorStop(1, buiten);
|
||||
return g;
|
||||
}
|
||||
|
||||
function glasPlugin(pal) {
|
||||
return {
|
||||
id: 'wealth-glass',
|
||||
afterDatasetsDraw(c) {
|
||||
if (!pal.ringSheen) return;
|
||||
const meta = c.getDatasetMeta(0);
|
||||
if (!meta?.data?.length || meta.hidden) return;
|
||||
const { ctx } = c;
|
||||
const st = c._vermogen || {};
|
||||
const aan = new Set([...(st.hover || []), ...(st.selectie || [])]);
|
||||
if (aan.size && pal.accentGlow) {
|
||||
ctx.save();
|
||||
ctx.shadowColor = pal.accentGlow;
|
||||
ctx.shadowBlur = 18;
|
||||
for (const n of aan) meta.data[n]?.draw(ctx);
|
||||
ctx.restore();
|
||||
}
|
||||
if (!pal.hairline) return;
|
||||
const eerste = meta.data[0];
|
||||
const lijn = ctx.createLinearGradient(0, eerste.y - eerste.outerRadius, 0, eerste.y + eerste.outerRadius);
|
||||
lijn.addColorStop(0, pal.hairline);
|
||||
lijn.addColorStop(1, pal.rimBottom || 'transparent');
|
||||
ctx.save();
|
||||
ctx.strokeStyle = lijn;
|
||||
ctx.lineWidth = 1;
|
||||
for (const boog of meta.data) {
|
||||
const rand = outerEdge(boog, { offset: boog.options?.offset || 0, spacing: boog.options?.spacing || 0 });
|
||||
if (rand.end - rand.start <= 0 || rand.r <= 1) continue;
|
||||
ctx.beginPath();
|
||||
ctx.arc(rand.cx, rand.cy, rand.r - 0.5, rand.start, rand.end);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function percentagePlugin(pal) {
|
||||
return {
|
||||
id: 'wealth-pct',
|
||||
afterDatasetsDraw(c) {
|
||||
const { ctx } = c;
|
||||
const meta = c.getDatasetMeta(0);
|
||||
if (!meta || meta.hidden) return;
|
||||
const rijen = c.data.datasets[0].data;
|
||||
const som = rijen.reduce((s, v) => s + v, 0);
|
||||
ctx.save();
|
||||
ctx.font = '600 11px system-ui, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
meta.data.forEach((boog, n) => {
|
||||
const pct = som > 0 ? (rijen[n] / som) * 100 : 0;
|
||||
if (pct < PCT_IN_BOOG) return;
|
||||
const { x, y } = boog.getCenterPoint();
|
||||
const tekst = pctTekst(pct);
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = pal.tooltipBg;
|
||||
ctx.strokeText(tekst, x, y);
|
||||
ctx.fillStyle = pal.text;
|
||||
ctx.fillText(tekst, x, y);
|
||||
});
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function middenLabelPlugin(pal, deel) {
|
||||
return {
|
||||
id: 'wealth-hole',
|
||||
afterDatasetsDraw(c) {
|
||||
const st = c._vermogen || {};
|
||||
const n = (st.hover || [])[0] ?? (st.selectie || [])[0];
|
||||
if (n == null) return;
|
||||
const ds = c.data.datasets[0];
|
||||
const seg = ds._segmenten?.[n];
|
||||
if (!seg) return;
|
||||
const meta = c.getDatasetMeta(0);
|
||||
const boog = meta?.data?.[n];
|
||||
if (!boog) return;
|
||||
|
||||
const { ctx } = c;
|
||||
const x = boog.x;
|
||||
const y = boog.y;
|
||||
const ruimte = (boog.innerRadius ?? 40) * 1.7;
|
||||
ctx.save();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = pal.text;
|
||||
ctx.font = '700 1.15rem system-ui, sans-serif';
|
||||
ctx.fillText(pctTekst(deel(seg.value)), x, y - 12);
|
||||
ctx.font = '600 0.78rem system-ui, sans-serif';
|
||||
ctx.fillText(kort(ctx, seg.label, ruimte), x, y + 6);
|
||||
ctx.fillStyle = pal.textSec;
|
||||
ctx.font = '0.72rem system-ui, sans-serif';
|
||||
ctx.fillText(kort(ctx, fmtBase(seg.value), ruimte), x, y + 22);
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function kort(ctx, tekst, breedte) {
|
||||
const s = String(tekst ?? '');
|
||||
if (ctx.measureText(s).width <= breedte) return s;
|
||||
let uit = s;
|
||||
while (uit.length > 1 && ctx.measureText(uit + '…').width > breedte) uit = uit.slice(0, -1);
|
||||
return uit + '…';
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
// Generated, do not edit. sha256:8f2431fd9a93cf7a3b491d7a9c3586996866c1c56d872590bc1b901665a63a6e
|
||||
import { listAccounts } from '../data/accounts.js';
|
||||
import { listAssets } from '../data/assets.js';
|
||||
import { listEvents, listFlowLegs, listTradeEvents } from '../data/events.js';
|
||||
import { firstPositionDate, positionAt, positionsByAsset, unexplainedByPair } from '../data/positions.js';
|
||||
import { listPriceHistory } from '../data/price-history.js';
|
||||
import { getPref } from '../data/preferences.js';
|
||||
import { toDailyGrid } from '../signals/data.js';
|
||||
import { listProviders } from '../data/providers.js';
|
||||
import { provenanceOn, provenanceOfRow, originFrom, liveOrigin } from '../../shared/price-provenance.js';
|
||||
import { ensurePriceHistoryFor, ensurePointsAtFor, liveSpotPriceFor, lastSpotSource } from '../price/providers.js';
|
||||
import { REIKWIJDTE, categorieVoor, reikwijdteVoor } from '../tax/constants.js';
|
||||
import { isOwnWealth } from '../../shared/wealth-scope.js';
|
||||
import { CONFIG } from '../../shared/config.js';
|
||||
import { modifiedDietz, irr, averageCapital } from './performance.js';
|
||||
import { dagreeks, metSpotVandaag, todayResults } from './series.js';
|
||||
import { listValuations } from '../data/valuations.js';
|
||||
import { toUtcIso, nowUtcIso } from '../../shared/time.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function priceAt(raster, isoDate) {
|
||||
if (!raster || !raster.grid || raster.grid.length === 0) return null;
|
||||
const { grid, dagen, kind } = raster;
|
||||
const ts = Date.parse(`${isoDate}T00:00:00Z`);
|
||||
if (ts < grid[0][0]) return null;
|
||||
const grens = CONFIG.PRICE_HISTORY.CARRY_FORWARD_DAYS[kind];
|
||||
if (grens != null && !versGenoeg(dagen, ts, grens)) return null;
|
||||
const idx = Math.min(grid.length - 1, Math.floor((ts - grid[0][0]) / DAY_MS));
|
||||
return grid[idx][1];
|
||||
}
|
||||
|
||||
function versGenoeg(dagen, ts, grens) {
|
||||
if (!dagen || dagen.length === 0) return false;
|
||||
for (let i = dagen.length - 1; i >= 0; i--) {
|
||||
if (dagen[i] <= ts) return ts - dagen[i] <= grens * DAY_MS;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const peilIso = year => toUtcIso(`${year}-01-01T00:00:00`);
|
||||
|
||||
const localNowIso = () => nowUtcIso();
|
||||
|
||||
async function liveSpots(assets) {
|
||||
const m = new Map();
|
||||
for (const a of assets) {
|
||||
try { const price = await liveSpotPriceFor(a); if (price != null) m.set(a.id, price); }
|
||||
catch { }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
function waardeVan(sats, div, price) {
|
||||
if (sats === 0) return 0;
|
||||
return price != null ? sats / div * price : null;
|
||||
}
|
||||
|
||||
function currentPriceFor(asset, raster, live) {
|
||||
const spot = live.get(asset.id);
|
||||
if (spot != null) return spot;
|
||||
const grid = raster && raster.grid;
|
||||
return grid && grid.length ? grid[grid.length - 1][1] : null;
|
||||
}
|
||||
|
||||
function raster(rows, asset) {
|
||||
const dagen = [...new Set(rows.filter(r => r.fiatPrice != null && r.fiatPrice > 0).map(r => r.observedAt.slice(0, 10)))]
|
||||
.sort()
|
||||
.map(d => Date.parse(`${d}T00:00:00Z`));
|
||||
return { grid: toDailyGrid(rows), dagen, kind: asset.kind, rows };
|
||||
}
|
||||
|
||||
function herkomstVan(eigenRaster, isoDate, price, ctx) {
|
||||
if (price == null || !eigenRaster || !eigenRaster.rows) return null;
|
||||
return originFrom(provenanceOn(eigenRaster.rows, isoDate,
|
||||
{ assetKind: eigenRaster.kind, manualProviderId: ctx.manualId, usedPrice: price }), ctx.names);
|
||||
}
|
||||
|
||||
function huidigeHerkomst(asset, eigenRaster, live, price, ctx) {
|
||||
if (price == null) return null;
|
||||
if (live.get(asset.id) != null) return liveOrigin(price, lastSpotSource(asset));
|
||||
const grid = eigenRaster && eigenRaster.grid;
|
||||
if (!grid || !grid.length || !eigenRaster.rows) return null;
|
||||
const d = provenanceOn(eigenRaster.rows, new Date(grid[grid.length - 1][0]).toISOString().slice(0, 10),
|
||||
{ assetKind: eigenRaster.kind, manualProviderId: ctx.manualId, usedPrice: price });
|
||||
if (!d || !d.row) return null;
|
||||
return originFrom(provenanceOfRow(d.row, { manualProviderId: ctx.manualId }), ctx.names);
|
||||
}
|
||||
|
||||
export async function herkomstContext() {
|
||||
try {
|
||||
const providers = await listProviders();
|
||||
const manual = providers.find(p => p.key === 'manual');
|
||||
return { names: new Map(providers.map(p => [p.id, p.name])), manualId: manual ? manual.id : null };
|
||||
} catch {
|
||||
return { names: new Map(), manualId: null };
|
||||
}
|
||||
}
|
||||
|
||||
async function buildGrids(assets, peilDates = []) {
|
||||
for (const a of assets) {
|
||||
try { await ensurePriceHistoryFor(a); }
|
||||
catch { }
|
||||
}
|
||||
for (const a of assets) {
|
||||
if (peilDates.length) { try { await ensurePointsAtFor(a, peilDates); } catch { } }
|
||||
}
|
||||
const grids = new Map();
|
||||
for (const a of assets) {
|
||||
try { grids.set(a.id, raster(await listPriceHistory(a.id), a)); }
|
||||
catch { grids.set(a.id, null); }
|
||||
}
|
||||
return grids;
|
||||
}
|
||||
|
||||
async function flows(fromIso, untilIso, grids, telt = () => true, grens = []) {
|
||||
let deposits = 0, withdrawals = 0, giften = 0;
|
||||
const legs = [];
|
||||
for (const e of await listTradeEvents(fromIso, untilIso)) {
|
||||
const kant = e.type === 'buy' ? e.toAccountId
|
||||
: e.type === 'sell' ? e.fromAccountId
|
||||
: (e.fromAccountId ?? e.toAccountId);
|
||||
if (!telt(kant, e.assetId)) continue;
|
||||
const eenheden = e.amount / Math.pow(10, e.decimals);
|
||||
const fee = e.fiatFee != null ? e.fiatFee : 0;
|
||||
if (e.type === 'transfer' && e.fromAccountId != null && fee > 0) {
|
||||
deposits += fee;
|
||||
legs.push({ fiat: fee, atIso: e.occurredAt });
|
||||
}
|
||||
if (e.type === 'transfer' && e.priceOrigin !== 'none') {
|
||||
if ((e.fromAccountId == null) === (e.toAccountId == null)) continue;
|
||||
const eigenRaster = grids ? grids.get(e.assetId) : null;
|
||||
const prijs = eigenRaster ? priceAt(eigenRaster, e.occurredAt.slice(0, 10)) : null;
|
||||
if (prijs == null) continue;
|
||||
const fiat = eenheden * prijs;
|
||||
if (e.fromAccountId == null) { deposits += fiat; legs.push({ fiat, atIso: e.occurredAt }); }
|
||||
else { withdrawals += fiat; legs.push({ fiat: -fiat, atIso: e.occurredAt }); }
|
||||
continue;
|
||||
}
|
||||
if (e.priceOrigin === 'none') {
|
||||
if (e.type === 'transfer' && (e.fromAccountId == null) === (e.toAccountId == null)) continue;
|
||||
const eigenRaster = grids ? grids.get(e.assetId) : null;
|
||||
const prijs = eigenRaster ? priceAt(eigenRaster, e.occurredAt.slice(0, 10)) : null;
|
||||
const binnen = e.type === 'transfer' ? e.fromAccountId == null : e.type === 'buy';
|
||||
if (prijs != null) {
|
||||
const fiat = (binnen ? 1 : -1) * eenheden * prijs;
|
||||
giften += fiat;
|
||||
legs.push({ fiat, atIso: e.occurredAt });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const fiat = eenheden * e.fiatPrice;
|
||||
if (e.type === 'buy') {
|
||||
deposits += fiat + fee;
|
||||
legs.push({ fiat: fiat + fee, atIso: e.occurredAt });
|
||||
} else {
|
||||
withdrawals += fiat - fee;
|
||||
legs.push({ fiat: -(fiat - fee), atIso: e.occurredAt });
|
||||
}
|
||||
}
|
||||
for (const g of grens) {
|
||||
if (!(g.occurredAt > fromIso && g.occurredAt <= untilIso)) continue;
|
||||
const eigenRaster = grids ? grids.get(g.assetId) : null;
|
||||
const prijs = eigenRaster ? priceAt(eigenRaster, g.occurredAt.slice(0, 10)) : null;
|
||||
if (prijs == null) continue;
|
||||
const fiat = g.eenheden * prijs;
|
||||
if (g.naarBuiten) { withdrawals += fiat; legs.push({ fiat: -fiat, atIso: g.occurredAt }); }
|
||||
else { deposits += fiat; legs.push({ fiat, atIso: g.occurredAt }); }
|
||||
}
|
||||
return { deposits, withdrawals, giften, legs };
|
||||
}
|
||||
|
||||
function legValue(leg, eigenRaster) {
|
||||
const units = leg.units / Math.pow(10, leg.decimals);
|
||||
if (leg.priceOrigin !== 'none') {
|
||||
if (leg.type === 'buy') return units * leg.fiatPrice + (leg.fiatFee || 0);
|
||||
if (leg.type === 'sell') return units * leg.fiatPrice - (leg.fiatFee || 0);
|
||||
}
|
||||
const price = priceAt(eigenRaster, leg.occurredAt.slice(0, 10));
|
||||
return price == null ? null : units * price;
|
||||
}
|
||||
|
||||
async function pairFlows(fromIso, untilIso, grids) {
|
||||
const uit = new Map();
|
||||
for (const leg of await listFlowLegs(fromIso, untilIso)) {
|
||||
const key = `${leg.accountId}|${leg.assetId}`;
|
||||
const rec = uit.get(key) || { in: 0, uit: 0, onvolledig: false, legs: [] };
|
||||
const fiat = legValue(leg, grids.get(leg.assetId));
|
||||
if (fiat == null) rec.onvolledig = true;
|
||||
else {
|
||||
if (leg.direction === 'in') rec.in += fiat;
|
||||
else rec.uit += fiat;
|
||||
rec.legs.push({ fiat: leg.direction === 'in' ? fiat : -fiat, atIso: leg.occurredAt });
|
||||
}
|
||||
const fee = Number(leg.fiatFee) || 0;
|
||||
if (leg.type === 'transfer' && leg.direction === 'out' && fee > 0) {
|
||||
rec.in += fee;
|
||||
rec.legs.push({ fiat: fee, atIso: leg.occurredAt });
|
||||
}
|
||||
uit.set(key, rec);
|
||||
}
|
||||
return uit;
|
||||
}
|
||||
|
||||
async function setRowPct(rows, grids, { y, running, vanIso, totIso, openingPerPaar, methode }) {
|
||||
const stromen = await pairFlows(vanIso, totIso, grids);
|
||||
for (const r of rows) {
|
||||
const eindCel = running ? r.current : r.perYear[y + 1];
|
||||
const beginCel = r.perYear[y];
|
||||
if (!eindCel || !beginCel) continue;
|
||||
const key = `${r.account.id}|${r.asset.id}`;
|
||||
const stroom = stromen.get(key) || { in: 0, uit: 0, onvolledig: false, legs: [] };
|
||||
const begin = beginCel.value;
|
||||
const bruikbaar = begin != null && eindCel.value != null && !stroom.onvolledig;
|
||||
eindCel.gain = bruikbaar
|
||||
? eindCel.value - begin - stroom.in + stroom.uit - (openingPerPaar.get(key) || 0)
|
||||
: null;
|
||||
eindCel.pct = bruikbaar && begin > 0
|
||||
? rendementVan(methode, {
|
||||
gain: eindCel.gain, begin, eind: eindCel.value - (openingPerPaar.get(key) || 0), flows: stroom.legs,
|
||||
fromIso: vanIso, untilIso: totIso,
|
||||
})
|
||||
: null;
|
||||
eindCel.basis = bruikbaar && begin > 0
|
||||
? averageCapital(begin, stroom.legs, { fromIso: vanIso, untilIso: totIso })
|
||||
: null;
|
||||
eindCel.weging = bruikbaar
|
||||
? averageCapital(begin, stroom.legs, { fromIso: vanIso, untilIso: totIso })
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
export function sumReturn(cellen = []) {
|
||||
let gain = 0, weging = 0, begin = 0;
|
||||
for (const { begin: b, eind: e } of cellen) {
|
||||
if (!e || !b || e.gain == null || e.weging == null) return null;
|
||||
gain += e.gain;
|
||||
weging += e.weging;
|
||||
begin += b.value || 0;
|
||||
}
|
||||
if (!cellen.length) return null;
|
||||
return { gain, pct: begin > 0 && weging > 0 ? gain / weging : null };
|
||||
}
|
||||
|
||||
function rendementVan(methode, { gain, begin, eind, flows, fromIso, untilIso }) {
|
||||
if (methode === 'irr') {
|
||||
const r = irr([
|
||||
{ fiat: -begin, atIso: fromIso },
|
||||
...flows.map(f => ({ fiat: -f.fiat, atIso: f.atIso })),
|
||||
{ fiat: eind, atIso: untilIso },
|
||||
], { fromIso });
|
||||
if (r != null) return r;
|
||||
}
|
||||
return modifiedDietz({ gain, begin, flows, fromIso, untilIso });
|
||||
}
|
||||
|
||||
const geenRekening = () => ({ id: null, name: t('wealth.noAccount'), taxCategory: null, taxScope: null, isUnassigned: true });
|
||||
|
||||
async function restPerAsset(peil, perAssetSom) {
|
||||
const uit = new Map();
|
||||
for (const r of await positionsByAsset(peil)) {
|
||||
const rest = r.net - (perAssetSom.get(r.id) || 0);
|
||||
if (rest !== 0) uit.set(r.id, rest);
|
||||
}
|
||||
return uit;
|
||||
}
|
||||
|
||||
const withoutTaxSettings = x => ({ ...x, taxCategory: null, taxScope: null });
|
||||
|
||||
export async function buildWealthModel({ ignoreTaxSettings = false } = {}) {
|
||||
let assets = await listAssets();
|
||||
let accounts = await listAccounts();
|
||||
const hasTaxSettings = [...assets, ...accounts].some(x => x.taxCategory != null || x.taxScope != null);
|
||||
if (ignoreTaxSettings) {
|
||||
assets = assets.map(withoutTaxSettings);
|
||||
accounts = accounts.map(withoutTaxSettings);
|
||||
}
|
||||
const first = await firstPositionDate();
|
||||
const now = new Date();
|
||||
const nowIso = localNowIso();
|
||||
if (!first || assets.length === 0) return { years: [], rows: [], totals: null };
|
||||
|
||||
const firstYear = Number(first.slice(0, 4));
|
||||
const huidigJaar = now.getFullYear();
|
||||
const startPref = Number(await getPref('app', 'start_year')) || CONFIG.START_YEAR.AUTO;
|
||||
const methode = String(await getPref('app', 'mwr_method') || 'dietz');
|
||||
const startYear = Math.min(Math.max(firstYear + 1, startPref), huidigJaar);
|
||||
const ingekort = startYear > firstYear + 1;
|
||||
const years = [];
|
||||
for (let y = startYear; y <= huidigJaar; y++) years.push(y);
|
||||
|
||||
const grids = await buildGrids(assets, years.map(y => `${y}-01-01`));
|
||||
const live = await liveSpots(assets);
|
||||
const multiAsset = assets.length > 1;
|
||||
const hctx = await herkomstContext();
|
||||
|
||||
let rows = [];
|
||||
const somPerAsset = new Map();
|
||||
for (const moment of [...years.map(peilIso), nowIso]) somPerAsset.set(moment, new Map());
|
||||
const telMee = (moment, assetId, sats) => {
|
||||
const m = somPerAsset.get(moment);
|
||||
m.set(assetId, (m.get(assetId) || 0) + sats);
|
||||
};
|
||||
for (const ac of accounts) {
|
||||
for (const a of assets) {
|
||||
const eigenRaster = grids.get(a.id);
|
||||
const div = Math.pow(10, a.decimals);
|
||||
const perYear = {};
|
||||
let any = false;
|
||||
for (const y of years) {
|
||||
const sats = await positionAt(ac.id, a.id, peilIso(y));
|
||||
telMee(peilIso(y), a.id, sats);
|
||||
const price = priceAt(eigenRaster, `${y}-01-01`);
|
||||
perYear[y] = { sats, price, value: waardeVan(sats, div, price), gain: null, pct: null,
|
||||
origin: sats === 0 ? null : herkomstVan(eigenRaster, `${y}-01-01`, price, hctx) };
|
||||
if (sats !== 0) any = true;
|
||||
}
|
||||
const curSats = await positionAt(ac.id, a.id, nowIso);
|
||||
telMee(nowIso, a.id, curSats);
|
||||
const curPrice = currentPriceFor(a, eigenRaster, live);
|
||||
const current = { sats: curSats, price: curPrice, value: waardeVan(curSats, div, curPrice), gain: null, pct: null,
|
||||
origin: curSats === 0 ? null : huidigeHerkomst(a, eigenRaster, live, curPrice, hctx) };
|
||||
if (curSats !== 0) any = true;
|
||||
if (!any) continue;
|
||||
rows.push({
|
||||
account: ac, asset: a,
|
||||
categorie: categorieVoor(ac, a),
|
||||
reikwijdte: reikwijdteVoor(ac, a),
|
||||
name: multiAsset ? `${ac.name} · ${a.symbol}` : ac.name,
|
||||
perYear, current,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const restPerMoment = new Map();
|
||||
for (const [moment, som] of somPerAsset) restPerMoment.set(moment, await restPerAsset(moment, som));
|
||||
for (const a of assets) {
|
||||
const div = Math.pow(10, a.decimals);
|
||||
const eigenRaster = grids.get(a.id);
|
||||
const perYear = {};
|
||||
let any = false;
|
||||
for (const y of years) {
|
||||
const sats = restPerMoment.get(peilIso(y)).get(a.id) || 0;
|
||||
const price = priceAt(eigenRaster, `${y}-01-01`);
|
||||
perYear[y] = { sats, price, value: waardeVan(sats, div, price), gain: null, pct: null,
|
||||
origin: sats === 0 ? null : herkomstVan(eigenRaster, `${y}-01-01`, price, hctx) };
|
||||
if (sats !== 0) any = true;
|
||||
}
|
||||
const curSats = restPerMoment.get(nowIso).get(a.id) || 0;
|
||||
const curPrice = currentPriceFor(a, eigenRaster, live);
|
||||
const current = { sats: curSats, price: curPrice, value: waardeVan(curSats, div, curPrice), gain: null, pct: null,
|
||||
origin: curSats === 0 ? null : huidigeHerkomst(a, eigenRaster, live, curPrice, hctx) };
|
||||
if (curSats !== 0) any = true;
|
||||
if (!any) continue;
|
||||
const ac = geenRekening();
|
||||
rows.push({
|
||||
account: ac, asset: a,
|
||||
categorie: categorieVoor(ac, a),
|
||||
reikwijdte: reikwijdteVoor(ac, a),
|
||||
name: multiAsset ? `${ac.name} · ${a.symbol}` : ac.name,
|
||||
perYear, current,
|
||||
});
|
||||
}
|
||||
|
||||
const buitenBox3 = rows
|
||||
.filter(r => r.reikwijdte === REIKWIJDTE.NONE)
|
||||
.map(r => ({ name: r.name, value: r.current.value }));
|
||||
rows = rows.filter(r => r.reikwijdte !== REIKWIJDTE.NONE);
|
||||
const categoryRows = rows;
|
||||
rows = rows.filter(r => r.reikwijdte !== REIKWIJDTE.CATEGORY);
|
||||
|
||||
const emptyYears = years.filter(y => rows.every(r => r.perYear[y].sats === 0));
|
||||
|
||||
const totals = { perYear: {}, current: 0 };
|
||||
for (const y of years) totals.perYear[y] = rows.reduce((s, r) => s + (r.perYear[y].value || 0), 0);
|
||||
totals.current = rows.reduce((s, r) => s + (r.current.value || 0), 0);
|
||||
|
||||
const ONBEKEND = 'onbekend';
|
||||
const catVanRij = (r) => r.categorie || ONBEKEND;
|
||||
const perCategorie = (waardeVanRij, lijst = rows) => {
|
||||
const uit = {};
|
||||
for (const r of lijst) {
|
||||
const cat = catVanRij(r);
|
||||
uit[cat] = (uit[cat] || 0) + (waardeVanRij(r) || 0);
|
||||
}
|
||||
return uit;
|
||||
};
|
||||
totals.perYearPerCategorie = {};
|
||||
for (const y of years) totals.perYearPerCategorie[y] = perCategorie(r => r.perYear[y].value);
|
||||
totals.currentPerCategorie = perCategorie(r => r.current.value);
|
||||
const categorySplit = { perYear: {}, current: perCategorie(r => r.current.value, categoryRows) };
|
||||
for (const y of years) categorySplit.perYear[y] = perCategorie(r => r.perYear[y].value, categoryRows);
|
||||
|
||||
const jaarTotaal = (perCat) => Object.values(perCat).reduce((s, v) => s + v, 0);
|
||||
|
||||
const rijPerPaar = new Map(rows.map(r => [`${r.account.id}|${r.asset.id}`, r]));
|
||||
|
||||
const rekeningVan = new Map(accounts.map(a => [a.id, a]));
|
||||
const assetVan = new Map(assets.map(a => [a.id, a]));
|
||||
const telt = (accountId, assetId) =>
|
||||
reikwijdteVoor(accountId == null ? geenRekening() : rekeningVan.get(accountId), assetVan.get(assetId))
|
||||
=== REIKWIJDTE.ALL;
|
||||
const grens = (await listEvents())
|
||||
.filter(e => e.type === 'transfer' && e.fromAccountId != null && e.toAccountId != null
|
||||
&& telt(e.fromAccountId, e.assetId) !== telt(e.toAccountId, e.assetId))
|
||||
.map(e => ({
|
||||
assetId: e.assetId, occurredAt: e.occurredAt, naarBuiten: telt(e.fromAccountId, e.assetId),
|
||||
eenheden: (Number(e.amount) || 0) / Math.pow(10, assetVan.get(e.assetId)?.decimals ?? 0),
|
||||
}));
|
||||
|
||||
const aanwas = [];
|
||||
for (const y of years) {
|
||||
const running = y === now.getFullYear();
|
||||
const perCatBegin = totals.perYearPerCategorie[y];
|
||||
const perCatEind = running ? totals.currentPerCategorie : totals.perYearPerCategorie[y + 1];
|
||||
const begin = jaarTotaal(perCatBegin);
|
||||
const eindIso = running ? nowIso : peilIso(y + 1);
|
||||
const eind = perCatEind ? jaarTotaal(perCatEind) : null;
|
||||
if (eind == null) continue;
|
||||
let beginWeg = 0, eindWeg = 0;
|
||||
const zonderKoers = [];
|
||||
for (const r of rows) {
|
||||
const b = r.perYear[y], e = running ? r.current : r.perYear[y + 1];
|
||||
if (!b || !e) continue;
|
||||
const bOnbekend = b.sats !== 0 && b.value == null;
|
||||
const eOnbekend = e.sats !== 0 && e.value == null;
|
||||
if (bOnbekend === eOnbekend) continue;
|
||||
if (bOnbekend) eindWeg += e.value || 0;
|
||||
else beginWeg += b.value || 0;
|
||||
zonderKoers.push(r.name);
|
||||
}
|
||||
const f = await flows(peilIso(y), eindIso, grids, telt, grens);
|
||||
let unexplained = 0;
|
||||
let uitBalansen = 0;
|
||||
const unexplainedRows = [];
|
||||
const openingPerPaar = new Map();
|
||||
for (const brok of await unexplainedByPair(peilIso(y), eindIso)) {
|
||||
const paar = `${brok.accountId}|${brok.assetId}`;
|
||||
const r = rijPerPaar.get(paar);
|
||||
if (!r) continue;
|
||||
const prijs = running ? r.current.price : (r.perYear[y + 1] ? r.perYear[y + 1].price : null);
|
||||
if (prijs == null) continue;
|
||||
const valueOf = eenheden => eenheden / Math.pow(10, r.asset.decimals) * prijs;
|
||||
const opening = valueOf(brok.openingUnits);
|
||||
const value = valueOf(brok.units);
|
||||
uitBalansen += opening;
|
||||
openingPerPaar.set(paar, opening);
|
||||
if (Math.abs(value - opening) < 0.005) continue;
|
||||
unexplained += value - opening;
|
||||
unexplainedRows.push({ name: r.name, units: brok.units, value: value - opening });
|
||||
}
|
||||
unexplainedRows.sort((a, b) => Math.abs(b.value) - Math.abs(a.value));
|
||||
await setRowPct(rows, grids, { y, running, vanIso: peilIso(y), totIso: eindIso, openingPerPaar, methode });
|
||||
const result = (eind - eindWeg) - (begin - beginWeg) - f.deposits + f.withdrawals - uitBalansen - f.giften;
|
||||
const beginMeet = begin - beginWeg;
|
||||
const pct = beginMeet > 0
|
||||
? rendementVan(methode, {
|
||||
gain: result, begin: beginMeet, eind: eind - eindWeg - uitBalansen, flows: f.legs,
|
||||
fromIso: peilIso(y), untilIso: eindIso,
|
||||
})
|
||||
: null;
|
||||
aanwas.push({
|
||||
year: y, running, begin, eind, beginWeg,
|
||||
deposits: f.deposits, withdrawals: f.withdrawals, uitBalansen, giften: f.giften,
|
||||
result, pct, unexplained, unexplainedRows, zonderKoers,
|
||||
});
|
||||
}
|
||||
|
||||
const pct = { perYear: {}, current: null };
|
||||
for (const y of years) pct.perYear[y] = null;
|
||||
for (const a of aanwas) {
|
||||
if (a.running) pct.current = a.pct; else pct.perYear[a.year + 1] = a.pct;
|
||||
}
|
||||
|
||||
const perJaar = years.map(y => {
|
||||
const perCategorie = totals.perYearPerCategorie[y] || {};
|
||||
const rendement = aanwas.find(x => x.year === y) || null;
|
||||
return {
|
||||
year: y,
|
||||
value: totals.perYear[y],
|
||||
perCategorie,
|
||||
categorieCompleet: !(perCategorie[ONBEKEND] > 0),
|
||||
rendement: rendement ? rendement.result : null,
|
||||
running: rendement ? rendement.running : false,
|
||||
};
|
||||
});
|
||||
|
||||
return { years, nowIso, rows, totals, pct, aanwas, perJaar, multiAsset, buitenBox3, firstYear,
|
||||
ingekort, emptyYears, mwrMethod: methode, categoryRows, categorySplit, hasTaxSettings };
|
||||
}
|
||||
|
||||
export async function currentStanding() {
|
||||
const assets = await listAssets();
|
||||
if (assets.length === 0) return { rows: [], total: 0, totalSats: 0, today: null };
|
||||
const grids = await buildGrids(assets);
|
||||
const live = await liveSpots(assets);
|
||||
const hctx = await herkomstContext();
|
||||
const nowIso = localNowIso();
|
||||
const rows = [];
|
||||
let total = 0, totalSats = 0;
|
||||
const som = new Map();
|
||||
const voegToe = (ac, a, sats) => {
|
||||
if (sats === 0) return;
|
||||
const eigenRaster = grids.get(a.id);
|
||||
const price = currentPriceFor(a, eigenRaster, live);
|
||||
const value = price != null ? sats / Math.pow(10, a.decimals) * price : null;
|
||||
const origin = value != null ? huidigeHerkomst(a, eigenRaster, live, price, hctx) : null;
|
||||
rows.push({ account: ac, asset: a, sats, value, origin });
|
||||
if (value != null) total += value;
|
||||
totalSats += sats;
|
||||
};
|
||||
const accounts = await listAccounts();
|
||||
for (const ac of accounts) {
|
||||
const eigen = isOwnWealth(ac);
|
||||
for (const a of assets) {
|
||||
const sats = await positionAt(ac.id, a.id, nowIso);
|
||||
som.set(a.id, (som.get(a.id) || 0) + sats);
|
||||
if (eigen) voegToe(ac, a, sats);
|
||||
}
|
||||
}
|
||||
const rest = await restPerAsset(nowIso, som);
|
||||
for (const a of assets) voegToe(geenRekening(), a, rest.get(a.id) || 0);
|
||||
rows.sort((x, y) => (y.value || 0) - (x.value || 0));
|
||||
return { rows, total, totalSats, today: await resultToday(assets, grids, live, accounts) };
|
||||
}
|
||||
|
||||
async function resultToday(assets, grids, live, accounts) {
|
||||
try {
|
||||
const [events, valuations] = await Promise.all([listEvents(), listValuations()]);
|
||||
const prijzen = new Map(assets.map(a => [a.id, grids.get(a.id)?.grid || []]));
|
||||
const excludedAccounts = new Set(accounts.filter(ac => !isOwnWealth(ac)).map(ac => ac.id));
|
||||
return todayResults(metSpotVandaag(dagreeks({ events, valuations, assets, prijzen, excludedAccounts }), assets, live));
|
||||
} catch (err) {
|
||||
console.warn('[standing] today\'s result not computed:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Generated, do not edit. sha256:effc21e27380a5223a30516e074971aec0e56f100cf6c94c8684cfdf36fcca0e
|
||||
import { CONFIG } from '../../shared/config.js';
|
||||
|
||||
export function weightOf(atIso, fromIso, untilIso) {
|
||||
const from = Date.parse(fromIso);
|
||||
const until = Date.parse(untilIso);
|
||||
const at = Date.parse(atIso);
|
||||
if (!Number.isFinite(from) || !Number.isFinite(until) || !Number.isFinite(at)) return null;
|
||||
const span = until - from;
|
||||
if (span <= 0) return 1;
|
||||
return Math.min(1, Math.max(0, (until - at) / span));
|
||||
}
|
||||
|
||||
export function averageCapital(begin, flows = [], { fromIso, untilIso } = {}) {
|
||||
if (typeof begin !== 'number' || !Number.isFinite(begin)) return null;
|
||||
let som = begin;
|
||||
for (const f of flows) {
|
||||
const w = weightOf(f?.atIso, fromIso, untilIso);
|
||||
const fiat = Number(f?.fiat);
|
||||
if (w == null || !Number.isFinite(fiat)) continue;
|
||||
som += fiat * w;
|
||||
}
|
||||
return som;
|
||||
}
|
||||
|
||||
export function modifiedDietz({ gain, begin, flows = [], fromIso, untilIso } = {}) {
|
||||
if (typeof gain !== 'number' || !Number.isFinite(gain)) return null;
|
||||
const noemer = averageCapital(begin, flows, { fromIso, untilIso });
|
||||
if (noemer == null || !(noemer > 0)) return null;
|
||||
return gain / noemer;
|
||||
}
|
||||
|
||||
export function dailyFlows(cumulative = []) {
|
||||
return cumulative.map((v, i) => (i === 0 ? 0 : (Number(v) || 0) - (Number(cumulative[i - 1]) || 0)));
|
||||
}
|
||||
|
||||
export function timeWeighted(values = [], flows = [], outflows = []) {
|
||||
let factor = 1;
|
||||
let measured = false;
|
||||
for (const f of dailyFactors(values, flows, outflows)) {
|
||||
if (f == null) continue;
|
||||
factor *= f;
|
||||
measured = true;
|
||||
}
|
||||
return measured ? factor - 1 : null;
|
||||
}
|
||||
|
||||
export function dailyFactors(values = [], flows = [], outflows = []) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
const out = values.map(() => null);
|
||||
let closed = false;
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
const f = Number(flows[i]) || 0;
|
||||
const inflow = Math.max(f, 0);
|
||||
const outflow = Math.max(-f, 0) + (Number(outflows[i]) || 0);
|
||||
const previous = Number(values[i - 1]);
|
||||
if (closed) {
|
||||
const reopen = inflow > 0 && (!(previous > 0) || inflow >= CONFIG.WEALTH_RETURN.REOPEN_MULTIPLE * previous);
|
||||
if (!reopen) continue;
|
||||
closed = false;
|
||||
}
|
||||
const end = Number(values[i]);
|
||||
const base = previous + inflow;
|
||||
if (!Number.isFinite(end) || !Number.isFinite(base) || !(base > 0)) continue;
|
||||
out[i] = (end + outflow) / base;
|
||||
if (outflow > 0 && end < CONFIG.WEALTH_RETURN.DUST_SHARE * base) closed = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function lastDayHeld(units = [], from = 0, to = 0) {
|
||||
let i = Math.min(to, units.length - 1);
|
||||
while (i > from && !units[i]) i--;
|
||||
return i >= from && units[i] ? i : null;
|
||||
}
|
||||
|
||||
export function windowTwr(reeks = {}, venster = {}) {
|
||||
const s = windowSlices(reeks, venster);
|
||||
return s ? timeWeighted(s.values, s.inflows, s.outflows) : null;
|
||||
}
|
||||
|
||||
export function windowFactors(reeks = {}, venster = {}) {
|
||||
const out = (reeks.waarden || []).map(() => null);
|
||||
const s = windowSlices(reeks, venster);
|
||||
if (!s) return out;
|
||||
dailyFactors(s.values, s.inflows, s.outflows).forEach((f, k) => {
|
||||
const i = s.first + k;
|
||||
if (i >= 0 && i < out.length) out[i] = f;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function windowSlices(reeks, venster) {
|
||||
const waarden = reeks.waarden || [];
|
||||
const van = Number(venster.van) || 0;
|
||||
const start = reeks.eersteDag == null ? 0 : Number(reeks.eersteDag) - 1;
|
||||
const vanaf = Math.max(start, van > 0 ? van : -1);
|
||||
if (reeks.eenheden && lastDayHeld(reeks.eenheden, Math.max(vanaf, 0), venster.tot) == null) return null;
|
||||
const tot = Math.min(venster.tot, waarden.length - 1) + 1;
|
||||
const nulDag = vanaf < 0 ? [0] : [];
|
||||
const snee = r => [...nulDag, ...(r || []).slice(Math.max(vanaf, 0), tot)];
|
||||
const { erin, eruit } = flowsOf(snee(reeks.inleg), snee(reeks.ankers), reeks.uit ? snee(reeks.uit) : []);
|
||||
return { values: snee(waarden), inflows: erin, outflows: eruit, first: vanaf };
|
||||
}
|
||||
|
||||
export function flowsOf(inleg = [], ankers = [], uit = []) {
|
||||
const bruto = dailyFlows(inleg.map((v, i) => (Number(v) || 0) + (Number(uit[i]) || 0)));
|
||||
const verkocht = dailyFlows(uit);
|
||||
const anker = dailyFlows(ankers);
|
||||
return {
|
||||
erin: bruto.map((v, i) => v + Math.max(anker[i] || 0, 0)),
|
||||
eruit: bruto.map((_, i) => (verkocht[i] || 0) + Math.max(-(anker[i] || 0), 0)),
|
||||
};
|
||||
}
|
||||
|
||||
const IRR_EPS = 1e-7;
|
||||
const IRR_STAPPEN = 200;
|
||||
const IRR_MIN = -0.9999, IRR_MAX = 10;
|
||||
|
||||
function contanteWaarde(kasstromen, rente) {
|
||||
let som = 0;
|
||||
for (const k of kasstromen) som += k.fiat / Math.pow(1 + rente, k.jaren);
|
||||
return som;
|
||||
}
|
||||
|
||||
export function irr(flows = [], { fromIso } = {}) {
|
||||
const start = Date.parse(fromIso ?? flows[0]?.atIso);
|
||||
if (!Number.isFinite(start)) return null;
|
||||
const kasstromen = [];
|
||||
for (const f of flows) {
|
||||
const at = Date.parse(f?.atIso);
|
||||
const fiat = Number(f?.fiat);
|
||||
if (!Number.isFinite(at) || !Number.isFinite(fiat)) return null;
|
||||
kasstromen.push({ fiat, jaren: (at - start) / (365.2425 * 24 * 60 * 60 * 1000) });
|
||||
}
|
||||
if (!kasstromen.some(k => k.fiat > 0) || !kasstromen.some(k => k.fiat < 0)) return null;
|
||||
|
||||
let laag = IRR_MIN, hoog = IRR_MAX;
|
||||
let fLaag = contanteWaarde(kasstromen, laag);
|
||||
let fHoog = contanteWaarde(kasstromen, hoog);
|
||||
if (!Number.isFinite(fLaag) || !Number.isFinite(fHoog) || fLaag * fHoog > 0) return null;
|
||||
for (let i = 0; i < IRR_STAPPEN; i++) {
|
||||
const midden = (laag + hoog) / 2;
|
||||
const fMidden = contanteWaarde(kasstromen, midden);
|
||||
if (!Number.isFinite(fMidden)) return null;
|
||||
if (Math.abs(fMidden) < IRR_EPS || (hoog - laag) / 2 < IRR_EPS) return midden;
|
||||
if (fLaag * fMidden <= 0) { hoog = midden; fHoog = fMidden; } else { laag = midden; fLaag = fMidden; }
|
||||
}
|
||||
return (laag + hoog) / 2;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Generated, do not edit. sha256:80acd8f6f37bf4f64acd4a1259ffb1f609bdfe3d99f67a807eaad2154af68875
|
||||
import { loadCharting, readPalette } from '../ui/chart-setup.js';
|
||||
import { hexToRgb } from '../ui/charting.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { fmtBaseSigned, fmtPct, colouredSigns } from '../ui/format.js';
|
||||
|
||||
let tipEl = null;
|
||||
const hide = () => tipEl?.classList.remove('is-visible');
|
||||
|
||||
const tone = v => (v > 0.005 ? 't-pos' : v < -0.005 ? 't-neg' : '');
|
||||
|
||||
function row(label, amount, pct, extra = '') {
|
||||
const r = document.createElement('span');
|
||||
r.className = `ring-tip-row ${extra}`.trim();
|
||||
const name = document.createElement('span');
|
||||
name.className = 'ring-tip-pct';
|
||||
name.textContent = label;
|
||||
const value = document.createElement('span');
|
||||
value.className = `val ${tone(amount)}`;
|
||||
value.textContent = fmtBaseSigned(amount, colouredSigns()) + (pct == null ? '' : ` (${fmtPct(pct, { fine: true, signs: colouredSigns() })})`);
|
||||
r.append(name, value);
|
||||
return r;
|
||||
}
|
||||
|
||||
function tip({ chart, tooltip }, points, labels) {
|
||||
if (!tipEl || !tipEl.isConnected) {
|
||||
tipEl = document.createElement('div');
|
||||
tipEl.className = 'ring-tip';
|
||||
tipEl.setAttribute('role', 'tooltip');
|
||||
document.body.appendChild(tipEl);
|
||||
window.addEventListener('scroll', hide, { capture: true, passive: true });
|
||||
}
|
||||
if (!tooltip.opacity || !tooltip.dataPoints?.length) { hide(); return; }
|
||||
const n = tooltip.dataPoints[0].dataIndex;
|
||||
const p = points[n];
|
||||
const head = document.createElement('span');
|
||||
head.className = 'ring-tip-label';
|
||||
head.textContent = labels[n];
|
||||
const rows = [head];
|
||||
if (p.day != null) rows.push(row(t('wealth.returnDay'), p.day, p.pct));
|
||||
for (const c of p.top || []) rows.push(row(c.label, c.day, c.pct, 'ring-tip-row--sub'));
|
||||
rows.push(row(t('wealth.returnSoFar'), p.cumulative, null));
|
||||
if (p.carried) {
|
||||
const note = document.createElement('span');
|
||||
note.className = 'ring-tip-row ring-tip-row--sub t-muted';
|
||||
note.textContent = t('wealth.returnCarried');
|
||||
rows.push(note);
|
||||
}
|
||||
tipEl.replaceChildren(...rows);
|
||||
const box = chart.canvas.getBoundingClientRect();
|
||||
tipEl.style.left = `${Math.round(box.left + tooltip.caretX)}px`;
|
||||
tipEl.style.top = `${Math.round(box.top + tooltip.caretY)}px`;
|
||||
tipEl.classList.add('is-visible');
|
||||
}
|
||||
|
||||
const rgba = (hex, a) => { const c = hexToRgb(hex); return c ? `rgba(${c.r},${c.g},${c.b},${a})` : null; };
|
||||
|
||||
function gradientFill(green, red, pal) {
|
||||
return {
|
||||
id: 'return-gradient-fill',
|
||||
afterLayout(chart) {
|
||||
const { top, bottom } = chart.chartArea || {};
|
||||
const zero = chart.scales?.y?.getPixelForValue(0);
|
||||
if (![top, bottom, zero].every(Number.isFinite)) return;
|
||||
const upTop = rgba(green, 0.42), upZero = rgba(green, 0.04);
|
||||
const downZero = rgba(red, 0.04), downBottom = rgba(red, 0.42);
|
||||
if (!upTop || !downBottom) return;
|
||||
const { ctx } = chart;
|
||||
const up = ctx.createLinearGradient(0, top, 0, Math.max(zero, top + 1));
|
||||
up.addColorStop(0, upTop);
|
||||
up.addColorStop(1, upZero);
|
||||
const down = ctx.createLinearGradient(0, Math.min(zero, bottom - 1), 0, bottom);
|
||||
down.addColorStop(0, downZero);
|
||||
down.addColorStop(1, downBottom);
|
||||
chart.data.datasets[0].fill = { target: 'origin', above: up, below: down };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function hoverLine(pal) {
|
||||
return {
|
||||
id: 'return-hover-line',
|
||||
afterDatasetsDraw(chart) {
|
||||
const active = chart.tooltip?.getActiveElements?.() || [];
|
||||
if (!active.length) return;
|
||||
const x = active[0].element.x;
|
||||
const { top, bottom } = chart.chartArea;
|
||||
const { ctx } = chart;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = pal.textSec;
|
||||
ctx.globalAlpha = 0.6;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.round(x) + 0.5, top);
|
||||
ctx.lineTo(Math.round(x) + 0.5, bottom);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function drawReturnChart(canvas, points, labels) {
|
||||
const Chart = await loadCharting();
|
||||
const pal = readPalette();
|
||||
const green = pal.sigGreen, red = pal.sigRed;
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
plugins: [hoverLine(pal), gradientFill(green, red, pal)],
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
data: points.map(p => p.cumulative),
|
||||
borderColor: green, borderWidth: 2, pointRadius: 0, pointHoverRadius: 3,
|
||||
segment: {
|
||||
borderColor: c => (c.p1.parsed.y < 0 ? red : green),
|
||||
borderDash: c => (points[c.p0DataIndex]?.carried || points[c.p1DataIndex]?.carried ? [4, 4] : undefined),
|
||||
},
|
||||
pointHoverBackgroundColor: c => ((c.parsed?.y ?? 0) < 0 ? red : green), cubicInterpolationMode: 'monotone',
|
||||
fill: { target: 'origin', above: pal.greenZone, below: pal.redZone },
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
layout: { padding: { top: 6, right: 0, bottom: 6, left: 0 } },
|
||||
scales: {
|
||||
x: { display: false },
|
||||
y: { beginAtZero: true, ticks: { display: false }, border: { display: false }, afterFit: s => { s.width = 0; },
|
||||
grid: { color: c => (c.tick?.value === 0 ? pal.textSec : 'transparent') } },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false, external: ctx => tip(ctx, points, labels) },
|
||||
},
|
||||
},
|
||||
});
|
||||
canvas.addEventListener('mouseleave', hide);
|
||||
const destroy = chart.destroy.bind(chart);
|
||||
chart.destroy = () => { hide(); destroy(); };
|
||||
return chart;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Generated, do not edit. sha256:c1446a22c58388fe401f93d2f54194b187120f3e6ec07b997b51d0acb9bb51b4
|
||||
import * as dialog from '../ui/dialog.js';
|
||||
import { esc } from '../ui/escape.js';
|
||||
import { fmtBaseSigned, fmtPct, pctTone, colouredSigns, isBaseAsset } from '../ui/format.js';
|
||||
import { assetLabel, byKindAndLabel } from '../ui/asset-label.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { localDateNl } from '../../shared/time.js';
|
||||
import { ENUMS } from '../../shared/config.js';
|
||||
import { seriesFor } from './series.js';
|
||||
import { periodChoices, startPeriod, returnLine, idsForSelection, contributorsPerDay } from './return-view.js';
|
||||
import { getPref, setPref } from '../data/preferences.js';
|
||||
import { drawReturnChart } from './return-chart.js';
|
||||
import { carriedDays } from './carried.js';
|
||||
import { selectLabel } from '../ui/select-label.js';
|
||||
|
||||
const DASH = '-';
|
||||
const PREF_SCOPE = 'wealth.return';
|
||||
const live = new Set();
|
||||
|
||||
export function openReturnDialog({ reeks, assets = [], page = null, selection = null } = {}) {
|
||||
const closed = dialog.detail({ title: t('wealth.returnTitle'), html: '<div class="return-view return-view--dialog" data-return></div>' });
|
||||
const hosts = document.querySelectorAll('.dialog-overlay [data-return]');
|
||||
const host = hosts[hosts.length - 1];
|
||||
if (!host) return;
|
||||
const controls = host.closest('.dialog')?.querySelector('[data-dialog-aside]') || null;
|
||||
const ids = idsForSelection(selection, assets);
|
||||
const view = mount(host, { reeks, assets, page, data: dataKeyFor(selection, ids), selectionIds: ids, controls });
|
||||
closed.then(() => view.destroy());
|
||||
}
|
||||
|
||||
export async function mountReturnPanel(host, { reeks, asset }) {
|
||||
const key = String(asset.id);
|
||||
const stored = await getPref(PREF_SCOPE, key, null).catch(() => null);
|
||||
if (!host.isConnected) return null;
|
||||
return mount(host, { reeks, assets: [asset], page: null, data: `asset:${asset.id}`, stored,
|
||||
onPeriod: period => setPref(PREF_SCOPE, key, period).catch(() => { }) });
|
||||
}
|
||||
|
||||
export function refreshReturn() {
|
||||
for (const v of [...live]) v.render();
|
||||
}
|
||||
|
||||
export function lineForData({ reeks, assets = [], data = 'total', venster = null, selectionIds = null }) {
|
||||
const ids = idsOf(data, assets, selectionIds);
|
||||
const line = returnLine(seriesFor(reeks, ids && ids.length === 1 ? ids[0] : ids), venster);
|
||||
const noPct = withoutPct(ids, assets);
|
||||
const top = contributorsOf(reeks, ids ?? assets.map(a => a.id), venster, assets);
|
||||
const carried = carriedDays(reeks, ids, assets);
|
||||
return {
|
||||
amount: line.amount,
|
||||
pct: noPct ? null : line.pct,
|
||||
points: line.points.map(p => ({ ...p, pct: noPct ? null : p.pct, top: top.get(p.i) || [], carried: carried.has(p.i) })),
|
||||
};
|
||||
}
|
||||
|
||||
export function drawLine(canvas, reeks, points) {
|
||||
return drawReturnChart(canvas, points, points.map(p => localDateNl(`${reeks.dagen[p.i]}T00:00:00Z`)));
|
||||
}
|
||||
|
||||
function dataKeyFor(selection, ids) {
|
||||
if (!ids) return 'total';
|
||||
if (selection?.kind) return `kind:${selection.kind}`;
|
||||
if (ids.length === 1) return `asset:${ids[0]}`;
|
||||
return 'selection';
|
||||
}
|
||||
|
||||
function idsOf(key, assets, selectionIds) {
|
||||
if (key === 'total') return null;
|
||||
if (key === 'selection') return selectionIds;
|
||||
if (key.startsWith('kind:')) return assets.filter(a => a.kind === key.slice(5)).map(a => a.id);
|
||||
return [Number(key.slice(6))];
|
||||
}
|
||||
|
||||
const withoutPct = (ids, assets) => ids?.length === 1 && isBaseAsset(assets.find(a => a.id === ids[0]) || {});
|
||||
|
||||
function contributorsOf(reeks, ids, venster, assets) {
|
||||
const byId = new Map(assets.map(a => [a.id, a]));
|
||||
const named = new Map();
|
||||
for (const [i, list] of contributorsPerDay(reeks, ids, venster)) {
|
||||
named.set(i, list.filter(c => byId.has(c.id)).map(c => ({
|
||||
label: assetLabel(byId.get(c.id)), day: c.day, pct: isBaseAsset(byId.get(c.id)) ? null : c.pct,
|
||||
})));
|
||||
}
|
||||
return named;
|
||||
}
|
||||
|
||||
function mount(host, { reeks, assets, page, data, selectionIds = null, controls = null, stored = null, onPeriod = null }) {
|
||||
const length = reeks?.dagen?.length || 0;
|
||||
const periods = periodChoices(length, page);
|
||||
const state = { data, period: startPeriod(periods, stored), chart: null, gen: 0, lines: null };
|
||||
|
||||
const view = {
|
||||
render() {
|
||||
if (!host.isConnected) { view.destroy(); return; }
|
||||
if (!length) { host.innerHTML = `<div class="t-muted-cell">${t('wealth.noChart')}</div>`; return; }
|
||||
const lines = new Map(periods.map(p => [p.key, lineForData({ reeks, assets, data: state.data, venster: p.venster, selectionIds })]));
|
||||
state.lines = lines;
|
||||
if (controls) controls.innerHTML = dataSelect(assets, state.data, selectionIds);
|
||||
host.innerHTML = `
|
||||
<div class="return-periods" role="group">${periods.map(p => periodButton(p, lines.get(p.key), state.period)).join('')}</div>
|
||||
<div class="return-plot"><canvas data-return-chart></canvas></div>`;
|
||||
drawChart(lines.get(state.period)?.points || []);
|
||||
},
|
||||
destroy() {
|
||||
live.delete(view);
|
||||
state.chart?.destroy();
|
||||
state.chart = null;
|
||||
},
|
||||
};
|
||||
|
||||
async function drawChart(points) {
|
||||
const gen = ++state.gen;
|
||||
state.chart?.destroy();
|
||||
state.chart = null;
|
||||
const canvas = host.querySelector('[data-return-chart]');
|
||||
if (!canvas) return;
|
||||
if (points.length < 2) {
|
||||
canvas.parentElement.innerHTML = `<div class="t-muted-cell">${t('wealth.noChart')}</div>`;
|
||||
return;
|
||||
}
|
||||
const chart = await drawLine(canvas, reeks, points).catch(() => null);
|
||||
if (gen !== state.gen || !host.isConnected) { chart?.destroy(); return; }
|
||||
state.chart = chart;
|
||||
}
|
||||
|
||||
host.addEventListener('click', e => {
|
||||
const btn = e.target.closest('[data-period]');
|
||||
if (!btn || btn.disabled || btn.dataset.period === state.period) return;
|
||||
state.period = btn.dataset.period;
|
||||
onPeriod?.(state.period);
|
||||
for (const b of host.querySelectorAll('[data-period]')) b.classList.toggle('is-on', b === btn);
|
||||
if (!host.querySelector('[data-return-chart]')) {
|
||||
host.querySelector('.return-plot').innerHTML = '<canvas data-return-chart></canvas>';
|
||||
}
|
||||
drawChart(state.lines?.get(state.period)?.points || []);
|
||||
});
|
||||
controls?.addEventListener('change', e => {
|
||||
const sel = e.target.closest('[data-return-data]');
|
||||
if (!sel) return;
|
||||
state.data = sel.value;
|
||||
view.render();
|
||||
});
|
||||
|
||||
live.add(view);
|
||||
view.render();
|
||||
return view;
|
||||
}
|
||||
|
||||
function periodLabel(p) {
|
||||
if (p.key === 'page') return p.label ?? DASH;
|
||||
if (p.days == null) return t('wealth.sinceStart');
|
||||
if (p.days === 1) return t('wealth.periodDay');
|
||||
if (p.days === 365) return t('wealth.periodYear');
|
||||
return t('wealth.periodDays', { n: p.days });
|
||||
}
|
||||
|
||||
const tone = v => (v == null ? '' : v > 0.005 ? 't-pos' : v < -0.005 ? 't-neg' : '');
|
||||
|
||||
function periodButton(p, line, current) {
|
||||
const on = p.key === current ? ' is-on' : '';
|
||||
const off = p.venster ? '' : ' disabled';
|
||||
const amount = line?.amount == null ? DASH : `<span class="val">${esc(fmtBaseSigned(line.amount, colouredSigns()))}</span>`;
|
||||
const pct = line?.pct == null ? '' : fmtPct(line.pct, { fine: p.days === 1, signs: colouredSigns() });
|
||||
return `<button type="button" class="return-period${on}" data-period="${esc(p.key)}"${off}>
|
||||
<span class="return-period-label">${esc(periodLabel(p))}</span>
|
||||
<span class="return-period-amount ${tone(line?.amount)}">${amount}</span>
|
||||
<span class="return-period-pct">${pct ? `<span class="delta ${pctTone(line.pct, { fine: p.days === 1 })}">${esc(pct)}</span>` : ''}</span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function dataSelect(assets, current, selectionIds) {
|
||||
const opt = (value, label) => `<option value="${esc(value)}"${value === current ? ' selected' : ''}>${esc(label)}</option>`;
|
||||
const kinds = ENUMS.ASSET_KIND.filter(k => assets.some(a => a.kind === k));
|
||||
const sorted = [...assets].sort(byKindAndLabel);
|
||||
return selectLabel(`<select data-return-data aria-label="${esc(t('wealth.returnData'))}">
|
||||
${opt('total', t('wealth.returnTotal'))}
|
||||
${selectionIds?.length > 1 ? opt('selection', t('wealth.returnSelection', { n: selectionIds.length })) : ''}
|
||||
<optgroup label="${esc(t('wealth.returnKinds'))}">${kinds.map(k => opt(`kind:${k}`, t('enums.asset_kind.' + k))).join('')}</optgroup>
|
||||
<optgroup label="${esc(t('wealth.returnAssets'))}">${sorted.map(a => opt(`asset:${a.id}`, assetLabel(a))).join('')}</optgroup>
|
||||
</select>`);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Generated, do not edit. sha256:79d0944a46421ed3e71c6aff535ca9c3c77b373253595260dd7e749f587d9edd
|
||||
import { CONFIG } from '../../shared/config.js';
|
||||
import { windowTwr, windowFactors } from './performance.js';
|
||||
import { dailyResults, meetVenster, rollingWindows, seriesFor, windowResult } from './series.js';
|
||||
|
||||
export function periodChoices(length = 0, page = null, days = CONFIG.WEALTH_RETURN.ROLLING_DAYS) {
|
||||
const rolling = rollingWindows(length, days)
|
||||
.map(p => ({ key: p.days == null ? 'start' : `d${p.days}`, days: p.days, label: null, venster: p.venster }));
|
||||
if (!page?.venster || !rolling.length) return rolling;
|
||||
const venster = meetVenster(page.venster, length);
|
||||
if (venster.van <= 0 && venster.tot >= length - 1) return rolling;
|
||||
return [{ key: 'page', days: null, label: page.label ?? null, venster }, ...rolling];
|
||||
}
|
||||
|
||||
export function startPeriod(periods, stored = null) {
|
||||
if (stored && periods.some(p => p.key === stored && p.venster)) return stored;
|
||||
return periods.some(p => p.key === 'page') ? 'page' : 'start';
|
||||
}
|
||||
|
||||
export function returnLine(input = {}, venster = null) {
|
||||
if (!venster) return { amount: null, pct: null, points: [] };
|
||||
const results = dailyResults(input);
|
||||
const factors = windowFactors(input, venster);
|
||||
const tot = Math.min(venster.tot, results.length - 1);
|
||||
const points = [];
|
||||
let cumulative = 0;
|
||||
if (venster.van > 0) points.push({ i: venster.van, cumulative: 0, day: null, pct: null });
|
||||
for (let i = venster.van > 0 ? venster.van + 1 : 0; i <= tot; i++) {
|
||||
cumulative += results[i];
|
||||
points.push({ i, cumulative, day: results[i], pct: factors[i] == null ? null : factors[i] - 1 });
|
||||
}
|
||||
return { amount: windowResult(input, venster), pct: windowTwr(input, venster), points };
|
||||
}
|
||||
|
||||
export function contributorsPerDay(reeks = {}, ids = [], venster = null, n = 3) {
|
||||
const out = new Map();
|
||||
if (!venster || !ids || ids.length < 2) return out;
|
||||
const per = ids.map(id => {
|
||||
const input = seriesFor(reeks, id);
|
||||
return { id, results: dailyResults(input), factors: windowFactors(input, venster) };
|
||||
});
|
||||
const tot = Math.min(venster.tot, (reeks.dagen?.length || 0) - 1);
|
||||
for (let i = venster.van > 0 ? venster.van + 1 : 0; i <= tot; i++) {
|
||||
const top = per
|
||||
.map(p => ({ id: p.id, day: p.results[i] || 0, pct: p.factors[i] == null ? null : p.factors[i] - 1 }))
|
||||
.filter(c => Math.abs(c.day) >= 0.005)
|
||||
.sort((a, b) => Math.abs(b.day) - Math.abs(a.day))
|
||||
.slice(0, n);
|
||||
if (top.length) out.set(i, top);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function idsForSelection(sel, assets = []) {
|
||||
if (sel?.assetIds?.length) return [...sel.assetIds];
|
||||
if (sel?.kind) {
|
||||
const ids = assets.filter(a => a.kind === sel.kind).map(a => a.id);
|
||||
return ids.length ? ids : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Generated, do not edit. sha256:0655118dcbbdee6f738790678dd93e04fa937dd8c25c946b1d37d8d899f98d2a
|
||||
export function sameSelection(a, b) {
|
||||
if (!a || !b) return a === b;
|
||||
if (a.kind || b.kind) return a.kind === b.kind;
|
||||
if (a.account != null || b.account != null) return a.account === b.account;
|
||||
return String(a.assetIds) === String(b.assetIds);
|
||||
}
|
||||
|
||||
export function selectionFromStored(bewaard) {
|
||||
if (!bewaard || typeof bewaard !== 'object') return null;
|
||||
if (bewaard.kind) return { kind: String(bewaard.kind) };
|
||||
const ids = Array.isArray(bewaard.assetIds) ? bewaard.assetIds.map(Number).filter(n => Number.isInteger(n) && n > 0) : [];
|
||||
const account = Number(bewaard.account);
|
||||
if (bewaard.account != null && Number.isInteger(account) && account >= 0) return { account, assetIds: ids };
|
||||
return ids.length ? { assetIds: ids } : null;
|
||||
}
|
||||
|
||||
export function hiddenFromStored(bewaard, { allowZero = false } = {}) {
|
||||
if (!Array.isArray(bewaard)) return new Set();
|
||||
return new Set(bewaard.map(Number).filter(n => Number.isInteger(n) && (allowZero ? n >= 0 : n > 0)));
|
||||
}
|
||||
|
||||
export function hiddenAfterFilter(hidden, itemIds, selected) {
|
||||
const chips = new Set(itemIds);
|
||||
const s = new Set([...hidden].filter(id => !chips.has(id)));
|
||||
for (const id of itemIds) if (!selected.has(id)) s.add(id);
|
||||
return [...s].sort((a, b) => (typeof a === 'number' && typeof b === 'number' ? a - b : String(a).localeCompare(String(b))));
|
||||
}
|
||||
|
||||
export const NO_ACCOUNT_KIND = 'none';
|
||||
export const accountKindOf = c => c.account?.kind ?? NO_ACCOUNT_KIND;
|
||||
|
||||
export function hiddenKindsFromStored(bewaard, allowed = []) {
|
||||
if (!Array.isArray(bewaard)) return new Set();
|
||||
const geldig = new Set([...allowed, NO_ACCOUNT_KIND]);
|
||||
return new Set(bewaard.map(String).filter(k => geldig.has(k)));
|
||||
}
|
||||
|
||||
export function accountKindItems(cards = []) {
|
||||
const uit = [];
|
||||
for (const c of cards) {
|
||||
const kind = accountKindOf(c);
|
||||
if (!uit.includes(kind)) uit.push(kind);
|
||||
}
|
||||
return uit;
|
||||
}
|
||||
|
||||
export function splitHidden(kaarten = [], hidden = new Set(), keyOf = k => k.asset.id) {
|
||||
const shown = [], verborgen = [];
|
||||
for (const k of kaarten) (hidden.has(keyOf(k)) ? verborgen : shown).push(k);
|
||||
return { shown, hidden: verborgen };
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
// Generated, do not edit. sha256:ed8ca8faa6323fa2c4ff508c950678174612f6518b6d32ceba92945717d7bce7
|
||||
import { listEvents } from '../data/events.js';
|
||||
import { listValuations } from '../data/valuations.js';
|
||||
import { listAssets } from '../data/assets.js';
|
||||
import { listPriceHistory } from '../data/price-history.js';
|
||||
import { toDailyGrid } from '../signals/data.js';
|
||||
import { liveSpotPriceFor } from '../price/providers.js';
|
||||
import { nowUtcIso } from '../../shared/time.js';
|
||||
import { saleResult, feeResult } from '../../shared/wealth-metrics.js';
|
||||
import { CONFIG } from '../../shared/config.js';
|
||||
import { windowTwr } from './performance.js';
|
||||
|
||||
const DAG_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export const dagVan = iso => String(iso).slice(0, 10);
|
||||
|
||||
const paar = (accountId, assetId) => `${accountId ?? 'geen'}|${assetId}`;
|
||||
const accountVanSleutel = sleutel => {
|
||||
const deel = String(sleutel).split('|')[0];
|
||||
return deel === 'geen' ? null : Number(deel);
|
||||
};
|
||||
|
||||
export function dagenTussen(vanDag, totDag) {
|
||||
const start = Date.parse(`${vanDag}T00:00:00Z`);
|
||||
const eind = Date.parse(`${totDag}T00:00:00Z`);
|
||||
if (!Number.isFinite(start) || !Number.isFinite(eind) || eind < start) return [];
|
||||
const uit = [];
|
||||
for (let t = start; t <= eind; t += DAG_MS) uit.push(new Date(t).toISOString().slice(0, 10));
|
||||
return uit;
|
||||
}
|
||||
|
||||
export function deltasVan(e) {
|
||||
const bedrag = Number(e.amount) || 0;
|
||||
const fee = Number(e.unitFee) || 0;
|
||||
switch (e.type) {
|
||||
case 'buy':
|
||||
case 'interest':
|
||||
return [{ sleutel: paar(e.toAccountId, e.assetId), delta: bedrag }];
|
||||
case 'sell':
|
||||
return [{ sleutel: paar(e.fromAccountId, e.assetId), delta: -(bedrag + fee) }];
|
||||
case 'transfer':
|
||||
return [
|
||||
...(e.fromAccountId != null ? [{ sleutel: paar(e.fromAccountId, e.assetId), delta: -(bedrag + fee) }] : []),
|
||||
...(e.toAccountId != null ? [{ sleutel: paar(e.toAccountId, e.assetId), delta: bedrag }] : []),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function ownView(e, excluded) {
|
||||
if (!excluded || !excluded.size) return e;
|
||||
const outside = id => id != null && excluded.has(id);
|
||||
if (e.type === 'transfer') {
|
||||
const fromOwn = e.fromAccountId != null && !outside(e.fromAccountId);
|
||||
const toOwn = e.toAccountId != null && !outside(e.toAccountId);
|
||||
if (!fromOwn && !toOwn) return null;
|
||||
if (fromOwn && outside(e.toAccountId)) return { ...e, toAccountId: null, crossing: -1 };
|
||||
if (toOwn && outside(e.fromAccountId)) return { ...e, fromAccountId: null, crossing: 1 };
|
||||
return e;
|
||||
}
|
||||
if (e.type === 'sell') return outside(e.fromAccountId) ? null : e;
|
||||
return outside(e.toAccountId) ? null : e;
|
||||
}
|
||||
|
||||
function accountsVan(boekingen, ankers) {
|
||||
const uit = new Set();
|
||||
for (const e of boekingen) for (const { sleutel } of deltasVan(e)) uit.add(accountVanSleutel(sleutel));
|
||||
for (const v of ankers) uit.add(v.accountId);
|
||||
return [...uit];
|
||||
}
|
||||
|
||||
function accountFlows(e, waardeVan) {
|
||||
const v = waardeVan(e.amount);
|
||||
if (v == null) return [];
|
||||
const van = e.fromAccountId ?? null;
|
||||
const naar = e.toAccountId ?? null;
|
||||
if (e.type === 'transfer') {
|
||||
if (van != null && naar != null) return van === naar ? [] : [{ accountId: van, fiat: -v }, { accountId: naar, fiat: v }];
|
||||
if (van == null && naar == null) return [];
|
||||
return van == null ? [{ accountId: naar, fiat: v }] : [{ accountId: van, fiat: -v }];
|
||||
}
|
||||
const uit = [];
|
||||
if (e.priceOrigin === 'none') uit.push({ accountId: e.type === 'sell' ? van : naar, fiat: e.type === 'buy' ? v : -v });
|
||||
if (e.type === 'interest') uit.push({ accountId: naar, fiat: v, rente: v });
|
||||
return uit;
|
||||
}
|
||||
|
||||
const oplopend = perAsset => new Map([...perAsset].map(([id, set]) => [id, [...set].sort((a, b) => a - b)]));
|
||||
|
||||
function koersOp(grid, dag) {
|
||||
if (!grid || !grid.length) return null;
|
||||
const ts = Date.parse(`${dag}T00:00:00Z`);
|
||||
if (ts < grid[0][0]) return null;
|
||||
const i = Math.min(grid.length - 1, Math.floor((ts - grid[0][0]) / DAG_MS));
|
||||
return grid[i][1];
|
||||
}
|
||||
|
||||
export function dagreeks({ events = [], valuations = [], assets = [], prijzen = new Map(),
|
||||
vanDag = null, totDag = null, excludedAccounts = null } = {}) {
|
||||
const buiten = excludedAccounts && excludedAccounts.size ? excludedAccounts : null;
|
||||
const ankers = valuations.filter(v => v.accountId != null && !(buiten && buiten.has(v.accountId)));
|
||||
const boekingen = buiten ? events.map(e => ownView(e, buiten)).filter(Boolean) : events;
|
||||
const eersteDag = vanDag ?? vroegsteDag(boekingen, ankers);
|
||||
const laatsteDag = totDag ?? dagVan(nowUtcIso());
|
||||
const dagen = eersteDag ? dagenTussen(eersteDag, laatsteDag) : [];
|
||||
const leeg = { dagen: [], waarde: [], ingelegd: [], uitgehaald: [], perAsset: new Map(), inlegPerAsset: new Map(), uitPerAsset: new Map(), ankersPerAsset: new Map(), rentePerAsset: new Map(), gerealiseerdPerAsset: new Map(), onbekendPerAsset: new Map(), nietVerklaardPerAsset: new Map(),
|
||||
koersPerAsset: new Map(), eenhedenPerAsset: new Map(),
|
||||
aankopen: new Map(), verkopen: new Map(), eersteDagPerAsset: new Map(),
|
||||
perAccount: new Map(), inlegPerAccount: new Map(), uitPerAccount: new Map(), ankersPerAccount: new Map(),
|
||||
rentePerAccount: new Map(), nietVerklaardPerAccount: new Map(), bezitPerAccount: new Map(),
|
||||
eersteDagPerAccount: new Map(), standNu: [] };
|
||||
if (!dagen.length) return leeg;
|
||||
|
||||
const tijdlijn = [
|
||||
...boekingen.map(e => ({ op: e.occurredAt, soort: 'boeking', e })),
|
||||
...ankers.map(v => ({ op: v.rateAt, soort: 'anker', v })),
|
||||
].sort((a, b) => String(a.op).localeCompare(String(b.op)));
|
||||
|
||||
const decimalen = new Map(assets.map(a => [a.id, Math.pow(10, a.decimals)]));
|
||||
const stand = new Map();
|
||||
const assetVanPaar = new Map();
|
||||
let inleg = 0;
|
||||
|
||||
const waarde = [];
|
||||
const ingelegd = [];
|
||||
const perAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const koersPerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const eenhedenPerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const inlegPerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const inlegNu = new Map(assets.map(a => [a.id, 0]));
|
||||
let uit = 0;
|
||||
const uitgehaald = [];
|
||||
const uitPerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const uitNu = new Map(assets.map(a => [a.id, 0]));
|
||||
const ankersPerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const ankerNu = new Map(assets.map(a => [a.id, 0]));
|
||||
const rentePerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const renteNu = new Map(assets.map(a => [a.id, 0]));
|
||||
const gerealiseerdPerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const gerealiseerdNu = new Map(assets.map(a => [a.id, 0]));
|
||||
const onbekendPerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const onbekendNu = new Map(assets.map(a => [a.id, 0]));
|
||||
const kostprijsPerAsset = new Map(assets.map(a => [a.id, { buy_amount: 0, buy_spot_raw: 0, buy_fee_total: 0, gift_amount: 0 }]));
|
||||
const eenhedenVan = assetId => {
|
||||
let n = 0;
|
||||
for (const [sleutel, eenheden] of stand) if (assetVanPaar.get(sleutel) === assetId) n += eenheden;
|
||||
return n;
|
||||
};
|
||||
const nietVerklaardPerAsset = new Map(assets.map(a => [a.id, []]));
|
||||
const nietVerklaardNu = new Map(assets.map(a => [a.id, 0]));
|
||||
const geboekt = new Set();
|
||||
const aankoopDagen = new Map(assets.map(a => [a.id, new Set()]));
|
||||
const verkoopDagen = new Map(assets.map(a => [a.id, new Set()]));
|
||||
const eersteDagPerAsset = new Map(assets.map(a => [a.id, null]));
|
||||
|
||||
const accounts = accountsVan(boekingen, ankers);
|
||||
const perAccountReeks = () => new Map(accounts.map(id => [id, []]));
|
||||
const perAccountNu = () => new Map(accounts.map(id => [id, 0]));
|
||||
const acc = {
|
||||
reeks: { waarde: perAccountReeks(), inleg: perAccountReeks(), uit: perAccountReeks(), ankers: perAccountReeks(),
|
||||
rente: perAccountReeks(), nietVerklaard: perAccountReeks(), bezit: perAccountReeks() },
|
||||
nu: { inleg: perAccountNu(), uit: perAccountNu(), ankers: perAccountNu(), rente: perAccountNu(), nietVerklaard: perAccountNu() },
|
||||
};
|
||||
const accBij = (soort, accountId, v) => { if (acc.nu[soort].has(accountId)) acc.nu[soort].set(accountId, acc.nu[soort].get(accountId) + v); };
|
||||
const eersteDagPerAccount = new Map(accounts.map(id => [id, null]));
|
||||
|
||||
let i = 0;
|
||||
for (const [dagNr, dag] of dagen.entries()) {
|
||||
const grens = `${dag}T23:59:59.999Z`;
|
||||
while (i < tijdlijn.length && String(tijdlijn[i].op) <= grens) {
|
||||
const item = tijdlijn[i++];
|
||||
if (item.soort === 'anker') {
|
||||
const s = paar(item.v.accountId, item.v.assetId);
|
||||
const nieuw = Number(item.v.balance) || 0;
|
||||
const sprong = nieuw - (stand.get(s) || 0);
|
||||
const koers = koersOp(prijzen.get(item.v.assetId), dag);
|
||||
if (sprong !== 0 && koers != null && ankerNu.has(item.v.assetId)) {
|
||||
const fiat = (sprong / (decimalen.get(item.v.assetId) || 1)) * koers;
|
||||
ankerNu.set(item.v.assetId, ankerNu.get(item.v.assetId) + fiat);
|
||||
const bevinding = geboekt.has(s) && !item.v.gapAcceptedAt;
|
||||
if (bevinding) nietVerklaardNu.set(item.v.assetId, nietVerklaardNu.get(item.v.assetId) + fiat);
|
||||
accBij('ankers', item.v.accountId, fiat);
|
||||
if (bevinding) accBij('nietVerklaard', item.v.accountId, fiat);
|
||||
}
|
||||
stand.set(s, nieuw);
|
||||
assetVanPaar.set(s, item.v.assetId);
|
||||
continue;
|
||||
}
|
||||
const e = item.e;
|
||||
const kp = kostprijsPerAsset.get(e.assetId);
|
||||
if (kp && e.type === 'sell' && e.priceOrigin !== 'none') {
|
||||
const decimals = assets.find(a => a.id === e.assetId)?.decimals ?? 0;
|
||||
const r = saleResult({ decimals, net: eenhedenVan(e.assetId), ...kp }, e);
|
||||
if (r == null) onbekendNu.set(e.assetId, onbekendNu.get(e.assetId) + 1);
|
||||
else gerealiseerdNu.set(e.assetId, gerealiseerdNu.get(e.assetId) + r);
|
||||
}
|
||||
if (kp && e.type === 'transfer' && e.fromAccountId != null && Number(e.unitFee) > 0) {
|
||||
const decimals = assets.find(a => a.id === e.assetId)?.decimals ?? 0;
|
||||
const r = feeResult({ decimals, net: eenhedenVan(e.assetId), ...kp }, e.unitFee);
|
||||
if (r == null) onbekendNu.set(e.assetId, onbekendNu.get(e.assetId) + 1);
|
||||
else gerealiseerdNu.set(e.assetId, gerealiseerdNu.get(e.assetId) + r);
|
||||
}
|
||||
if (kp && e.type === 'transfer' && e.fromAccountId != null && Number(e.fiatFee) > 0) {
|
||||
gerealiseerdNu.set(e.assetId, gerealiseerdNu.get(e.assetId) - Number(e.fiatFee));
|
||||
}
|
||||
if (kp) {
|
||||
const n = Number(e.amount) || 0;
|
||||
if (e.type === 'buy' && e.priceOrigin !== 'none') {
|
||||
kp.buy_amount += n;
|
||||
kp.buy_spot_raw += n * (Number(e.fiatPrice) || 0);
|
||||
kp.buy_fee_total += Number(e.fiatFee) || 0;
|
||||
}
|
||||
if (e.priceOrigin === 'none') {
|
||||
if (e.type === 'buy') kp.gift_amount += n;
|
||||
else if (e.type === 'sell') kp.gift_amount -= n;
|
||||
else if (e.type === 'transfer' && e.fromAccountId == null && e.toAccountId != null) kp.gift_amount += n;
|
||||
else if (e.type === 'transfer' && e.toAccountId == null && e.fromAccountId != null) kp.gift_amount -= n;
|
||||
}
|
||||
}
|
||||
for (const { sleutel, delta } of deltasVan(e)) {
|
||||
stand.set(sleutel, (stand.get(sleutel) || 0) + delta);
|
||||
assetVanPaar.set(sleutel, e.assetId);
|
||||
geboekt.add(sleutel);
|
||||
}
|
||||
const bij = inlegVan(e, decimalen.get(e.assetId) || 1);
|
||||
inleg += bij;
|
||||
if (inlegNu.has(e.assetId)) inlegNu.set(e.assetId, inlegNu.get(e.assetId) + bij);
|
||||
const eruit = uitVan(e, decimalen.get(e.assetId) || 1);
|
||||
uit += eruit;
|
||||
if (uitNu.has(e.assetId)) uitNu.set(e.assetId, uitNu.get(e.assetId) + eruit);
|
||||
if (decimalen.has(e.assetId)) {
|
||||
const kant = e.type === 'sell' || e.type === 'transfer' ? (e.fromAccountId ?? null) : (e.toAccountId ?? null);
|
||||
accBij('inleg', kant, bij);
|
||||
accBij('uit', kant, eruit);
|
||||
const koers = koersOp(prijzen.get(e.assetId), dag);
|
||||
const waardeVan = n => (koers == null ? null : ((Number(n) || 0) / decimalen.get(e.assetId)) * koers);
|
||||
for (const f of accountFlows(e, waardeVan)) {
|
||||
accBij('ankers', f.accountId, f.fiat);
|
||||
if (f.rente) accBij('rente', f.accountId, f.rente);
|
||||
}
|
||||
}
|
||||
const eenzijdig = e.type === 'transfer' && (e.fromAccountId == null) !== (e.toAccountId == null);
|
||||
if (e.priceOrigin === 'none' && (e.type !== 'transfer' || eenzijdig) && ankerNu.has(e.assetId)) {
|
||||
const koers = koersOp(prijzen.get(e.assetId), dag);
|
||||
if (koers != null) {
|
||||
const binnen = e.type === 'transfer' ? e.fromAccountId == null : e.type === 'buy';
|
||||
const fiat = ((Number(e.amount) || 0) / (decimalen.get(e.assetId) || 1)) * koers;
|
||||
ankerNu.set(e.assetId, ankerNu.get(e.assetId) + (binnen ? fiat : -fiat));
|
||||
}
|
||||
}
|
||||
if (eenzijdig && e.priceOrigin !== 'none' && ankerNu.has(e.assetId)) {
|
||||
const koers = koersOp(prijzen.get(e.assetId), dag);
|
||||
if (koers != null) {
|
||||
const fiat = ((Number(e.amount) || 0) / (decimalen.get(e.assetId) || 1)) * koers;
|
||||
ankerNu.set(e.assetId, ankerNu.get(e.assetId) + (e.fromAccountId == null ? fiat : -fiat));
|
||||
}
|
||||
}
|
||||
if (e.type === 'interest' && ankerNu.has(e.assetId)) {
|
||||
const koers = koersOp(prijzen.get(e.assetId), dag);
|
||||
if (koers != null) {
|
||||
const fiat = ((Number(e.amount) || 0) / (decimalen.get(e.assetId) || 1)) * koers;
|
||||
ankerNu.set(e.assetId, ankerNu.get(e.assetId) + fiat);
|
||||
renteNu.set(e.assetId, renteNu.get(e.assetId) + fiat);
|
||||
}
|
||||
}
|
||||
if (dagVan(e.occurredAt) === dag) {
|
||||
if (e.type === 'buy') aankoopDagen.get(e.assetId)?.add(dagNr);
|
||||
else if (e.type === 'sell') verkoopDagen.get(e.assetId)?.add(dagNr);
|
||||
}
|
||||
}
|
||||
|
||||
const eenhedenVandaag = new Map();
|
||||
for (const [sleutel, eenheden] of stand) {
|
||||
const assetId = assetVanPaar.get(sleutel);
|
||||
eenhedenVandaag.set(assetId, (eenhedenVandaag.get(assetId) || 0) + eenheden);
|
||||
}
|
||||
let dagTotaal = 0;
|
||||
for (const a of assets) {
|
||||
const eenheden = eenhedenVandaag.get(a.id) || 0;
|
||||
if (eenheden !== 0 && eersteDagPerAsset.get(a.id) === null) eersteDagPerAsset.set(a.id, dagNr);
|
||||
const koers = koersOp(prijzen.get(a.id), dag);
|
||||
const v = koers == null ? 0 : (eenheden / (decimalen.get(a.id) || 1)) * koers;
|
||||
perAsset.get(a.id).push(v);
|
||||
koersPerAsset.get(a.id).push(koers);
|
||||
eenhedenPerAsset.get(a.id).push(eenheden);
|
||||
inlegPerAsset.get(a.id).push(inlegNu.get(a.id) || 0);
|
||||
uitPerAsset.get(a.id).push(uitNu.get(a.id) || 0);
|
||||
ankersPerAsset.get(a.id).push(ankerNu.get(a.id) || 0);
|
||||
rentePerAsset.get(a.id).push(renteNu.get(a.id) || 0);
|
||||
gerealiseerdPerAsset.get(a.id).push(gerealiseerdNu.get(a.id) || 0);
|
||||
onbekendPerAsset.get(a.id).push(onbekendNu.get(a.id) || 0);
|
||||
nietVerklaardPerAsset.get(a.id).push(nietVerklaardNu.get(a.id) || 0);
|
||||
dagTotaal += v;
|
||||
}
|
||||
waarde.push(dagTotaal);
|
||||
ingelegd.push(inleg);
|
||||
uitgehaald.push(uit);
|
||||
|
||||
const accWaarde = new Map(accounts.map(id => [id, 0]));
|
||||
const accBezit = new Map(accounts.map(id => [id, 0]));
|
||||
for (const [sleutel, eenheden] of stand) {
|
||||
const assetId = assetVanPaar.get(sleutel);
|
||||
const accountId = accountVanSleutel(sleutel);
|
||||
if (!eenheden || !decimalen.has(assetId) || !accWaarde.has(accountId)) continue;
|
||||
const koers = koersOp(prijzen.get(assetId), dag);
|
||||
if (koers != null) accWaarde.set(accountId, accWaarde.get(accountId) + (eenheden / decimalen.get(assetId)) * koers);
|
||||
accBezit.set(accountId, accBezit.get(accountId) + 1);
|
||||
}
|
||||
for (const id of accounts) {
|
||||
if (accBezit.get(id) && eersteDagPerAccount.get(id) === null) eersteDagPerAccount.set(id, dagNr);
|
||||
acc.reeks.waarde.get(id).push(accWaarde.get(id));
|
||||
acc.reeks.bezit.get(id).push(accBezit.get(id));
|
||||
for (const soort of Object.keys(acc.nu)) acc.reeks[soort].get(id).push(acc.nu[soort].get(id));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dagen, waarde, ingelegd, uitgehaald, perAsset, inlegPerAsset, uitPerAsset, ankersPerAsset, rentePerAsset,
|
||||
gerealiseerdPerAsset, onbekendPerAsset, nietVerklaardPerAsset,
|
||||
koersPerAsset, eenhedenPerAsset, eersteDagPerAsset,
|
||||
aankopen: oplopend(aankoopDagen),
|
||||
verkopen: oplopend(verkoopDagen),
|
||||
perAccount: acc.reeks.waarde, inlegPerAccount: acc.reeks.inleg, uitPerAccount: acc.reeks.uit,
|
||||
ankersPerAccount: acc.reeks.ankers, rentePerAccount: acc.reeks.rente, nietVerklaardPerAccount: acc.reeks.nietVerklaard,
|
||||
bezitPerAccount: acc.reeks.bezit, eersteDagPerAccount,
|
||||
standNu: [...stand].filter(([s, n]) => n && decimalen.has(assetVanPaar.get(s)))
|
||||
.map(([s, n]) => ({ accountId: accountVanSleutel(s), assetId: assetVanPaar.get(s), eenheden: n,
|
||||
deler: decimalen.get(assetVanPaar.get(s)) })),
|
||||
};
|
||||
}
|
||||
|
||||
export function accountSeries(reeks = {}, accountId = null) {
|
||||
return {
|
||||
waarden: reeks.perAccount?.get(accountId) || [],
|
||||
inleg: reeks.inlegPerAccount?.get(accountId) || [],
|
||||
uit: reeks.uitPerAccount?.get(accountId) || [],
|
||||
ankers: reeks.ankersPerAccount?.get(accountId) || [],
|
||||
rente: reeks.rentePerAccount?.get(accountId) || [],
|
||||
eenheden: reeks.bezitPerAccount?.get(accountId),
|
||||
eersteDag: reeks.eersteDagPerAccount?.get(accountId) ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function inlegVan(e, deler) {
|
||||
const kosten = Number(e.fiatFee) || 0;
|
||||
if (e.type === 'buy') return ((Number(e.amount) || 0) / deler) * (Number(e.fiatPrice) || 0) + kosten;
|
||||
if (e.type === 'sell') return -(((Number(e.amount) || 0) / deler) * (Number(e.fiatPrice) || 0)) + kosten;
|
||||
if (e.type === 'transfer' && e.fromAccountId != null) return kosten;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function uitVan(e, deler) {
|
||||
if (e.type !== 'sell') return 0;
|
||||
return ((Number(e.amount) || 0) / deler) * (Number(e.fiatPrice) || 0) - (Number(e.fiatFee) || 0);
|
||||
}
|
||||
|
||||
export function metSpotVandaag(reeks, assets = [], spots = new Map(), vandaag = dagVan(nowUtcIso())) {
|
||||
const i = (reeks?.dagen?.length || 0) - 1;
|
||||
if (i < 0 || reeks.dagen[i] !== vandaag) return reeks;
|
||||
const decimalen = new Map(assets.map(a => [a.id, Math.pow(10, a.decimals)]));
|
||||
let totaal = 0;
|
||||
for (const [assetId, waarden] of reeks.perAsset) {
|
||||
const spot = Number(spots.get(assetId));
|
||||
const eenheden = reeks.eenhedenPerAsset?.get(assetId);
|
||||
if (Number.isFinite(spot) && spot > 0 && eenheden && decimalen.has(assetId)) {
|
||||
reeks.koersPerAsset.get(assetId)[i] = spot;
|
||||
waarden[i] = (eenheden[i] / decimalen.get(assetId)) * spot;
|
||||
}
|
||||
totaal += waarden[i] || 0;
|
||||
}
|
||||
reeks.waarde[i] = totaal;
|
||||
if (reeks.perAccount?.size) {
|
||||
const perAccount = new Map([...reeks.perAccount.keys()].map(id => [id, 0]));
|
||||
for (const { accountId, assetId, eenheden, deler } of (reeks.standNu || [])) {
|
||||
const koers = reeks.koersPerAsset?.get(assetId)?.[i];
|
||||
if (koers != null && deler && perAccount.has(accountId)) {
|
||||
perAccount.set(accountId, perAccount.get(accountId) + (eenheden / deler) * koers);
|
||||
}
|
||||
}
|
||||
for (const [id, waarde] of perAccount) reeks.perAccount.get(id)[i] = waarde;
|
||||
}
|
||||
return reeks;
|
||||
}
|
||||
|
||||
export function jaarVensters(dagen = []) {
|
||||
const uit = new Map();
|
||||
dagen.forEach((dag, i) => {
|
||||
const jaar = Number(String(dag).slice(0, 4));
|
||||
if (!Number.isFinite(jaar)) return;
|
||||
const v = uit.get(jaar);
|
||||
if (v) v.tot = i;
|
||||
else uit.set(jaar, { van: i, tot: i });
|
||||
});
|
||||
return uit;
|
||||
}
|
||||
|
||||
export function meetVenster({ van, tot } = {}, lengte = Infinity) {
|
||||
return { van, tot: Math.min((Number(tot) || 0) + 1, lengte - 1) };
|
||||
}
|
||||
|
||||
export function windowDelta(r, { van = 0, tot = 0 } = {}) {
|
||||
if (!Array.isArray(r) || !r.length) return 0;
|
||||
const eind = Number(r[Math.min(tot, r.length - 1)]) || 0;
|
||||
return eind - (van > 0 ? (Number(r[van]) || 0) : 0);
|
||||
}
|
||||
|
||||
export function windowResult({ waarden = [], inleg = [], ankers = [], rente = [] } = {}, venster = {}) {
|
||||
return windowDelta(waarden, venster) - windowDelta(inleg, venster)
|
||||
- windowDelta(ankers, venster) + windowDelta(rente, venster);
|
||||
}
|
||||
|
||||
export function dailyResults({ waarden = [], inleg = [], ankers = [], rente = [] } = {}) {
|
||||
const step = (r, i) => (Number(r[i]) || 0) - (i > 0 ? (Number(r[i - 1]) || 0) : 0);
|
||||
return waarden.map((_, i) => step(waarden, i) - step(inleg, i) - step(ankers, i) + step(rente, i));
|
||||
}
|
||||
|
||||
export function todayResults(reeks = {}, vandaag = dagVan(nowUtcIso())) {
|
||||
const i = (reeks.dagen?.length || 0) - 1;
|
||||
if (i < 0 || reeks.dagen[i] !== vandaag) return null;
|
||||
const last = input => dailyResults(input)[i];
|
||||
const perAsset = new Map();
|
||||
for (const id of (reeks.perAsset?.keys?.() || [])) perAsset.set(id, last(seriesFor(reeks, id)));
|
||||
return { total: last(seriesFor(reeks)), perAsset };
|
||||
}
|
||||
|
||||
export function seriesFor(reeks = {}, assetId = null) {
|
||||
if (assetId == null) {
|
||||
return {
|
||||
waarden: reeks.waarde || [], inleg: reeks.ingelegd || [], uit: reeks.uitgehaald || [],
|
||||
ankers: sumPerDay(reeks.ankersPerAsset), rente: sumPerDay(reeks.rentePerAsset), eersteDag: 0,
|
||||
};
|
||||
}
|
||||
if (Array.isArray(assetId)) {
|
||||
const only = map => new Map([...(map || new Map())].filter(([id]) => assetId.includes(id)));
|
||||
const firsts = assetId.map(id => reeks.eersteDagPerAsset?.get(id)).filter(d => d != null);
|
||||
return {
|
||||
waarden: sumPerDay(only(reeks.perAsset)), inleg: sumPerDay(only(reeks.inlegPerAsset)),
|
||||
uit: sumPerDay(only(reeks.uitPerAsset)), ankers: sumPerDay(only(reeks.ankersPerAsset)),
|
||||
rente: sumPerDay(only(reeks.rentePerAsset)), eersteDag: firsts.length ? Math.min(...firsts) : 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
waarden: reeks.perAsset?.get(assetId) || [],
|
||||
inleg: reeks.inlegPerAsset?.get(assetId) || [],
|
||||
uit: reeks.uitPerAsset?.get(assetId) || [],
|
||||
ankers: reeks.ankersPerAsset?.get(assetId) || [],
|
||||
rente: reeks.rentePerAsset?.get(assetId) || [],
|
||||
eenheden: reeks.eenhedenPerAsset?.get(assetId),
|
||||
eersteDag: reeks.eersteDagPerAsset?.get(assetId) ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function sumPerDay(perAsset) {
|
||||
const out = [];
|
||||
for (const r of (perAsset?.values?.() || [])) {
|
||||
r.forEach((v, i) => { out[i] = (out[i] || 0) + v; });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function rollingWindows(length = 0, days = CONFIG.WEALTH_RETURN.ROLLING_DAYS) {
|
||||
const last = length - 1;
|
||||
if (last < 0) return [];
|
||||
return [
|
||||
...days.map(n => ({ days: n, venster: last - n >= 1 ? { van: last - n, tot: last } : null })),
|
||||
{ days: null, venster: { van: 0, tot: last } },
|
||||
];
|
||||
}
|
||||
|
||||
export function rollingReturns(input = {}, days = CONFIG.WEALTH_RETURN.ROLLING_DAYS) {
|
||||
return rollingWindows((input.waarden || []).length, days).map(({ days: n, venster }) => ({
|
||||
days: n,
|
||||
venster,
|
||||
amount: venster ? windowResult(input, venster) : null,
|
||||
pct: venster ? windowTwr(input, venster) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
export function vroegsteDag(events, ankers) {
|
||||
const momenten = [...events.map(e => e.occurredAt), ...ankers.map(v => v.rateAt)]
|
||||
.filter(Boolean).map(dagVan).sort();
|
||||
return momenten.length ? momenten[0] : null;
|
||||
}
|
||||
|
||||
export function managedCostPots({ events = [], prijzen = new Map(), managed = new Set(), totIso = null } = {}) {
|
||||
const uit = new Map();
|
||||
if (!managed || !managed.size) return uit;
|
||||
const pot = (accountId, assetId) => {
|
||||
if (accountId == null || !managed.has(accountId)) return null;
|
||||
const sleutel = `${accountId}|${assetId}`;
|
||||
if (!uit.has(sleutel)) uit.set(sleutel, { buy_amount: 0, buy_spot_raw: 0, buy_fee_total: 0, buy_events: 0, gift_amount: 0 });
|
||||
return uit.get(sleutel);
|
||||
};
|
||||
const volgorde = events.filter(e => !totIso || String(e.occurredAt) <= totIso)
|
||||
.sort((a, b) => String(a.occurredAt).localeCompare(String(b.occurredAt)));
|
||||
for (const e of volgorde) {
|
||||
const n = Number(e.amount) || 0;
|
||||
const geen = e.priceOrigin === 'none';
|
||||
if (e.type === 'buy') {
|
||||
const p = pot(e.toAccountId, e.assetId);
|
||||
if (!p) continue;
|
||||
if (geen) { p.gift_amount += n; continue; }
|
||||
p.buy_amount += n;
|
||||
p.buy_spot_raw += n * (Number(e.fiatPrice) || 0);
|
||||
p.buy_fee_total += Number(e.fiatFee) || 0;
|
||||
p.buy_events += 1;
|
||||
} else if (e.type === 'sell') {
|
||||
const p = pot(e.fromAccountId, e.assetId);
|
||||
if (p && geen) p.gift_amount -= n;
|
||||
} else if (e.type === 'transfer') {
|
||||
const van = e.fromAccountId ?? null;
|
||||
const naar = e.toAccountId ?? null;
|
||||
const p = pot(naar, e.assetId);
|
||||
if (p && van != null && van !== naar) {
|
||||
const koers = koersOp(prijzen.get(e.assetId), dagVan(e.occurredAt));
|
||||
if (koers != null) { p.buy_amount += n; p.buy_spot_raw += n * koers; }
|
||||
} else if (p && van == null && geen) {
|
||||
p.gift_amount += n;
|
||||
}
|
||||
const q = naar == null ? pot(van, e.assetId) : null;
|
||||
if (q && geen) q.gift_amount -= n;
|
||||
}
|
||||
}
|
||||
return uit;
|
||||
}
|
||||
|
||||
export async function dagreeksVanNu({ vanDag = null, totDag = null, excludedAccounts = null, managedAccounts = null } = {}) {
|
||||
const [events, valuations, assets] = await Promise.all([listEvents(), listValuations(), listAssets()]);
|
||||
|
||||
const prijzen = new Map();
|
||||
const rijenPerAsset = new Map();
|
||||
for (const a of assets) {
|
||||
let rijen = [];
|
||||
try { rijen = await listPriceHistory(a.id); } catch { rijen = []; }
|
||||
rijenPerAsset.set(a.id, rijen);
|
||||
try {
|
||||
prijzen.set(a.id, toDailyGrid(rijen));
|
||||
} catch {
|
||||
prijzen.set(a.id, []);
|
||||
}
|
||||
}
|
||||
const spots = new Map();
|
||||
for (const a of assets) {
|
||||
try { const price = await liveSpotPriceFor(a); if (price != null) spots.set(a.id, price); }
|
||||
catch { }
|
||||
}
|
||||
const reeks = metSpotVandaag(dagreeks({ events, valuations, assets, prijzen, vanDag, totDag, excludedAccounts }), assets, spots);
|
||||
reeks.rijenPerAsset = rijenPerAsset;
|
||||
if (managedAccounts && managedAccounts.size) {
|
||||
reeks.beheerdeKostprijs = totIso => managedCostPots({ events, prijzen, managed: managedAccounts, totIso });
|
||||
}
|
||||
return reeks;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Generated, do not edit. sha256:2b462247335f4413d5ae4038e57da8d4d3e70b4fb676fc84dcbba876e90e1c18
|
||||
import { loadCharting, readPalette } from '../ui/chart-setup.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { fmtBase } from '../ui/format.js';
|
||||
|
||||
let tipEl = null;
|
||||
const verberg = () => tipEl?.classList.remove('is-visible');
|
||||
|
||||
function tip({ chart, tooltip }, rijen) {
|
||||
if (!tipEl) {
|
||||
tipEl = document.createElement('div');
|
||||
tipEl.className = 'ring-tip';
|
||||
document.body.appendChild(tipEl);
|
||||
window.addEventListener('scroll', verberg, { capture: true, passive: true });
|
||||
}
|
||||
if (!tooltip.opacity || !tooltip.dataPoints?.length) { verberg(); return; }
|
||||
const r = rijen[tooltip.dataPoints[0].dataIndex];
|
||||
const regel = (label, bedrag) => {
|
||||
const rij = document.createElement('span');
|
||||
rij.className = 'ring-tip-row';
|
||||
const naam = document.createElement('span');
|
||||
naam.className = 'ring-tip-pct';
|
||||
naam.textContent = label;
|
||||
const val = document.createElement('span');
|
||||
val.className = 'val';
|
||||
val.textContent = fmtBase(bedrag || 0);
|
||||
rij.append(naam, val);
|
||||
return rij;
|
||||
};
|
||||
const kop = document.createElement('span');
|
||||
kop.className = 'ring-tip-label';
|
||||
kop.textContent = r.label;
|
||||
tipEl.replaceChildren(kop,
|
||||
regel(t('wealth.valueEnd'), r.waarde),
|
||||
regel(t('wealth.investedNet'), r.inleg),
|
||||
regel(t('wealth.result'), r.resultaat));
|
||||
const box = chart.canvas.getBoundingClientRect();
|
||||
tipEl.style.left = `${Math.round(box.left + tooltip.caretX)}px`;
|
||||
tipEl.style.top = `${Math.round(box.top + tooltip.caretY)}px`;
|
||||
tipEl.classList.add('is-visible');
|
||||
}
|
||||
|
||||
export async function tekenJaarGrafiek(canvas, rijen) {
|
||||
const Chart = await loadCharting();
|
||||
const pal = readPalette();
|
||||
const resultaat = rijen.map(r => r.resultaat || 0);
|
||||
const grafiek = new Chart(canvas, {
|
||||
data: {
|
||||
labels: rijen.map(r => r.label),
|
||||
datasets: [
|
||||
{ type: 'bar', label: t('wealth.investedNet'), data: rijen.map(r => r.inleg || 0),
|
||||
backgroundColor: pal.accentFaint, order: 2, yAxisID: 'y' },
|
||||
{ type: 'bar', label: t('wealth.result'), data: resultaat,
|
||||
backgroundColor: resultaat.map(v => (v >= 0 ? pal.sigGreen : pal.sigRed)), order: 2, yAxisID: 'y' },
|
||||
{ type: 'line', label: t('wealth.valueEnd'), data: rijen.map(r => r.waarde || 0),
|
||||
borderColor: pal.accent, backgroundColor: pal.accent, borderWidth: 2, pointRadius: 3,
|
||||
cubicInterpolationMode: 'monotone', order: 1, yAxisID: 'waarde' },
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { color: pal.textSec } },
|
||||
y: { ticks: { display: false }, grid: { color: pal.grid }, border: { display: false } },
|
||||
waarde: { position: 'right', display: false, beginAtZero: true },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: true, position: 'bottom', labels: { color: pal.textSec, boxWidth: 10, boxHeight: 10 } },
|
||||
tooltip: { enabled: false, external: ctx => tip(ctx, rijen) },
|
||||
},
|
||||
},
|
||||
});
|
||||
canvas.addEventListener('mouseleave', verberg);
|
||||
const destroy = grafiek.destroy.bind(grafiek);
|
||||
grafiek.destroy = () => { verberg(); destroy(); };
|
||||
return grafiek;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Generated, do not edit. sha256:e2b7a86933c03dfaed3d23288997f18fd48f414cd445220f5c23fdf7ceb2cc5e
|
||||
export const ALL_YEARS = 'alles';
|
||||
|
||||
export function choiceToText(k) {
|
||||
if (k === ALL_YEARS) return ALL_YEARS;
|
||||
return k.van === k.tot ? String(k.van) : `${k.van}-${k.tot}`;
|
||||
}
|
||||
|
||||
export function choiceFromText(tekst) {
|
||||
if (tekst === ALL_YEARS) return ALL_YEARS;
|
||||
const m = /^(\d{4})(?:-(\d{4}))?$/.exec(String(tekst ?? ''));
|
||||
if (!m) { const nu = new Date().getFullYear(); return { van: nu, tot: nu }; }
|
||||
const a = Number(m[1]);
|
||||
const b = m[2] ? Number(m[2]) : a;
|
||||
return { van: Math.min(a, b), tot: Math.max(a, b) };
|
||||
}
|
||||
|
||||
export function withoutYear(k, jaar) {
|
||||
if (k === ALL_YEARS || !Number.isFinite(jaar)) return k;
|
||||
if (jaar >= k.tot) return ALL_YEARS;
|
||||
return { van: jaar + 1, tot: k.tot };
|
||||
}
|
||||
|
||||
export function yearsIn(k) {
|
||||
if (k === ALL_YEARS) return [];
|
||||
return Array.from({ length: k.tot - k.van + 1 }, (_, i) => k.van + i);
|
||||
}
|
||||
Reference in new issue
Block a user