V5-Modell: Einkommen nominal, Ausgaben real->nominal, Inflation plan-weit, Sparquoten-Grafik
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:
2026-07-15 22:20:06 +02:00
parent 310ebc66fb
commit 6402583e0c
7 changed files with 212 additions and 80 deletions
+38 -25
View File
@@ -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);