// 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; } }