Monte-Carlo-Simulation (Roadmap Nr. 19, Stufe A)
Deploy App / deploy (push) Successful in 57s

Button in der Planansicht -> Dialog: Erklaerung, Eingaben, Lauf, Ergebnis + Faecher.
Statt einer festen Rendite/Inflation werden tausende Zufallspfade gerechnet und die
Erfolgs-/Ruinwahrscheinlichkeit des Plans ausgewiesen.

Kern (computePlan-Nahtstelle):
- computePlan(plan, sample?) nimmt optional pro Jahr Inflation und pro Element/Jahr
  eine Rendite. Ohne sample bitgenau wie bisher (durch Golden Tests abgesichert).
- Dazu Inflation auf ein kumulatives Deflator-Array umgestellt (statt (1+i)^t), damit
  sie pro Jahr variieren kann. Deterministisch identisch.
- Reine Funktion ohne Server-Deps -> Simulation laeuft komplett im Browser, null
  Serverlast. ~10'000 Laeufe in ~1 s, Fortschritt alle 500 Laeufe (kein Freeze).

Statistik (montecarlo.ts):
- Fettschwaenzig (standardisierte Student-t, nu=5): Extremcrashs realistisch haeufig,
  eine Normalverteilung wuerde sie stark unterschaetzen.
- Gemeinsamer Marktschock (rho=0.7): riskante Anlagen fallen im Crash zusammen,
  nicht gegeneinander.
- Boeden: 0 % fuer PK/3a, -100 % sonst. Seedbar (reproduzierbar).
- Zwei Renditezahlen: geplante (Zielbalken) vs. historische (Streu-Mittelpunkt) --
  sonst waere P(>= geplantes Endvermoegen) immer ~50 %.

UI: Erklaerung, pro Element/Inflation historischer Oe (Pflicht, kein Default) +
Streuungsstufe (recherchierte sigma-Werte) + Anzahl Laeufe. Ergebnis: Ruin-/
Erfolgswahrscheinlichkeit, Faecher (10/50/90) + deterministische Linie.

