diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index a362623..664a5ea 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from "react"; import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { BarChart3, Download, LineChart as LineChartIcon } from "lucide-react"; import { WealthChart, type TimelineSeries } from "@/components/WealthChart"; +import { SparquoteChart } from "@/components/SparquoteChart"; import { api } from "@/lib/api-client"; import { formatChf } from "@/lib/format"; import type { PlanComputed } from "@/lib/calculations"; @@ -97,6 +98,18 @@ export function Dashboard({ +
+

+ + Einkommen vs. Ausgaben pro Jahr +

+

+ 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. +

+ +
+

diff --git a/src/components/ElementDetail.tsx b/src/components/ElementDetail.tsx index 4147f1e..935015e 100644 --- a/src/components/ElementDetail.tsx +++ b/src/components/ElementDetail.tsx @@ -24,7 +24,8 @@ export interface CellContext { carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben 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 { @@ -136,28 +137,42 @@ export function ElementPhaseFields({ case "INCOME": case "EXPENSE": { const isIncome = element.category === "INCOME"; - const teuerung = ( - 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)"; + // Basiswert (erstes Jahr). Ab Phase 2 mit dem fortgeschriebenen Wert der Vorphase + // vorbelegt, aber bewusst aenderbar (Teilzeit, Befoerderung, Jobwechsel …). 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 ( <> +

+ {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."} +

setP({ amount: v })} /> - {teuerung} + + setP({ teuerungsausgleich: v })} + /> ); } diff --git a/src/components/PhaseDetail.tsx b/src/components/PhaseDetail.tsx index e5f2d40..dbdffa4 100644 --- a/src/components/PhaseDetail.tsx +++ b/src/components/PhaseDetail.tsx @@ -10,20 +10,17 @@ export function PhaseDetail({ phase, maxDurationYears, isLast, - inflationDefault, onSaved, onDeleted, }: { phase: PhaseInput; maxDurationYears: number | null; isLast: boolean; - inflationDefault: number; onSaved: () => void; onDeleted: () => void; }) { const [name, setName] = useState(phase.name); const [durationYears, setDurationYears] = useState(phase.durationYears); - const [inflationRate, setInflationRate] = useState(phase.inflationRate); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); @@ -33,7 +30,7 @@ export function PhaseDetail({ setSaving(true); setError(null); try { - await api.put(`/api/phases/${phase.id}`, { name, durationYears, inflationRate }); + await api.put(`/api/phases/${phase.id}`, { name, durationYears }); onSaved(); } catch (e) { setError(e instanceof Error ? e.message : "Speichern fehlgeschlagen."); @@ -69,7 +66,7 @@ export function PhaseDetail({ )}

-
+
setDurationYears(cap != null ? Math.min(v, cap) : v)} /> -
+

Die Inflationsrate gilt plan-weit und wird in den Plan-Einstellungen gesetzt.

{error &&

{error}

}
diff --git a/src/components/PlanView.tsx b/src/components/PlanView.tsx index 3cde570..b94784c 100644 --- a/src/components/PlanView.tsx +++ b/src/components/PlanView.tsx @@ -176,6 +176,7 @@ export function PlanView({ carried: ce?.carried ?? false, derivedStart: ce?.baseValue ?? 0, phaseInflation: phaseInflationFor(phase.id), + deflatorStart: phase.cumulativeInflationStart, }; } @@ -192,6 +193,7 @@ export function PlanView({ carried: ce?.carried ?? false, derivedStart: 0, phaseInflation: phaseInflationFor(fromPhase.id), + deflatorStart: fromPhase.cumulativeInflationStart, }; } @@ -617,7 +619,6 @@ export function PlanView({ phase={phaseInput} maxDurationYears={phase.maxDurationYears} isLast={isLast} - inflationDefault={plan.inflationRateDefault} onSaved={onChanged} onDeleted={() => { setSelected(null); @@ -681,8 +682,9 @@ function phaseCellContent( ): React.ReactNode { if (!ce) return "–"; if (ce.note) return ce.note; + const isFlow = ce.category === "INCOME" || ce.category === "EXPENSE"; 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)) { return ( @@ -711,7 +713,8 @@ function PhaseHeader({ }) { const quotaLabel = phase.isConsumption ? "Verzehr" : "Quote"; 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 ( ))} -
Einkommen {valStr(phase.incomeStart, dS, mode)} → {valStr(phase.incomeEnd, dE, mode)}
-
Ausgaben {valStr(phase.expenseStart, dS, mode)} → {valStr(phase.expenseEnd, dE, mode)}
+
Einkommen {valStr(phase.incomeStart, dS, mode)} → {valStr(phase.incomeEnd, dF, mode)}
+
Ausgaben {valStr(phase.expenseStart, dS, mode)} → {valStr(phase.expenseEnd, dF, mode)}
- {quotaLabel} {valStr(phase.quotaStart, dS, mode)} → {valStr(phase.quotaEnd, dE, mode)} + {quotaLabel} {valStr(phase.quotaStart, dS, mode)} → {valStr(phase.quotaEnd, dF, mode)}
Cash {valStr(phase.cashStart, dS, mode)} → {valStr(phase.cashEnd, dE, mode)} @@ -842,7 +845,8 @@ function AddElementDialog({ carriedEndValue: 0, carried: false, derivedStart: 0, - phaseInflation: plan.phases.find((p) => p.id === firstPhase.id)?.inflationRate ?? plan.inflationRateDefault, + phaseInflation: plan.inflationRateDefault, + deflatorStart: firstPhase.cumulativeInflationStart, }; async function create() { diff --git a/src/components/SparquoteChart.tsx b/src/components/SparquoteChart.tsx new file mode 100644 index 0000000..d54b23b --- /dev/null +++ b/src/components/SparquoteChart.tsx @@ -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

Noch keine Phasen vorhanden.

; + + 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 ( +
+ + + + `J. ${v}`} /> + Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)} + /> + [typeof v === "number" ? formatChf(v) : v, name]} + labelFormatter={(v) => `Jahr ${v}`} + /> + + + + + + + + +
+ ); +} diff --git a/src/lib/calculations.test.ts b/src/lib/calculations.test.ts index efde012..0fc6aaf 100644 --- a/src/lib/calculations.test.ts +++ b/src/lib/calculations.test.ts @@ -39,48 +39,48 @@ function plan(opts: { const within = (actual: number, expected: number, pct: number) => Math.abs(actual - expected) <= Math.abs(expected) * pct; -describe("V4 Golden Tests", () => { - it("Test 1 – Ansparphase, Flows wachsen (Cash 0%)", () => { +// V5-Modell: Einkommen = nominale Basis + nominale Lohnerhoehung; Ausgaben = REALE Basis + +// 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({ age: 40, retirementAge: 60, phases: [{ id: "p1", durationYears: 15 }], elements: [ 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 } }), ], }); - const r = computePlan(p); - const ph = r.phases[0]; + const ph = computePlan(p).phases[0]; expect(within(ph.endWealthNominal, 761654, 0.01)).toBe(true); expect(within(ph.endWealthReal, 565928, 0.01)).toBe(true); 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({ age: 65, retirementAge: 65, phases: [{ id: "p1", durationYears: 35 }], elements: [ 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 } }), ], }); - const r = computePlan(p); - expect(r.ruinAge).toBe(94); + expect(computePlan(p).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({ age: 40, retirementAge: 60, phases: [{ id: "p1", durationYears: 3 }], elements: [ 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 } }), ], }); @@ -89,14 +89,14 @@ describe("V4 Golden Tests", () => { 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({ age: 40, retirementAge: 60, phases: [{ id: "p1", durationYears: 3 }], elements: [ 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 } }), ], }); @@ -105,23 +105,19 @@ describe("V4 Golden Tests", () => { 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({ age: 40, retirementAge: 60, - 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 } }), - ], + phases: [{ id: "p1", durationYears: 5 }], + elements: [el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } })], }); const ph = computePlan(p).phases[0]; - expect(ph.cashStart).toBe(50000); - expect(ph.cashEnd).toBe(50000); // Quote 0, keine Raten -> Cash unveraendert + expect(ph.expenseStart).toBe(100000); // Jahr 1 nominal = real + 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({ age: 40, retirementAge: 60, @@ -133,7 +129,24 @@ describe("V4 Golden Tests", () => { 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({ age: 40, retirementAge: 70, @@ -143,7 +156,7 @@ describe("V4 Golden Tests", () => { ], elements: [ 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); diff --git a/src/lib/calculations.ts b/src/lib/calculations.ts index f155cf9..2dd1d85 100644 --- a/src/lib/calculations.ts +++ b/src/lib/calculations.ts @@ -62,13 +62,24 @@ export interface PhaseComputed { elements: ElementPhaseComputed[]; startWealthNominal: number; // inkl. Cash endWealthNominal: number; // inkl. Cash - cumulativeInflationStart: number; // Kaufkraft-Deflator zu Phasenbeginn (Startwerte) - cumulativeInflationEnd: number; // Kaufkraft-Deflator am Phasenende (Endwerte) + cumulativeInflationStart: number; // Kaufkraft-Deflator zu Phasenbeginn (Bestandswerte) + cumulativeInflationEnd: number; // Kaufkraft-Deflator am Phasenende (Bestandswerte) + flowDeflatorEnd: number; // Deflator fuer den Flow-Endwert (Jahr `duration`) 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 { phases: PhaseComputed[]; + yearly: YearPoint[]; nachlass: number; 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()); const result: PhaseComputed[] = []; + const yearly: YearPoint[] = []; let yearsBefore = 0; let cumulativeInflation = 1; let cashCarryIn = Math.round(plan.initialCash || 0); @@ -133,7 +145,9 @@ export function computePlan(plan: PlanInput): PlanComputed { const nextPhase = phases[i + 1]; const isFirstPhase = i === 0; 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 ra = retirementAge.get(p.id)!; @@ -231,9 +245,11 @@ export function computePlan(plan: PlanInput): PlanComputed { switch (e.category) { case "INCOME": case "EXPENSE": { - const idx = num(pd.teuerungsausgleich, phaseInflation); - // Basiswert: Phase 1 = eingegebener Betrag; ab Phase 2 = fortgeschriebener Endwert - // der Vorphase, ausser der Nutzer hat ihn bewusst geaendert (pd.amount gesetzt). + // Einkommen: Basis + Rate sind NOMINAL (Lohnerhoehung). Ausgaben: Basis + Rate sind + // REAL (heutige Kaufkraft; reale Mehrausgaben) -- die Inflation kommt separat dazu. + // 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)); const basis = !carry.hasCarry ? Math.round(num(pd.amount)) @@ -241,6 +257,8 @@ export function computePlan(plan: PlanInput): PlanComputed { ? Math.round(pd.amount) : ec.baseValue; (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; } case "AHV": { @@ -347,20 +365,32 @@ export function computePlan(plan: PlanInput): PlanComputed { let quotaEnd = 0; for (let t = 1; t <= duration; t++) { + // Einkommen: nominal (Basis x (1+Lohnerhoehung)^(t-1)) + Renten (nominal fix). let incomeFlow = renteTotal; for (const inc of incomes) incomeFlow += inc.basis * Math.pow(1 + inc.idx / 100, t - 1); - let expenseFlow = 0; - for (const exp of expenses) expenseFlow += exp.basis * Math.pow(1 + exp.idx / 100, t - 1); - const quote = incomeFlow - expenseFlow; + // Ausgaben: real (Basis x (1+reale Mehrausgabe)^(t-1)); nominal = real x kumul. Inflation. + const inflFactor = cumInflStart * Math.pow(1 + infl / 100, t - 1); + 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) { incomeStart = incomeFlow; - expenseStart = expenseFlow; + expenseStart = expenseNominal; quotaStart = quote; } if (t === duration) { incomeEnd = incomeFlow; - expenseEnd = expenseFlow; + expenseEnd = expenseNominal; quotaEnd = quote; } @@ -376,15 +406,19 @@ export function computePlan(plan: PlanInput): PlanComputed { 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) { inc.ec.startValue = Math.round(inc.basis); inc.ec.endValue = Math.round(inc.basis * Math.pow(1 + inc.idx / 100, duration - 1)); inc.ec.summary = fmt(inc.ec.startValue); } for (const exp of expenses) { - exp.ec.startValue = Math.round(exp.basis); - exp.ec.endValue = Math.round(exp.basis * Math.pow(1 + exp.idx / 100, duration - 1)); + exp.ec.startValue = Math.round(exp.basis * cumInflStart); + exp.ec.endValue = Math.round(exp.basis * Math.pow(1 + exp.idx / 100, duration - 1) * flowDeflatorEnd); exp.ec.summary = fmt(exp.ec.startValue); } for (const a of assets) { @@ -409,8 +443,7 @@ export function computePlan(plan: PlanInput): PlanComputed { const cashEnd = Math.round(cash); const startWealthNominal = Math.round(wealthStart + cashStart); const endWealthNominal = Math.round(wealthEnd + cashEnd); - const cumulativeInflationStart = cumulativeInflation; - cumulativeInflation = cumulativeInflation * Math.pow(1 + phaseInflation / 100, duration); + cumulativeInflation = cumInflStart * Math.pow(1 + infl / 100, duration); result.push({ id: phase.id, @@ -435,8 +468,9 @@ export function computePlan(plan: PlanInput): PlanComputed { elements: orderedElements.map((e) => ecById.get(e.id)!), startWealthNominal, endWealthNominal, - cumulativeInflationStart, + cumulativeInflationStart: cumInflStart, cumulativeInflationEnd: cumulativeInflation, + flowDeflatorEnd, endWealthReal: endWealthNominal / cumulativeInflation, }); @@ -452,10 +486,8 @@ export function computePlan(plan: PlanInput): PlanComputed { !!owner && !!nextPhase && workingByPerson.get(owner.id) === true && 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") { - const idx = num(pd.teuerungsausgleich, phaseInflation); - carry.flowBasis = ec.startValue * Math.pow(1 + idx / 100, duration); carry.hasCarry = true; continue; } @@ -547,7 +579,7 @@ export function computePlan(plan: PlanInput): PlanComputed { } const nachlass = result.length > 0 ? result[result.length - 1].endWealthNominal : 0; - return { phases: result, nachlass, ruinAge }; + return { phases: result, yearly, nachlass, ruinAge }; } function retiresInPhase(