Versie 0.2.0

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

No files matched your search

+538
View File
@@ -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;
}
}