7 MC-Tests inkl. deterministischer Aequivalenz, Vol-Drag, Reproduzierbarkeit, Boden,
Ruin (41 -> 48). Keine DB-Aenderung. Spezifikation auf v0.7 (Kap. 4.12, 9.15).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 22:47:18 +02:00
parent 5bf35bd6da
commit 71137f7cea
6 changed files with 922 additions and 17 deletions
+129 -7
View File
@@ -4,10 +4,10 @@
| | |
|---|---|
| **Dokument** | Funktionale und Technische Spezifikation FPT |
| **Version** | 0.6 |
| **Version** | 0.7 |
| **Datum** | 2026-07-17 |
| **Status** | Lebendes Dokument |
| **Codestand** | Arbeitsstand nach `a97de5b` inkl. Teilverkauf (Sonstiges Vermögen) und Sonderamortisation (Immobilie) (Branch `main`) |
| **Codestand** | Arbeitsstand nach `5bf35bd` inkl. Monte-Carlo-Simulation (Stufe A) (Branch `main`) |
| **Ersetzt** | `FDD_TDD_FPT.docx` (v1v5) im Ordner `Info Dateien` diese sind ab Version 0.1 dieses Dokuments obsolet |
| **Geltungsbereich** | Gesamter Code im Verzeichnis `FPT` |
@@ -17,6 +17,7 @@
| Version | Datum | Autor | Änderung |
|---|---|---|---|
| 0.7 | 2026-07-17 | Claude (Opus 4.8) | **Monte-Carlo-Simulation** (Roadmap Nr. 19, Stufe A). Button in der Planansicht → Dialog mit Erklärung, Eingaben und Ergebnis. Statt einer festen Rendite/Inflation werden tausende Zufallspfade gerechnet; ausgewiesen werden **Ruinwahrscheinlichkeit**, **Erfolgswahrscheinlichkeit** (P(Endvermögen ≥ Zielbetrag)) und ein **Fächer** (10 %/Median/90 %) plus die deterministische Linie. Läuft komplett im Browser (`computePlan` ist rein). Modell: fettschwänzige Verteilung (Student-t, ν=5), gemeinsamer Marktschock (ρ=0.7), Böden 0 % für PK/3a und 100 % sonst. Pro renditetragendem Element und für die Inflation je: historischer Ø (Pflicht), Streuungsstufe, σ. Neue Datei `montecarlo.ts` + optionaler `sample`-Parameter in `computePlan` (Inflation neu als kumulatives Array; deterministisch identisch). Neues Kapitel 4.12; 7 MC-Tests + Refactor-Absicherung (41 → 48). Keine Verhaltensänderung, keine DB-Änderung. |
| 0.6 | 2026-07-17 | Claude (Opus 4.8) | **Teilverkauf** von Sonstigem Vermögen (Roadmap Nr. 42) und **Sonderamortisation** der Hypothek (Roadmap Nr. 15). `OTHER_ASSET` am Übergang neu: Halten / Verkaufen / **Teilverkauf** ein Betrag fliesst ins Cash (erscheint als „Kapitalzufluss" im Phasenkopf), der Rest bleibt investiert. `REAL_ESTATE` im Halten-Fall neu mit **Einmaltilgung** aus dem Cash (analog zur Sofort-Tilgung bei Schulden; erscheint als „Kapitalinvestition"). Damit lässt sich die indirekte Amortisation via 3a mechanisch nachbilden. Die eigentliche Roadmap Nr. 15 (Steuerwirkung der indirekten Amortisation) bleibt mit dem Steuer-Bündel 13/14/23 zurückgestellt siehe 9.14. Kapitel 4.9.3/4.9.4 ergänzt. Fünf Regressionstests (36 → 41). Keine Verhaltensänderung für bestehende Pläne. |
| 0.5 | 2026-07-17 | Claude (Opus 4.8) | **Netto/Brutto geklärt** (Roadmap Nr. 9, reduziert) und **Immobilien-Modul erweitert** (Roadmap Nr. 8). Einkommen ist neu explizit als **Nettolohn** definiert (Label und Hilfetext); für die AHV rechnet das Tool intern mit `AHV_GROSS_FROM_NET_FACTOR = 1.12` auf den Bruttolohn hoch die AHV bemisst sich am Brutto, die bisherige Netto-Basis unterschätzte die Rente um bis zu ~1'900/Jahr. Immobilie neu mit **Hypothekarzins** (% der Restschuld, sinkt mit der Amortisation, mit Doppelzählungs-Schalter) und **Wertsteigerung** (auf die **Liegenschaft**, nicht auf das Eigenkapital Hebeleffekt). Grundstückgewinnsteuer bemisst sich neu explizit am ursprünglichen Kaufpreis. Keine Aufschlüsselung bei Einkommen oder Ausgaben, keine Steuerschätzung (Begründung: 9.14). Neues Kapitel 4.4.5; 4.6.5 und 4.9.4 überarbeitet; Abschnitt 9 um zwei Punkte ergänzt. Sechs Regressionstests (30 → 36). **Verhaltensänderung:** siehe 9.13. |
| 0.4 | 2026-07-17 | Claude (Opus 4.8) | **AHV-Rente einkommensabhängig** (Roadmap Nr. 3) und **Fortschreibungs-Warnhinweis** (Roadmap Nr. 4). Die AHV-Rente folgt neu der amtlichen Rentenformel (Skala 44) über das massgebende durchschnittliche Jahreseinkommen statt pauschal der Maximalrente; verifiziert gegen die amtliche Tabelle 318.117.1 (51/51 Zeilen). Prüfung der Beitragskarriere am Pensions-Übergang, mit Zusatzfeldern für die Jahre vor Planbeginn (ab Alter 21). Alles **real** gerechnet. Neue Konstanten `AHV_MIN_MONTHLY_FULL`, `AHV_PENSION_MONTHS`, `AHV_CONTRIBUTION_START_AGE`; `AHV_MAX_ANNUAL_SINGLE` neu abgeleitet. Warnhinweis in Phasenzellen und Phasen-Detail, wenn Folgephasen existieren. Neue Kapitel 3.5.6, 4.4; Abschnitt 9 um zwei Punkte ergänzt. Zwölf Regressionstests (18 → 30). **Verhaltensänderung:** siehe 9.10. |
@@ -1418,6 +1419,106 @@ Die drei Default-Sätze werden **sowohl als UI-Vorschlag als auch in der Berechn
verwendet (`num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE)`). Grund laut Code-Kommentar: Damit
ein nicht angetippter Wert nicht fälschlich als 0 gerechnet wird.
## 4.12 Monte-Carlo-Simulation
Die deterministische Berechnung nimmt pro Anlage *eine* feste Rendite und *eine* feste Inflation
an. Real schwanken beide. Die Monte-Carlo-Simulation (Roadmap Nr. 19, Stufe A, `montecarlo.ts`)
würfelt viele tausend mögliche Verläufe und weist die **Erfolgswahrscheinlichkeit** des Plans aus.
### 4.12.1 Die Nahtstelle in `computePlan`
`computePlan(plan, sample?)` nimmt optional ein `PlanSample`:
```ts
interface PlanSample {
inflation: number[]; // Inflation %/Jahr (Index 0 = Jahr 1)
assetReturn: (elementId: string, year: number) => number; // Rendite %/Jahr (1-basiert)
}
```
Ohne `sample` rechnet die Funktion **exakt wie bisher** (die geplanten Annahmen). Mit `sample`
liefert sie einen einzelnen simulierten Pfad. Voraussetzung war eine Umstellung der Inflation
auf ein **kumulatives Deflator-Array** `cumInfl[]` (statt der geschlossenen `(1+i)^t`-Formel),
damit die Inflation pro Jahr variieren kann deterministisch bitgenau identisch, durch die
Golden Tests abgesichert. Die AHV-Karriere bleibt bewusst auf der festen Plan-Inflation (sie
ist eine Real-Grösse auf Planungsbasis, sie wird nicht mitgewürfelt).
**Architektur-Vorteil:** `computePlan` ist eine reine Funktion ohne Server-Abhängigkeiten und
läuft damit **im Browser**. Die gesamte Simulation rechnet client-seitig null Serverlast.
~10'000 Läufe in rund 1 Sekunde; die Ausführung gibt alle 500 Läufe die Kontrolle ab
(Fortschrittsbalken, keine eingefrorene Oberfläche).
### 4.12.2 Das statistische Modell
Pro Jahr ein **gemeinsamer Marktschock** `z_markt`; je Element und Jahr:
```
rendite = mittelwert + σ × (ρ × z_markt + √(1−ρ²) × z_eigen)
```
- `z_markt`, `z_eigen`: **standardisierte Student-t** (ν = 5) „fette Ränder", damit
Extremcrashs realistisch häufig auftreten. Eine Normalverteilung macht ein 40%-Jahr zu einem
1-in-250-Ereignis; real ist es ~1-in-15. Standardisiert auf Einheitsvarianz → die eingegebene
Standardabweichung σ bleibt die tatsächliche.
- `ρ = 0.7` → Korrelation zweier riskanter Anlagen ≈ 0.5: **alle riskanten Anlagen fallen im
Crash gemeinsam.** Unabhängiges Würfeln würde das Absturzrisiko systematisch unterschätzen.
- **Böden:** `max(0, rendite)` für PK/3a (schreiben keine negative Rendite gut),
`max(100, rendite)` sonst.
Die Inflation wird analog gezogen (eigener Student-t-Schock, Mittelwert + σ), unabhängig vom
Marktschock. Der Zufallsgenerator ist **seedbar** (reproduzierbare Läufe).
### 4.12.3 Zwei Renditezahlen — und warum
Pro renditetragendem Element (PK, 3a, Sonstiges Vermögen, Immobilie) gibt es im Simulations-Dialog
**zwei** Renditen mit verschiedenen Rollen:
| Zahl | Rolle |
|---|---|
| **Geplante Rendite** (im Plan) | zeichnet die deterministische Linie = der *Zielbalken* |
| **Historische Ø-Rendite** (im Dialog, Pflicht) | der Mittelpunkt, um den die Simulation streut |
Ohne diese Trennung wäre die Kennzahl „P(erreiche mein geplantes Endvermögen)" **immer ~50 %**,
egal welche Rendite man annimmt (der Zielbetrag wüchse ja mit). Erst weil die Simulation um die
*historische* Rendite streut, während der Zielbalken auf der *geplanten* steht, wird ein
konservativer Plan (tiefe Planannahme) korrekt mit einer höheren Erfolgsquote belohnt als ein
optimistischer. Analog auf Plan-Ebene für die Inflation.
**Ehrliche Grenze (im Dialog ausgewiesen):** Die Simulation misst das Risiko *um deine Annahmen
herum* sie beurteilt **nicht**, ob deine Mittelwerte realistisch sind. Ein zu optimistischer
Mittelwert liefert eine beruhigende, aber unrealistische Erfolgsquote (siehe 9.15).
### 4.12.4 Streuungsstufen
| Stufe (Rendite) | σ | Beispiele | | Stufe (Inflation) | σ |
|---|---|---|---|---|---|
| Sehr niedrig | 3 % | Staatsanleihen, Geldmarkt | | Sehr niedrig | 1 % |
| Niedrig | 6 % | Immobilien, defensive Mischportfolios | | Niedrig | 2 % |
| Moderat | 15 % | breit diversifizierte Aktien-ETFs/Fonds | | Manuell | frei |
| Hoch | 25 % | Einzelaktien, Branchen-/Schwellenländerfonds | | | |
| Sehr hoch | 55 % | Kryptowährungen, hochspekulative Anlagen | | | |
| Manuell | frei | eigene Eingabe | | | |
Default-Stufe je Typ: PK → sehr niedrig · 3a/Immobilie → niedrig · Sonstiges Vermögen → moderat.
Bei Inflation gibt es bewusst nur zwei Stufen (höhere wären Hyperinflations-Annahmen).
Quellen der σ-Werte: Anleihen ~6 %, globale Aktien ~1518 %, Schweizer Immobilien(fonds) ~2 %,
Bitcoin ~54 %, Schweizer Inflation SD der letzten 20 J. ~1 %. Belege: BSV/Weltbank sowie
Markt-/Volatilitätsstatistiken (recherchiert 2026-07-17).
### 4.12.5 Ergebnis
| Kennzahl | Bedeutung |
|---|---|
| **Ruinwahrscheinlichkeit** | Anteil der Läufe mit `ruinAge !== null` (Vermögen fällt vor Planende unter 0) |
| **Erfolgswahrscheinlichkeit** | Anteil der Läufe mit Endvermögen ≥ Zielbetrag (nominal, vorbelegt mit dem geplanten Nachlass) |
| **Fächer** | je Alterspunkt (Phasengrenzen) das 10-/50-/90-Perzentil des Vermögens; dazu die deterministische Planungslinie |
Der Median liegt typischerweise **unter** der deterministischen Linie der „Volatilitäts-Drag"
(`geometrisch ≈ arithmetisch σ²/2`) macht sichtbar, dass die glatte Ein-Zahl-Planung schon
leicht zu optimistisch ist. Pflichtfelder: ohne die historischen Ø-Werte startet die Simulation
nicht.
---
# 5. Technische Spezifikation
@@ -1489,6 +1590,7 @@ PlanComputed ← an den Client geliefert
| `elements.ts` | Kategorien, Labels, Reihenfolge, JSON-Payload-Typen, Zod-Schemas, `num()` |
| `types.ts` | Domänentypen für API und Berechnung |
| `constants.ts` | Schweizer Systemparameter |
| `montecarlo.ts` | Monte-Carlo-Simulation (Sampler + Treiber). Keine I/O, läuft im Browser. |
| `queries.ts` | Prisma-Includes, `toPlanInput()`, Ownership-Abfragen |
| `db.ts` | Prisma-Singleton (Global-Cache im Dev, verhindert Connection-Leak bei HMR) |
| `auth.ts` | JWT erzeugen/prüfen (Edge-kompatibel via `jose`) |
@@ -1712,6 +1814,7 @@ wird der Plan neu geladen; die Berechnung kommt immer vom Server.
| `PlanProfileFields` | 101 | Wiederverwendete Grundprofil-Felder |
| `PhaseDetail` | 95 | Phase bearbeiten/löschen |
| `SparquoteChart` | 64 | Einkommen/Ausgaben pro Jahr mit Spar-/Verzehrbändern |
| `MonteCarloDialog` | ~430 | Monte-Carlo-Dialog: Erklärung, Eingaben, Lauf, Ergebnis + Fächer |
| `InfoBubble` | 28 | Hilfe-Tooltip |
### 5.5.3 Wiederverwendungsmuster
@@ -1919,10 +2022,11 @@ Zielumgebung: Hetzner CX23, Traefik als Reverse Proxy, Domain `fpt.aicds.ch`, Gi
## 8.1 Teststrategie
Getestet wird ausschliesslich der Berechnungskern bewusst, da dort die Fachlogik und das
Regressionsrisiko liegen. `src/lib/calculations.test.ts` enthält 41 Tests (AHV-Rentenformel,
Immobilie, Teilverkauf, Sonderamortisation, AHV einkommensabhängig, „V5 Golden Tests"),
ausgeführt mit Vitest in der Node-Umgebung (`vitest.config.ts`, Include `src/**/*.test.ts`).
Es gibt **keine** Komponenten-, API- oder E2E-Tests.
Regressionsrisiko liegen. `src/lib/calculations.test.ts` (41 Tests: AHV-Rentenformel, Immobilie,
Teilverkauf, Sonderamortisation, AHV einkommensabhängig, „V5 Golden Tests") und
`src/lib/montecarlo.test.ts` (7 Tests) ergeben zusammen **48 Tests**, ausgeführt mit Vitest in
der Node-Umgebung (`vitest.config.ts`, Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-,
API- oder E2E-Tests.
## 8.2 Testfälle
@@ -1945,6 +2049,10 @@ Es gibt **keine** Komponenten-, API- oder E2E-Tests.
| **Immobilie: Verkauf** | Grundstückgewinnsteuer auf `Verkaufspreis Kaufpreis`, nicht auf den Verkehrswert |
| **Teilverkauf (42)** | Betrag ins Cash (→ `capitalInflow`), Rest bleibt aktiv; am Endwert gekappt; Halten/Vollverkauf unverändert |
| **Sonderamortisation (15)** | Einmaltilgung senkt Restschuld, belastet Cash (→ `capitalInvest`), am Restsaldo gekappt |
| **MC: Determinismus** | Streuung 0 reproduziert exakt das deterministische Ergebnis (Bänder kollabieren) |
| **MC: Volatilität / Vol-Drag** | σ > 0 spreizt p10<median<p90; Median unter dem deterministischen Wert |
| **MC: Erfolg / Reproduzierbarkeit** | P(≥ Ziel) fällt mit steigendem Ziel; gleicher Seed → identisches Ergebnis |
| **MC: Boden / Ruin** | 0%-Boden hält PK/3a ≥ Startwert; sicherer Verzehr → Ruinwahrscheinlichkeit 100 % |
| Test 1 Ansparen | Einkommen +2 % nominal, Ausgaben real flach: Endvermögen 761'654 nominal / 565'928 real (±1 %), kein negatives Cash |
| Test 2 Verzehr/Ruin | Rente nominal fix 60k, Ausgaben real 100k, Vermögen 900k @3 %: `ruinAge === 94` |
| Test 3 Cash-Ausgleich | Sparrate 6'364: `cashEnd === 5472`, nie negativ |
@@ -2125,7 +2233,21 @@ hinterfragt. Der Benutzer kennt seine Steuerrechnung dagegen exakt aus der letzt
**Heutiger Weg:** ein normales Ausgaben-Element „Steuern" dafür braucht es kein neues Feld.
Echte Steuerlogik ist Roadmap Nr. 23, mit Kanton und Gemeinde als Eingabe.
## 9.15 Kleinere Beobachtungen
## 9.15 Monte-Carlo misst Risiko um die Annahmen, nicht deren Richtigkeit
Die Simulation streut um die eingegebenen Mittelwerte (historische Rendite/Inflation). Sie
beurteilt **nicht**, ob diese Mittelwerte realistisch sind. Ein zu optimistischer Mittelwert
liefert eine beruhigende, aber unrealistische Erfolgsquote die präzise Prozentzahl täuscht
dann Sicherheit vor. Der Dialog weist das explizit aus; die Mittelwerte sind Pflichtfelder ohne
Default, damit sie bewusst gesetzt werden.
Weitere bewusste Vereinfachungen der Stufe A: Inflation und Renditen werden **unabhängig**
gezogen (real sind sie negativ korreliert); die Normalverteilungs-Alternative wird gar nicht
angeboten (fette Ränder fest eingebaut); die Simulationsparameter werden **nicht persistiert**
(ephemer im Dialog). Ein historischer Backtest (Stufe B) und korrelierte/vollständigere Modelle
(Stufe C) sind offen.
## 9.16 Kleinere Beobachtungen
- `planToCsv(plan, computed)` erhält den `plan`-Parameter, verwendet ihn aber nicht.
- Der Typ `Selection` in `PlanView` hat nur eine Variante (`{ type: "phase" }`) ein Rest der