151 lines
5.6 KiB
JavaScript
151 lines
5.6 KiB
JavaScript
// 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;
|
|
}
|