V5-Modell: Einkommen nominal, Ausgaben real->nominal, Inflation plan-weit, Sparquoten-Grafik
Deploy App / deploy (push) Successful in 57s
Deploy App / deploy (push) Successful in 57s
- Einkommen: nominale Basis + nominale Lohnerhoehung (Default 0). Real read-only als Info. - Ausgaben: REALE Basis (heutige Kaufkraft) + reale Mehrausgaben (Default 0); nominal = real x plan-weite Inflation, read-only. Loest die "nominale Ausgaben verwirren"-Problematik. - Inflation nur noch plan-weit (Phasen-Override entfernt). - Engine liefert flowDeflatorEnd (Flow-Realwerte) + yearly-Serie (Jahr/Alter/Einkommen/ Ausgabe nominal/real). Nominal/Real-Umschalter nutzt Flow- vs. Bestands-Deflatoren. - Neue Grafik (Dashboard): Einkommen vs. nominale Ausgabe mit eingefaerbter Sparquoten- Flaeche (gruen/rot) + reale Ausgabe als Referenzlinie (Inflationskeil). - In-Tool-Erklaerungen in den Einkommen/Ausgaben-Popups. - Golden Tests aufs neue Modell hergeleitet (8 gruen, u.a. Ausgaben real->nominal, Ruin 94). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
|
|||||||
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||||
import { BarChart3, Download, LineChart as LineChartIcon } from "lucide-react";
|
import { BarChart3, Download, LineChart as LineChartIcon } from "lucide-react";
|
||||||
import { WealthChart, type TimelineSeries } from "@/components/WealthChart";
|
import { WealthChart, type TimelineSeries } from "@/components/WealthChart";
|
||||||
|
import { SparquoteChart } from "@/components/SparquoteChart";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
import { formatChf } from "@/lib/format";
|
import { formatChf } from "@/lib/format";
|
||||||
import type { PlanComputed } from "@/lib/calculations";
|
import type { PlanComputed } from "@/lib/calculations";
|
||||||
@@ -97,6 +98,18 @@ export function Dashboard({
|
|||||||
<StatCard label="Geschaetzter Nachlass" value={computed.nachlass} help="Endvermoegen der letzten Phase - potenziell vererbbar." />
|
<StatCard label="Geschaetzter Nachlass" value={computed.nachlass} help="Endvermoegen der letzten Phase - potenziell vererbbar." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section className="rounded-xl border border-border bg-surface p-4 shadow-sm">
|
||||||
|
<h3 className="mb-1 flex items-center gap-1.5 text-sm font-semibold text-fg">
|
||||||
|
<LineChartIcon className="h-4 w-4 text-accent" />
|
||||||
|
Einkommen vs. Ausgaben pro Jahr
|
||||||
|
</h3>
|
||||||
|
<p className="mb-3 text-xs text-muted">
|
||||||
|
Die Flaeche zwischen Einkommen und nominalen Ausgaben ist die Spar- (gruen) bzw. Verzehrquote (rot).
|
||||||
|
Die blasse Linie sind die realen Ausgaben – der Abstand zur nominalen Linie ist der Inflationsanteil.
|
||||||
|
</p>
|
||||||
|
<SparquoteChart computed={computed} />
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="rounded-xl border border-border bg-surface p-4 shadow-sm">
|
<section className="rounded-xl border border-border bg-surface p-4 shadow-sm">
|
||||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
<h3 className="flex items-center gap-1.5 text-sm font-semibold text-fg">
|
<h3 className="flex items-center gap-1.5 text-sm font-semibold text-fg">
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export interface CellContext {
|
|||||||
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima
|
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima
|
||||||
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
|
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
|
||||||
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
|
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
|
||||||
phaseInflation: number; // Default fuer Teuerungsausgleich
|
phaseInflation: number; // Plan-Inflationsrate (Info)
|
||||||
|
deflatorStart: number; // Kaufkraft-Deflator zu Phasenbeginn (real <-> nominal, erstes Jahr)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -136,28 +137,42 @@ export function ElementPhaseFields({
|
|||||||
case "INCOME":
|
case "INCOME":
|
||||||
case "EXPENSE": {
|
case "EXPENSE": {
|
||||||
const isIncome = element.category === "INCOME";
|
const isIncome = element.category === "INCOME";
|
||||||
const teuerung = (
|
// Basiswert (erstes Jahr). Ab Phase 2 mit dem fortgeschriebenen Wert der Vorphase
|
||||||
<NumberField
|
// vorbelegt, aber bewusst aenderbar (Teilzeit, Befoerderung, Jobwechsel …).
|
||||||
label="Teuerungsausgleich (%/Jahr)"
|
|
||||||
help="Indexiert den Betrag ueber die Phasenjahre (Jahr t = Basis x (1+Satz)^(t-1)). Default = Phaseninflation. 0% = nominal flach (real sinkend)."
|
|
||||||
step={0.1}
|
|
||||||
value={num(pd.teuerungsausgleich, context.phaseInflation)}
|
|
||||||
onChange={(v) => setP({ teuerungsausgleich: v })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
// Basiswert (erstes Jahr der Phase). Ab Phase 2 mit dem fortgeschriebenen Endwert der
|
|
||||||
// Vorphase vorbelegt, aber bewusst aenderbar (Teilzeit, Beförderung, Jobwechsel …).
|
|
||||||
const baseLabel = isIncome ? "Jahreseinkommen (CHF, erstes Jahr)" : "Jahresausgaben (CHF, erstes Jahr)";
|
|
||||||
const baseValue = typeof pd.amount === "number" ? pd.amount : carried ? context.derivedStart : 0;
|
const baseValue = typeof pd.amount === "number" ? pd.amount : carried ? context.derivedStart : 0;
|
||||||
|
const rate = num(pd.teuerungsausgleich, 0);
|
||||||
|
const d = context.deflatorStart || 1;
|
||||||
|
// Info-Gegenwert im ersten Jahr: Einkommen -> real; Ausgaben -> nominal.
|
||||||
|
const otherValue = isIncome ? Math.round(baseValue / d) : Math.round(baseValue * d);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<p className="col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||||
|
{isIncome
|
||||||
|
? "Einkommen wird NOMINAL erfasst (die Zahl auf dem Lohnausweis). Der reale Wert (heutige Kaufkraft) wird nur zur Info angezeigt."
|
||||||
|
: "Ausgaben werden REAL erfasst (in heutiger Kaufkraft). Die Inflation (plan-weit) rechnet daraus automatisch die nominalen Ausgaben – nur zur Info."}
|
||||||
|
</p>
|
||||||
<MoneyField
|
<MoneyField
|
||||||
label={baseLabel}
|
label={isIncome ? "Jahreseinkommen NOMINAL (erstes Jahr)" : "Jahresausgaben REAL (erstes Jahr, heutige Kaufkraft)"}
|
||||||
help={carried ? "Vorbelegt mit dem fortgeschriebenen Wert der Vorphase; hier bewusst aenderbar." : undefined}
|
help={carried ? "Vorbelegt mit dem fortgeschriebenen Wert der Vorphase; hier bewusst aenderbar." : undefined}
|
||||||
value={baseValue}
|
value={baseValue}
|
||||||
onChange={(v) => setP({ amount: v })}
|
onChange={(v) => setP({ amount: v })}
|
||||||
/>
|
/>
|
||||||
{teuerung}
|
<DerivedField
|
||||||
|
label={isIncome ? "≈ real (heutige Kaufkraft)" : "≈ nominal (in diesem Jahr)"}
|
||||||
|
value={otherValue}
|
||||||
|
help="Nur zur Info, wird automatisch berechnet."
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
label={isIncome ? "Nominale Lohnerhoehung (%/Jahr)" : "Reale Mehrausgaben (%/Jahr)"}
|
||||||
|
help={
|
||||||
|
isIncome
|
||||||
|
? "Geschaetzte jaehrliche Lohnerhoehung. 0% = Lohn bleibt nominal gleich und verliert real an Kaufkraft."
|
||||||
|
: "Zusaetzliche reale Mehrausgaben pro Jahr (z. B. weil man sich mehr goennt). Die Inflation kommt separat automatisch dazu. 0% = gleicher Lebensstandard."
|
||||||
|
}
|
||||||
|
step={0.1}
|
||||||
|
value={rate}
|
||||||
|
onChange={(v) => setP({ teuerungsausgleich: v })}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,17 @@ export function PhaseDetail({
|
|||||||
phase,
|
phase,
|
||||||
maxDurationYears,
|
maxDurationYears,
|
||||||
isLast,
|
isLast,
|
||||||
inflationDefault,
|
|
||||||
onSaved,
|
onSaved,
|
||||||
onDeleted,
|
onDeleted,
|
||||||
}: {
|
}: {
|
||||||
phase: PhaseInput;
|
phase: PhaseInput;
|
||||||
maxDurationYears: number | null;
|
maxDurationYears: number | null;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
inflationDefault: number;
|
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
onDeleted: () => void;
|
onDeleted: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [name, setName] = useState(phase.name);
|
const [name, setName] = useState(phase.name);
|
||||||
const [durationYears, setDurationYears] = useState(phase.durationYears);
|
const [durationYears, setDurationYears] = useState(phase.durationYears);
|
||||||
const [inflationRate, setInflationRate] = useState<number | null>(phase.inflationRate);
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -33,7 +30,7 @@ export function PhaseDetail({
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await api.put(`/api/phases/${phase.id}`, { name, durationYears, inflationRate });
|
await api.put(`/api/phases/${phase.id}`, { name, durationYears });
|
||||||
onSaved();
|
onSaved();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : "Speichern fehlgeschlagen.");
|
setError(e instanceof Error ? e.message : "Speichern fehlgeschlagen.");
|
||||||
@@ -69,7 +66,7 @@ export function PhaseDetail({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<TextField label="Bezeichnung" value={name} onChange={setName} />
|
<TextField label="Bezeichnung" value={name} onChange={setName} />
|
||||||
<NumberField
|
<NumberField
|
||||||
label={`Dauer (Jahre)${cap != null ? ` · max. ${cap}` : ""}`}
|
label={`Dauer (Jahre)${cap != null ? ` · max. ${cap}` : ""}`}
|
||||||
@@ -79,14 +76,8 @@ export function PhaseDetail({
|
|||||||
max={cap ?? undefined}
|
max={cap ?? undefined}
|
||||||
onChange={(v) => setDurationYears(cap != null ? Math.min(v, cap) : v)}
|
onChange={(v) => setDurationYears(cap != null ? Math.min(v, cap) : v)}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
|
||||||
label="Inflationsrate (%)"
|
|
||||||
help="Ueberschreibt die Standardannahme aus dem Plan-Grundprofil."
|
|
||||||
value={inflationRate ?? inflationDefault}
|
|
||||||
step={0.1}
|
|
||||||
onChange={setInflationRate}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-faint">Die Inflationsrate gilt plan-weit und wird in den Plan-Einstellungen gesetzt.</p>
|
||||||
|
|
||||||
{error && <p className="text-sm text-danger">{error}</p>}
|
{error && <p className="text-sm text-danger">{error}</p>}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ export function PlanView({
|
|||||||
carried: ce?.carried ?? false,
|
carried: ce?.carried ?? false,
|
||||||
derivedStart: ce?.baseValue ?? 0,
|
derivedStart: ce?.baseValue ?? 0,
|
||||||
phaseInflation: phaseInflationFor(phase.id),
|
phaseInflation: phaseInflationFor(phase.id),
|
||||||
|
deflatorStart: phase.cumulativeInflationStart,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +193,7 @@ export function PlanView({
|
|||||||
carried: ce?.carried ?? false,
|
carried: ce?.carried ?? false,
|
||||||
derivedStart: 0,
|
derivedStart: 0,
|
||||||
phaseInflation: phaseInflationFor(fromPhase.id),
|
phaseInflation: phaseInflationFor(fromPhase.id),
|
||||||
|
deflatorStart: fromPhase.cumulativeInflationStart,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,7 +619,6 @@ export function PlanView({
|
|||||||
phase={phaseInput}
|
phase={phaseInput}
|
||||||
maxDurationYears={phase.maxDurationYears}
|
maxDurationYears={phase.maxDurationYears}
|
||||||
isLast={isLast}
|
isLast={isLast}
|
||||||
inflationDefault={plan.inflationRateDefault}
|
|
||||||
onSaved={onChanged}
|
onSaved={onChanged}
|
||||||
onDeleted={() => {
|
onDeleted={() => {
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
@@ -681,8 +682,9 @@ function phaseCellContent(
|
|||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
if (!ce) return "–";
|
if (!ce) return "–";
|
||||||
if (ce.note) return ce.note;
|
if (ce.note) return ce.note;
|
||||||
|
const isFlow = ce.category === "INCOME" || ce.category === "EXPENSE";
|
||||||
const dS = phase.cumulativeInflationStart;
|
const dS = phase.cumulativeInflationStart;
|
||||||
const dE = phase.cumulativeInflationEnd;
|
const dE = isFlow ? phase.flowDeflatorEnd : phase.cumulativeInflationEnd;
|
||||||
if (START_END_CATEGORIES.includes(ce.category) && (ce.startValue !== 0 || ce.endValue !== 0)) {
|
if (START_END_CATEGORIES.includes(ce.category) && (ce.startValue !== 0 || ce.endValue !== 0)) {
|
||||||
return (
|
return (
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
@@ -711,7 +713,8 @@ function PhaseHeader({
|
|||||||
}) {
|
}) {
|
||||||
const quotaLabel = phase.isConsumption ? "Verzehr" : "Quote";
|
const quotaLabel = phase.isConsumption ? "Verzehr" : "Quote";
|
||||||
const dS = phase.cumulativeInflationStart;
|
const dS = phase.cumulativeInflationStart;
|
||||||
const dE = phase.cumulativeInflationEnd;
|
const dE = phase.cumulativeInflationEnd; // Bestandswerte (Cash, Vermoegen)
|
||||||
|
const dF = phase.flowDeflatorEnd; // Flow-Werte (Einkommen, Ausgaben, Quote)
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -739,10 +742,10 @@ function PhaseHeader({
|
|||||||
{personLabel(p.role)} {p.startAge} → {p.endAge}
|
{personLabel(p.role)} {p.startAge} → {p.endAge}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div>Einkommen {valStr(phase.incomeStart, dS, mode)} → {valStr(phase.incomeEnd, dE, mode)}</div>
|
<div>Einkommen {valStr(phase.incomeStart, dS, mode)} → {valStr(phase.incomeEnd, dF, mode)}</div>
|
||||||
<div>Ausgaben {valStr(phase.expenseStart, dS, mode)} → {valStr(phase.expenseEnd, dE, mode)}</div>
|
<div>Ausgaben {valStr(phase.expenseStart, dS, mode)} → {valStr(phase.expenseEnd, dF, mode)}</div>
|
||||||
<div>
|
<div>
|
||||||
{quotaLabel} {valStr(phase.quotaStart, dS, mode)} → {valStr(phase.quotaEnd, dE, mode)}
|
{quotaLabel} {valStr(phase.quotaStart, dS, mode)} → {valStr(phase.quotaEnd, dF, mode)}
|
||||||
</div>
|
</div>
|
||||||
<div className={phase.cashNegative ? "text-danger font-medium" : ""}>
|
<div className={phase.cashNegative ? "text-danger font-medium" : ""}>
|
||||||
Cash {valStr(phase.cashStart, dS, mode)} → {valStr(phase.cashEnd, dE, mode)}
|
Cash {valStr(phase.cashStart, dS, mode)} → {valStr(phase.cashEnd, dE, mode)}
|
||||||
@@ -842,7 +845,8 @@ function AddElementDialog({
|
|||||||
carriedEndValue: 0,
|
carriedEndValue: 0,
|
||||||
carried: false,
|
carried: false,
|
||||||
derivedStart: 0,
|
derivedStart: 0,
|
||||||
phaseInflation: plan.phases.find((p) => p.id === firstPhase.id)?.inflationRate ?? plan.inflationRateDefault,
|
phaseInflation: plan.inflationRateDefault,
|
||||||
|
deflatorStart: firstPhase.cumulativeInflationStart,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Area,
|
||||||
|
CartesianGrid,
|
||||||
|
ComposedChart,
|
||||||
|
Legend,
|
||||||
|
Line,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
import { formatChf } from "@/lib/format";
|
||||||
|
import type { PlanComputed } from "@/lib/calculations";
|
||||||
|
|
||||||
|
const INCOME_COLOR = "#16a34a";
|
||||||
|
const EXPENSE_COLOR = "#dc2626";
|
||||||
|
const REAL_COLOR = "#9ca3af";
|
||||||
|
|
||||||
|
// Verlauf pro Jahr: Einkommen (nominal, inkl. Renten) vs. nominale Ausgaben; die Flaeche
|
||||||
|
// dazwischen ist die Spar-/Verzehrquote (gruen = Sparen, rot = Verzehr). Reale Ausgaben als
|
||||||
|
// blasse Referenzlinie -> der Keil zu den nominalen Ausgaben ist "das, was die Inflation frisst".
|
||||||
|
export function SparquoteChart({ computed }: { computed: PlanComputed }) {
|
||||||
|
if (computed.yearly.length === 0) return <p className="text-sm text-muted">Noch keine Phasen vorhanden.</p>;
|
||||||
|
|
||||||
|
const data = computed.yearly.map((y) => {
|
||||||
|
const surplus = y.income >= y.expenseNominal;
|
||||||
|
return {
|
||||||
|
year: y.year,
|
||||||
|
income: y.income,
|
||||||
|
expenseNominal: y.expenseNominal,
|
||||||
|
expenseReal: y.expenseReal,
|
||||||
|
// Range-Flaechen [unten, oben]; nur dort gesetzt, wo zutreffend (sonst Luecke).
|
||||||
|
sparBand: surplus ? [y.expenseNominal, y.income] : undefined,
|
||||||
|
verzehrBand: surplus ? undefined : [y.income, y.expenseNominal],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-80 w-full">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<ComposedChart data={data} margin={{ top: 8, right: 16, left: 8, bottom: 8 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
|
||||||
|
<XAxis dataKey="year" tick={{ fontSize: 11 }} tickFormatter={(v) => `J. ${v}`} />
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 11 }}
|
||||||
|
tickFormatter={(v) => Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(v, name) => [typeof v === "number" ? formatChf(v) : v, name]}
|
||||||
|
labelFormatter={(v) => `Jahr ${v}`}
|
||||||
|
/>
|
||||||
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||||
|
<Area dataKey="sparBand" name="Sparquote" stroke="none" fill={INCOME_COLOR} fillOpacity={0.18} connectNulls={false} isAnimationActive={false} legendType="none" />
|
||||||
|
<Area dataKey="verzehrBand" name="Verzehr" stroke="none" fill={EXPENSE_COLOR} fillOpacity={0.18} connectNulls={false} isAnimationActive={false} legendType="none" />
|
||||||
|
<Line dataKey="income" name="Einkommen (nominal)" stroke={INCOME_COLOR} strokeWidth={2} dot={false} isAnimationActive={false} />
|
||||||
|
<Line dataKey="expenseNominal" name="Ausgaben (nominal)" stroke={EXPENSE_COLOR} strokeWidth={2} dot={false} isAnimationActive={false} />
|
||||||
|
<Line dataKey="expenseReal" name="Ausgaben (real)" stroke={REAL_COLOR} strokeWidth={1.5} strokeDasharray="5 3" dot={false} isAnimationActive={false} />
|
||||||
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -39,48 +39,48 @@ function plan(opts: {
|
|||||||
|
|
||||||
const within = (actual: number, expected: number, pct: number) => Math.abs(actual - expected) <= Math.abs(expected) * pct;
|
const within = (actual: number, expected: number, pct: number) => Math.abs(actual - expected) <= Math.abs(expected) * pct;
|
||||||
|
|
||||||
describe("V4 Golden Tests", () => {
|
// V5-Modell: Einkommen = nominale Basis + nominale Lohnerhoehung; Ausgaben = REALE Basis +
|
||||||
it("Test 1 – Ansparphase, Flows wachsen (Cash 0%)", () => {
|
// reale Mehrausgaben, nominal = real x (plan-weite Inflation).
|
||||||
|
describe("V5 Golden Tests", () => {
|
||||||
|
it("Test 1 – Ansparen (Einkommen +2% nominal, Ausgaben real flach -> nominal +2%)", () => {
|
||||||
const p = plan({
|
const p = plan({
|
||||||
age: 40,
|
age: 40,
|
||||||
retirementAge: 60,
|
retirementAge: 60,
|
||||||
phases: [{ id: "p1", durationYears: 15 }],
|
phases: [{ id: "p1", durationYears: 15 }],
|
||||||
elements: [
|
elements: [
|
||||||
el("INCOME", "PERSON_A", { p1: { amount: 120000, teuerungsausgleich: 2 } }),
|
el("INCOME", "PERSON_A", { p1: { amount: 120000, teuerungsausgleich: 2 } }),
|
||||||
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 2 } }),
|
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
||||||
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 200000, expectedReturn: 5, annualContribution: 0 } }),
|
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 200000, expectedReturn: 5, annualContribution: 0 } }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const r = computePlan(p);
|
const ph = computePlan(p).phases[0];
|
||||||
const ph = r.phases[0];
|
|
||||||
expect(within(ph.endWealthNominal, 761654, 0.01)).toBe(true);
|
expect(within(ph.endWealthNominal, 761654, 0.01)).toBe(true);
|
||||||
expect(within(ph.endWealthReal, 565928, 0.01)).toBe(true);
|
expect(within(ph.endWealthReal, 565928, 0.01)).toBe(true);
|
||||||
expect(ph.cashNegative).toBe(false);
|
expect(ph.cashNegative).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Test 2 – Verzehr/Ruin: Gesamtvermoegen kippt, Ruin Alter 94", () => {
|
it("Test 2 – Verzehr/Ruin: Ausgaben nominal +2%, Rente fix -> Ruin Alter 94", () => {
|
||||||
const p = plan({
|
const p = plan({
|
||||||
age: 65,
|
age: 65,
|
||||||
retirementAge: 65,
|
retirementAge: 65,
|
||||||
phases: [{ id: "p1", durationYears: 35 }],
|
phases: [{ id: "p1", durationYears: 35 }],
|
||||||
elements: [
|
elements: [
|
||||||
el("INCOME", "PERSON_A", { p1: { amount: 60000, teuerungsausgleich: 0 } }), // Rente nominal fix
|
el("INCOME", "PERSON_A", { p1: { amount: 60000, teuerungsausgleich: 0 } }), // Rente nominal fix
|
||||||
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 2 } }),
|
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
||||||
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 900000, expectedReturn: 3, annualContribution: 0 } }),
|
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 900000, expectedReturn: 3, annualContribution: 0 } }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const r = computePlan(p);
|
expect(computePlan(p).ruinAge).toBe(94);
|
||||||
expect(r.ruinAge).toBe(94);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Test 3 – Cash-Ausgleich, Rate 6'364 (unter Anfangsquote), Cash nie negativ", () => {
|
it("Test 3 – Cash-Ausgleich, Rate 6'364, Cash nie negativ", () => {
|
||||||
const p = plan({
|
const p = plan({
|
||||||
age: 40,
|
age: 40,
|
||||||
retirementAge: 60,
|
retirementAge: 60,
|
||||||
phases: [{ id: "p1", durationYears: 3 }],
|
phases: [{ id: "p1", durationYears: 3 }],
|
||||||
elements: [
|
elements: [
|
||||||
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
||||||
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 90000, teuerungsausgleich: 2 } }),
|
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 90000, teuerungsausgleich: 0 } }),
|
||||||
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 0, expectedReturn: 0, annualContribution: 6364 } }),
|
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 0, expectedReturn: 0, annualContribution: 6364 } }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -89,14 +89,14 @@ describe("V4 Golden Tests", () => {
|
|||||||
expect(ph.cashNegative).toBe(false);
|
expect(ph.cashNegative).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Test 4 – Cash-Ausgleich, Rate 10'000 (ueber Endquote), Cash wird negativ", () => {
|
it("Test 4 – Cash-Ausgleich, Rate 10'000, Cash wird negativ", () => {
|
||||||
const p = plan({
|
const p = plan({
|
||||||
age: 40,
|
age: 40,
|
||||||
retirementAge: 60,
|
retirementAge: 60,
|
||||||
phases: [{ id: "p1", durationYears: 3 }],
|
phases: [{ id: "p1", durationYears: 3 }],
|
||||||
elements: [
|
elements: [
|
||||||
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
||||||
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 90000, teuerungsausgleich: 2 } }),
|
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 90000, teuerungsausgleich: 0 } }),
|
||||||
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 0, expectedReturn: 0, annualContribution: 10000 } }),
|
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 0, expectedReturn: 0, annualContribution: 10000 } }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -105,23 +105,19 @@ describe("V4 Golden Tests", () => {
|
|||||||
expect(ph.cashNegative).toBe(true);
|
expect(ph.cashNegative).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Cash-Anfangswert fliesst in die erste Phase ein", () => {
|
it("Ausgaben real -> nominal: reale Basis 100'000, 2% Inflation, 5 J.", () => {
|
||||||
const p = plan({
|
const p = plan({
|
||||||
age: 40,
|
age: 40,
|
||||||
retirementAge: 60,
|
retirementAge: 60,
|
||||||
initialCash: 50000,
|
phases: [{ id: "p1", durationYears: 5 }],
|
||||||
phases: [{ id: "p1", durationYears: 3 }],
|
elements: [el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } })],
|
||||||
elements: [
|
|
||||||
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
|
||||||
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const ph = computePlan(p).phases[0];
|
const ph = computePlan(p).phases[0];
|
||||||
expect(ph.cashStart).toBe(50000);
|
expect(ph.expenseStart).toBe(100000); // Jahr 1 nominal = real
|
||||||
expect(ph.cashEnd).toBe(50000); // Quote 0, keine Raten -> Cash unveraendert
|
expect(ph.expenseEnd).toBe(Math.round(100000 * Math.pow(1.02, 4))); // 108'243
|
||||||
});
|
});
|
||||||
|
|
||||||
it("indexRate 0 -> Einkommen bleibt nominal flach", () => {
|
it("Einkommen nominal flach bei Lohnerhoehung 0%", () => {
|
||||||
const p = plan({
|
const p = plan({
|
||||||
age: 40,
|
age: 40,
|
||||||
retirementAge: 60,
|
retirementAge: 60,
|
||||||
@@ -133,7 +129,24 @@ describe("V4 Golden Tests", () => {
|
|||||||
expect(ph.incomeEnd).toBe(80000);
|
expect(ph.incomeEnd).toBe(80000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Phasen-Fortschreibung: indexierter Endwert Phase 1 = Startwert Phase 2; Cash laeuft fort", () => {
|
it("Cash-Anfangswert fliesst in die erste Phase ein", () => {
|
||||||
|
const p = plan({
|
||||||
|
age: 40,
|
||||||
|
retirementAge: 60,
|
||||||
|
inflation: 0,
|
||||||
|
initialCash: 50000,
|
||||||
|
phases: [{ id: "p1", durationYears: 3 }],
|
||||||
|
elements: [
|
||||||
|
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
||||||
|
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const ph = computePlan(p).phases[0];
|
||||||
|
expect(ph.cashStart).toBe(50000);
|
||||||
|
expect(ph.cashEnd).toBe(50000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fortschreibung: nominaler Einkommens-Basiswert Phase 1 -> Startwert Phase 2; Cash laeuft fort", () => {
|
||||||
const p = plan({
|
const p = plan({
|
||||||
age: 40,
|
age: 40,
|
||||||
retirementAge: 70,
|
retirementAge: 70,
|
||||||
@@ -143,7 +156,7 @@ describe("V4 Golden Tests", () => {
|
|||||||
],
|
],
|
||||||
elements: [
|
elements: [
|
||||||
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 2 }, p2: {} }),
|
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 2 }, p2: {} }),
|
||||||
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 80000, teuerungsausgleich: 2 }, p2: {} }),
|
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 80000, teuerungsausgleich: 0 }, p2: {} }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const r = computePlan(p);
|
const r = computePlan(p);
|
||||||
|
|||||||
+53
-21
@@ -62,13 +62,24 @@ export interface PhaseComputed {
|
|||||||
elements: ElementPhaseComputed[];
|
elements: ElementPhaseComputed[];
|
||||||
startWealthNominal: number; // inkl. Cash
|
startWealthNominal: number; // inkl. Cash
|
||||||
endWealthNominal: number; // inkl. Cash
|
endWealthNominal: number; // inkl. Cash
|
||||||
cumulativeInflationStart: number; // Kaufkraft-Deflator zu Phasenbeginn (Startwerte)
|
cumulativeInflationStart: number; // Kaufkraft-Deflator zu Phasenbeginn (Bestandswerte)
|
||||||
cumulativeInflationEnd: number; // Kaufkraft-Deflator am Phasenende (Endwerte)
|
cumulativeInflationEnd: number; // Kaufkraft-Deflator am Phasenende (Bestandswerte)
|
||||||
|
flowDeflatorEnd: number; // Deflator fuer den Flow-Endwert (Jahr `duration`)
|
||||||
endWealthReal: number;
|
endWealthReal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ein Datenpunkt pro Jahr (ueber alle Phasen), fuer die Verlaufsgrafik.
|
||||||
|
export interface YearPoint {
|
||||||
|
year: number; // 1-basiert ab Planbeginn
|
||||||
|
age: number; // Alter Person A am Jahresende
|
||||||
|
income: number; // Einkommen inkl. Renten (nominal)
|
||||||
|
expenseNominal: number;
|
||||||
|
expenseReal: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlanComputed {
|
export interface PlanComputed {
|
||||||
phases: PhaseComputed[];
|
phases: PhaseComputed[];
|
||||||
|
yearly: YearPoint[];
|
||||||
nachlass: number;
|
nachlass: number;
|
||||||
ruinAge: number | null; // Alter (Person A), in dem das Gesamtvermoegen (inkl. Cash) erstmals < 0 faellt
|
ruinAge: number | null; // Alter (Person A), in dem das Gesamtvermoegen (inkl. Cash) erstmals < 0 faellt
|
||||||
}
|
}
|
||||||
@@ -123,6 +134,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
for (const e of plan.elements) carries.set(e.id, emptyCarry());
|
for (const e of plan.elements) carries.set(e.id, emptyCarry());
|
||||||
|
|
||||||
const result: PhaseComputed[] = [];
|
const result: PhaseComputed[] = [];
|
||||||
|
const yearly: YearPoint[] = [];
|
||||||
let yearsBefore = 0;
|
let yearsBefore = 0;
|
||||||
let cumulativeInflation = 1;
|
let cumulativeInflation = 1;
|
||||||
let cashCarryIn = Math.round(plan.initialCash || 0);
|
let cashCarryIn = Math.round(plan.initialCash || 0);
|
||||||
@@ -133,7 +145,9 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
const nextPhase = phases[i + 1];
|
const nextPhase = phases[i + 1];
|
||||||
const isFirstPhase = i === 0;
|
const isFirstPhase = i === 0;
|
||||||
const duration = Math.max(1, phase.durationYears);
|
const duration = Math.max(1, phase.durationYears);
|
||||||
const phaseInflation = phase.inflationRate ?? plan.inflationRateDefault;
|
// Inflation ist plan-weit (V5): keine Phasen-Ueberschreibung mehr.
|
||||||
|
const infl = plan.inflationRateDefault;
|
||||||
|
const cumInflStart = cumulativeInflation; // Kaufkraft-Deflator zu Phasenbeginn
|
||||||
|
|
||||||
const personInfos: PersonPhaseInfo[] = persons.map((p) => {
|
const personInfos: PersonPhaseInfo[] = persons.map((p) => {
|
||||||
const ra = retirementAge.get(p.id)!;
|
const ra = retirementAge.get(p.id)!;
|
||||||
@@ -231,9 +245,11 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
switch (e.category) {
|
switch (e.category) {
|
||||||
case "INCOME":
|
case "INCOME":
|
||||||
case "EXPENSE": {
|
case "EXPENSE": {
|
||||||
const idx = num(pd.teuerungsausgleich, phaseInflation);
|
// Einkommen: Basis + Rate sind NOMINAL (Lohnerhoehung). Ausgaben: Basis + Rate sind
|
||||||
// Basiswert: Phase 1 = eingegebener Betrag; ab Phase 2 = fortgeschriebener Endwert
|
// REAL (heutige Kaufkraft; reale Mehrausgaben) -- die Inflation kommt separat dazu.
|
||||||
// der Vorphase, ausser der Nutzer hat ihn bewusst geaendert (pd.amount gesetzt).
|
// Rate-Default = 0 %. Basiswert ab Phase 2 = fortgeschriebener Wert der Vorphase
|
||||||
|
// (nominal fuer Einkommen, real fuer Ausgaben), ausser bewusst geaendert (pd.amount).
|
||||||
|
const idx = num(pd.teuerungsausgleich, 0);
|
||||||
ec.baseValue = carry.hasCarry ? Math.round(carry.flowBasis) : Math.round(num(pd.amount));
|
ec.baseValue = carry.hasCarry ? Math.round(carry.flowBasis) : Math.round(num(pd.amount));
|
||||||
const basis = !carry.hasCarry
|
const basis = !carry.hasCarry
|
||||||
? Math.round(num(pd.amount))
|
? Math.round(num(pd.amount))
|
||||||
@@ -241,6 +257,8 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
? Math.round(pd.amount)
|
? Math.round(pd.amount)
|
||||||
: ec.baseValue;
|
: ec.baseValue;
|
||||||
(e.category === "INCOME" ? incomes : expenses).push({ basis, idx, ec });
|
(e.category === "INCOME" ? incomes : expenses).push({ basis, idx, ec });
|
||||||
|
// Basiswert der Folgephase fortschreiben (nominal fuer Einkommen, real fuer Ausgaben).
|
||||||
|
carry.flowBasis = basis * Math.pow(1 + idx / 100, duration);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "AHV": {
|
case "AHV": {
|
||||||
@@ -347,20 +365,32 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
let quotaEnd = 0;
|
let quotaEnd = 0;
|
||||||
|
|
||||||
for (let t = 1; t <= duration; t++) {
|
for (let t = 1; t <= duration; t++) {
|
||||||
|
// Einkommen: nominal (Basis x (1+Lohnerhoehung)^(t-1)) + Renten (nominal fix).
|
||||||
let incomeFlow = renteTotal;
|
let incomeFlow = renteTotal;
|
||||||
for (const inc of incomes) incomeFlow += inc.basis * Math.pow(1 + inc.idx / 100, t - 1);
|
for (const inc of incomes) incomeFlow += inc.basis * Math.pow(1 + inc.idx / 100, t - 1);
|
||||||
let expenseFlow = 0;
|
// Ausgaben: real (Basis x (1+reale Mehrausgabe)^(t-1)); nominal = real x kumul. Inflation.
|
||||||
for (const exp of expenses) expenseFlow += exp.basis * Math.pow(1 + exp.idx / 100, t - 1);
|
const inflFactor = cumInflStart * Math.pow(1 + infl / 100, t - 1);
|
||||||
const quote = incomeFlow - expenseFlow;
|
let expenseReal = 0;
|
||||||
|
for (const exp of expenses) expenseReal += exp.basis * Math.pow(1 + exp.idx / 100, t - 1);
|
||||||
|
const expenseNominal = expenseReal * inflFactor;
|
||||||
|
const quote = incomeFlow - expenseNominal;
|
||||||
|
|
||||||
|
yearly.push({
|
||||||
|
year: yearsBefore + t,
|
||||||
|
age: personA.age + yearsBefore + t,
|
||||||
|
income: Math.round(incomeFlow),
|
||||||
|
expenseNominal: Math.round(expenseNominal),
|
||||||
|
expenseReal: Math.round(expenseReal),
|
||||||
|
});
|
||||||
|
|
||||||
if (t === 1) {
|
if (t === 1) {
|
||||||
incomeStart = incomeFlow;
|
incomeStart = incomeFlow;
|
||||||
expenseStart = expenseFlow;
|
expenseStart = expenseNominal;
|
||||||
quotaStart = quote;
|
quotaStart = quote;
|
||||||
}
|
}
|
||||||
if (t === duration) {
|
if (t === duration) {
|
||||||
incomeEnd = incomeFlow;
|
incomeEnd = incomeFlow;
|
||||||
expenseEnd = expenseFlow;
|
expenseEnd = expenseNominal;
|
||||||
quotaEnd = quote;
|
quotaEnd = quote;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,15 +406,19 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
if (ruinAge === null && total < 0) ruinAge = personA.age + yearsBefore + t;
|
if (ruinAge === null && total < 0) ruinAge = personA.age + yearsBefore + t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endwerte je Element setzen + Endvermoegen bilden.
|
// Flow-Deflator fuer den Endwert (Jahr `duration`): eine Kaufkraft-Stufe weniger als der
|
||||||
|
// Bestands-Deflator am Phasenende.
|
||||||
|
const flowDeflatorEnd = cumInflStart * Math.pow(1 + infl / 100, duration - 1);
|
||||||
|
|
||||||
|
// Endwerte je Element setzen (Einkommen/Ausgaben nominal; Ausgaben-Nominal = real x Infl.).
|
||||||
for (const inc of incomes) {
|
for (const inc of incomes) {
|
||||||
inc.ec.startValue = Math.round(inc.basis);
|
inc.ec.startValue = Math.round(inc.basis);
|
||||||
inc.ec.endValue = Math.round(inc.basis * Math.pow(1 + inc.idx / 100, duration - 1));
|
inc.ec.endValue = Math.round(inc.basis * Math.pow(1 + inc.idx / 100, duration - 1));
|
||||||
inc.ec.summary = fmt(inc.ec.startValue);
|
inc.ec.summary = fmt(inc.ec.startValue);
|
||||||
}
|
}
|
||||||
for (const exp of expenses) {
|
for (const exp of expenses) {
|
||||||
exp.ec.startValue = Math.round(exp.basis);
|
exp.ec.startValue = Math.round(exp.basis * cumInflStart);
|
||||||
exp.ec.endValue = Math.round(exp.basis * Math.pow(1 + exp.idx / 100, duration - 1));
|
exp.ec.endValue = Math.round(exp.basis * Math.pow(1 + exp.idx / 100, duration - 1) * flowDeflatorEnd);
|
||||||
exp.ec.summary = fmt(exp.ec.startValue);
|
exp.ec.summary = fmt(exp.ec.startValue);
|
||||||
}
|
}
|
||||||
for (const a of assets) {
|
for (const a of assets) {
|
||||||
@@ -409,8 +443,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
const cashEnd = Math.round(cash);
|
const cashEnd = Math.round(cash);
|
||||||
const startWealthNominal = Math.round(wealthStart + cashStart);
|
const startWealthNominal = Math.round(wealthStart + cashStart);
|
||||||
const endWealthNominal = Math.round(wealthEnd + cashEnd);
|
const endWealthNominal = Math.round(wealthEnd + cashEnd);
|
||||||
const cumulativeInflationStart = cumulativeInflation;
|
cumulativeInflation = cumInflStart * Math.pow(1 + infl / 100, duration);
|
||||||
cumulativeInflation = cumulativeInflation * Math.pow(1 + phaseInflation / 100, duration);
|
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
id: phase.id,
|
id: phase.id,
|
||||||
@@ -435,8 +468,9 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
elements: orderedElements.map((e) => ecById.get(e.id)!),
|
elements: orderedElements.map((e) => ecById.get(e.id)!),
|
||||||
startWealthNominal,
|
startWealthNominal,
|
||||||
endWealthNominal,
|
endWealthNominal,
|
||||||
cumulativeInflationStart,
|
cumulativeInflationStart: cumInflStart,
|
||||||
cumulativeInflationEnd: cumulativeInflation,
|
cumulativeInflationEnd: cumulativeInflation,
|
||||||
|
flowDeflatorEnd,
|
||||||
endWealthReal: endWealthNominal / cumulativeInflation,
|
endWealthReal: endWealthNominal / cumulativeInflation,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -452,10 +486,8 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
!!owner && !!nextPhase && workingByPerson.get(owner.id) === true &&
|
!!owner && !!nextPhase && workingByPerson.get(owner.id) === true &&
|
||||||
retiresInPhase(owner.id, persons, retirementAge, yearsBefore + duration);
|
retiresInPhase(owner.id, persons, retirementAge, yearsBefore + duration);
|
||||||
|
|
||||||
// Einkommen/Ausgaben: indexierten Basiswert fortschreiben.
|
// Einkommen/Ausgaben: Basiswert wurde bereits im Element-Setup fortgeschrieben.
|
||||||
if (e.category === "INCOME" || e.category === "EXPENSE") {
|
if (e.category === "INCOME" || e.category === "EXPENSE") {
|
||||||
const idx = num(pd.teuerungsausgleich, phaseInflation);
|
|
||||||
carry.flowBasis = ec.startValue * Math.pow(1 + idx / 100, duration);
|
|
||||||
carry.hasCarry = true;
|
carry.hasCarry = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -547,7 +579,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nachlass = result.length > 0 ? result[result.length - 1].endWealthNominal : 0;
|
const nachlass = result.length > 0 ? result[result.length - 1].endWealthNominal : 0;
|
||||||
return { phases: result, nachlass, ruinAge };
|
return { phases: result, yearly, nachlass, ruinAge };
|
||||||
}
|
}
|
||||||
|
|
||||||
function retiresInPhase(
|
function retiresInPhase(
|
||||||
|
|||||||
Reference in New Issue
Block a user