Szenario-Vergleich in der Monte-Carlo-Simulation + Sensitivitaetsanalyse (Tornado)
Deploy App / deploy (push) Successful in 53s

Monte Carlo ueber mehrere Szenarien eines Plans in einem Lauf:
- Annahmen nur EINMAL je logischem Element (Zuordnung ueber sourceElementId,
  dieselbe Kette wie beim Diff) -- sonst vergleicht man die Eingaben statt
  der Szenarien
- gemeinsamer Seed fuer alle Szenarien (Common Random Numbers), damit
  Unterschiede strukturell und nicht zufaellig sind
- Zielbetrag bleibt szenario-eigen: die Erfolgswahrscheinlichkeit misst,
  wie oft ein Szenario sein EIGENES Versprechen haelt
- Vergleichstabelle + Median-Linien; bei einem Szenario unveraenderter Faecher

Sensitivitaetsanalyse (Roadmap Nr. 20), eigener Dialog "Einflussfaktoren":
- neues reines Modul sensitivity.ts, One-at-a-time ueber 7 Treiber
- Bandbreiten je Treiber pflichtig und ohne Default (die Balkenlaenge haengt
  direkt davon ab)
- Pensionsalter bewusst ausgeschlossen: nicht variierbar ohne Mitverschieben
  der Phasengrenzen (Begruendung in 9.18)

SPEZIFIKATION auf 1.0: neue Kapitel 3.6.6, 4.12.6, 4.13, 9.18, 9.19 sowie
vier korrigierte Dokumentationsfehler (Kap. 1.2, 4.6.5, 5.2/5.3/5.5.2/8.1,
Glossar). 20 Tests ergaenzt (60 -> 80). Keine DB-Aenderung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
2026-07-18 14:33:27 +02:00
parent d203e504c0
commit 3fe7a3978d
9 changed files with 1762 additions and 211 deletions
+251 -23
View File
@@ -4,10 +4,10 @@
| | |
|---|---|
| **Dokument** | Funktionale und Technische Spezifikation FPT |
| **Version** | 0.9 |
| **Version** | 1.0 |
| **Datum** | 2026-07-18 |
| **Status** | Lebendes Dokument |
| **Codestand** | Arbeitsstand nach `a5d4868` inkl. UI-Umbau und Planstart (Branch `main`) |
| **Codestand** | Arbeitsstand nach `d203e50` inkl. Szenario-Vergleich in der Monte-Carlo-Simulation und Sensitivitätsanalyse (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 |
|---|---|---|---|
| 1.0 | 2026-07-18 | Claude (Opus 4.8) | **Szenario-Vergleich in der Monte-Carlo-Simulation** und **Sensitivitätsanalyse / Tornado** (Roadmap Nr. 20). (1) Die MC-Simulation rechnet neu **mehrere Szenarien desselben Plans in einem Lauf**. Die historischen Annahmen werden dabei nur **einmal je logischem Element** erfasst die Zuordnung über die Herkunfts-Kette `sourceElementId`, dieselbe Grundlage wie beim Diff (neue Funktionen `resolveRootElementId`, `buildElementGroups`, `paramsForScenario`, `runMonteCarloMulti`). Alle Szenarien laufen mit **demselben Seed** (Common Random Numbers), damit Unterschiede strukturell und nicht zufällig sind. Der **Zielbetrag bleibt szenario-eigen** (vorbelegt mit dem jeweils geplanten Endvermögen) nur so misst die Erfolgswahrscheinlichkeit, wie oft ein Szenario sein *eigenes* Versprechen hält. Ergebnis als Vergleichstabelle plus Median-Linien je Szenario; bei einem einzelnen Szenario unverändert der bisherige Fächer. (2) Neuer Bereich **Einflussfaktoren** (eigener Button, eigener Dialog) mit einem **Tornado-Chart** nach dem One-at-a-time-Verfahren: neues reines Modul `sensitivity.ts` mit sieben Treibern, je Treiber an-/abwählbar und mit **pflichtiger, frei definierbarer Bandbreite ohne Default**. Das **Pensionsalter ist bewusst nicht enthalten** (Begründung: 9.18). Neue Kapitel 3.6.6, 4.12.6, 4.13, 9.18; 20 Tests ergänzt (60 → 80). Keine DB-Änderung, keine Verhaltensänderung bestehender Pläne. **Ausserdem vier Dokumentationsfehler korrigiert:** Kap. 1.2 nannte noch Monte-Carlo, Hypothekarzinsen und Immobilien-Wertsteigerung als nicht umgesetzt (seit 0.5/0.7 vorhanden); Kap. 4.6.5 endete mit einem widersprüchlichen Restsatz zur fehlenden Wertsteigerung; Kap. 5.2/5.3/5.5.2/8.1 waren bei Migrationszahl, Dateiliste, Komponentenliste und Testzahlen veraltet; der Glossar-Eintrag „Szenario" beschrieb noch das Modell vor V6. |
| 0.9 | 2026-07-18 | Claude (Opus 4.8) | **UI-Umbau und Planstart.** (1) Die Grafiken liegen neu im eigenen Bereich **Grafiken** (Dialog, Button oben) statt unter der Matrix. (2) Kennzahl **Geschätzter Nachlass** entfernt sie war identisch mit dem nominalen Endvermögen. (3) **Monte-Carlo-Button** nach oben zu den Szenario-Aktionen verschoben. (4) Neues Profilfeld **Planstart (Jahr)** (`Scenario.startYear`, Migration; reine Anzeige, Berechnung bleibt in relativen Jahren) erscheint auf der Zeitachse. (5) Zeitachse zeigt neu die **Lebensphasen als Segmente** (Breite = Dauer, Einfärbung nach Phasentyp, Jahresspanne). (6) **Lebensphase bearbeiten** neu als Popup statt Panel unter der Tabelle. (7) **Vermögensverlauf** über **alle Jahre** statt nur über die Phasengrenzen dafür führt `computePlan` das Vermögen neu pro Jahr mit (`YearPoint.wealthNominal/wealthReal`). Zwei Tests ergänzt (58 → 60). |
| 0.8 | 2026-07-18 | Claude (Opus 4.8) | **Szenario-Hierarchie (V6)** grösste Umstrukturierung bisher. Der **Plan** ist neu ein schlanker Behälter ohne Finanzdaten; die berechenbare Einheit ist das **Szenario**, das Grundprofil (inkl. **Pensionsalter** → Frühpensionierungs-Szenarien), Phasen und Elemente trägt. Jeder Plan erhält beim Anlegen automatisch ein **Basisszenario**; weitere Szenarien entstehen als vollständige Kopie eines beliebigen Szenarios und hängen als **Baum** darunter (Sidebar zeigt die Verschachtelung). Kopierte Phasen/Elemente tragen Herkunfts-Verweise (`sourcePhaseId`, `sourceElementId`) darauf beruht die **Abweichungs-Markierung**: geändert = gelb, neu = grün, entfernt = graue Geisterzeile (eigene Theme-Tokens für alle drei Farbschemata). Charts vergleichen neu die Geschwister-Szenarien. Datenmodell: neue Tabelle `Plan`, bisheriger `Plan``Scenario` (IDs erhalten), `planId``scenarioId` in Person/Phase/FinancialElement. API neu unter `/api/scenarios/*`. Neue Kapitel 2.1, 3.2, 4.13; 9 Diff-Tests + Migrations-Test (48 → 58). **Migration mit echtem Postgres (PGlite) verifiziert**, inkl. verschachtelter Szenarien und Cascade. |
| 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. |
@@ -66,15 +67,25 @@ Aus dem Code direkt ableitbare Abgrenzungen:
- **Keine Steuerberechnung** ausser den drei explizit modellierten Sätzen
(Kapitalbezugssteuer, Grundstückgewinnsteuer, PK-Umwandlungssatz). Einkommens- und
Vermögenssteuern sind nicht modelliert und müssen vom Benutzer in den Ausgaben erfasst werden.
- **Keine Monte-Carlo-Simulation / keine Stochastik.** Alle Renditen sind deterministische
Jahresprozentsätze.
- **Keine Hypothekarzinsen.** Eine Hypothek reduziert nur den Nettowert der Immobilie;
Zinskosten sind vom Benutzer in den Ausgaben zu erfassen.
- **Keine Wertentwicklung von Immobilien.** Der Kaufpreis ist über die Phasendauer konstant
(Details siehe [4.6.5](#465-real_estate-immobilie)).
Vermögenssteuern sind nicht modelliert und müssen vom Benutzer in den Ausgaben erfasst werden
(Begründung: [9.14](#914-keine-steuerschätzung)).
- **Keine Nebenkosten, kein Eigenmietwert, keine Mieteinnahmen** bei Immobilien
(siehe [9.3](#93-immobilien-was-noch-fehlt)).
- **Keine automatische Deckung von Liquiditätslücken.** Negatives Cash wird gemeldet, aber nicht
korrigiert (siehe [9.1](#91-cash-wird-nicht-automatisch-ausgeglichen)).
- **Keine Mehrbenutzer-Kollaboration.** Pläne gehören genau einem Benutzer.
> **Hinweis zur Dokumenthistorie:** Bis Version 0.9 stand hier zusätzlich „keine
> Monte-Carlo-Simulation", „keine Hypothekarzinsen" und „keine Wertentwicklung von Immobilien".
> Alle drei sind seit Version 0.5 bzw. 0.7 umgesetzt ([4.4.5](#445-netto-brutto-umrechnung-für-die-ahv),
> [4.6.5](#465-real_estate-immobilie), [4.12](#412-monte-carlo-simulation)); die Abgrenzung war
> versehentlich stehen geblieben.
Die deterministische Rechnung bleibt der Kern des Tools; Stochastik kommt ausschliesslich in der
**Monte-Carlo-Simulation** ([4.12](#412-monte-carlo-simulation)) und als reine Was-wäre-wenn-
Rechnung in der **Sensitivitätsanalyse** ([4.13](#413-sensitivitätsanalyse-tornado)) dazu.
Beide verändern die gespeicherten Plandaten nicht.
## 1.3 Kernprinzip: Plan als selbsttragende Einheit
Seit dem V3-Rework (Migration `20260713150000_profile_to_plan_v3`) trägt **jeder Plan sein
@@ -823,6 +834,26 @@ Dateiname = Planname, nicht-alphanumerische Zeichen durch `_` ersetzt.
Referenz: `src/lib/calculations.ts` Zeilen 620648.
### 3.6.6 Analyse-Bereich „Einflussfaktoren"
Der Button **Einflussfaktoren berechnen** in der oberen Aktionsleiste (neben „Grafiken" und
„Monte-Carlo-Simulation") öffnet die Sensitivitätsanalyse als eigenen Dialog. Aufbau bewusst
analog zur Monte-Carlo-Simulation:
1. **Erklärung** was die Analyse beantwortet, wie sie rechnet, was man davon hat, und die zwei
ehrlichen Grenzen (Bandbreiten-Abhängigkeit, keine Wechselwirkungen).
2. **Zielgrösse** Endvermögen real (Default) oder nominal.
3. **Parameter** je Treiber eine Checkbox; erst angehakt erscheinen die beiden Pflichtfelder
„tief" und „hoch" in der Einheit des Treibers, mit Hilfe-Bubble zu plausiblen Bandbreiten.
Nicht anwendbare Treiber werden gar nicht erst angezeigt.
4. **Ergebnis** Basisfall, Tornado-Chart und Tabelle.
Der Dialog ist bewusst **nicht** Teil des Grafiken-Bereichs: Dieser ist reine Ausgabe, während der
Tornado Pflichteingaben braucht. Ein Hinweis am Ende der Parameterliste benennt, warum das
**Pensionsalter** nicht enthalten ist (siehe [9.18](#918-tornado-was-der-chart-nicht-leistet)).
Referenz: `src/components/SensitivityDialog.tsx`, `src/components/AppShell.tsx`.
## 3.7 Bedienoberfläche
### 3.7.1 Layout
@@ -1195,9 +1226,6 @@ Basis der Verzinsung ist die Liegenschaft.
Falls **nicht** fortgeschrieben und **nicht** Phase 1 (= Neukauf in einer späteren Phase):
`investmentsFromCash += max(0, equity)` das Eigenkapital wird aus dem Cash finanziert.
Der Wert der Immobilie ist über die Phasendauer **konstant der Kaufpreis**; nur die Hypothek
sinkt. Es gibt keine Wertsteigerung der Verkaufspreis wird erst am Übergang erfasst.
### 4.6.6 OTHER_ASSET
```
@@ -1592,6 +1620,146 @@ Der Median liegt typischerweise **unter** der deterministischen Linie der
leicht zu optimistisch ist. Pflichtfelder: ohne die historischen Ø-Werte startet die Simulation
nicht.
### 4.12.6 Mehrere Szenarien im Vergleich
Der Dialog rechnet auf Wunsch **mehrere Szenarien desselben Plans in einem Lauf**. Drei
Entscheide machen den Vergleich überhaupt aussagekräftig.
**(1) Eine Parametereingabe je logischem Element.** Die MC-Parameter hängen an der `elementId`,
und Element-IDs sind szenario-spezifisch eine Kopie bekommt neue IDs. Ohne Zuordnung müsste
dieselbe Anlage pro Szenario erneut erfasst werden. Das wäre nicht nur mühsam, es würde den
Vergleich **zerstören**: Mit 5 % im einen und 6 % im anderen Szenario vergleicht man die
Eingaben statt der Szenarien.
Die Zuordnung läuft über die Herkunfts-Kette `sourceElementId` dieselbe Grundlage wie beim
Diff ([3.2.6](#326-abweichungs-markierung-diff)). `resolveRootElementId` folgt ihr bis zum
Ursprung; alle Elemente mit derselben Wurzel bilden eine **Gruppe** und teilen einen
Parametersatz. Deshalb lädt der Dialog beim Öffnen **alle** Szenarien des Plans, nicht nur die
ausgewählten: Nur so löst sich die Kette auch über ein übersprungenes Zwischen-Szenario auf
(Basis → S1 → S2 bei Auswahl von Basis und S2). Ein Element, das es nur in einem Szenario gibt,
bildet eine eigene Gruppe und wird im Dialog entsprechend gekennzeichnet.
**(2) Gemeinsamer Seed.** Alle Szenarien eines Laufs verwenden denselben Zufalls-Seed
(*Common Random Numbers*). Ohne das wären kleine Unterschiede blosses Rauschen: Bei 1'000 Läufen
beträgt der Standardfehler der Erfolgswahrscheinlichkeit rund 1.5 Prozentpunkte zwei identische
Szenarien könnten 87 % und 90 % zeigen. Mit gemeinsamem Seed teilen **strukturgleiche** Szenarien
exakt dieselben Marktpfade, und die Unterschiede sind rein strukturell. Einschränkung: Die Pfade
sind nur dort identisch, wo die Struktur es ist abweichende Laufzeit oder Elementzahl verschiebt
die Ziehungsreihenfolge.
**(3) Zielbetrag je Szenario.** Der Zielbetrag ist bewusst **nicht** gemeinsam, sondern je
Szenario mit dessen geplantem Endvermögen vorbelegt (einzeln editierbar). Damit misst die
Erfolgswahrscheinlichkeit, wie oft ein Szenario **sein eigenes Versprechen** hält.
> **Warum das der entscheidende Punkt ist:** In der Simulation wird die *geplante* Rendite
> vollständig durch die gewürfelte ersetzt. Unterscheiden sich zwei Szenarien **nur** in der
> geplanten Rendite (5 % vs. 6 %), sind ihre simulierten Verteilungen **identisch** gleicher
> Median, gleicher Fächer, gleiche Ruinwahrscheinlichkeit. Der einzige Unterschied ist der
> Zielbetrag. Mit einem gemeinsamen Zielbetrag zeigte der Vergleich zwei identische Zeilen; mit
> szenario-eigenem Zielbetrag zeigt er die eigentliche Aussage: Das pessimistisch geplante
> Szenario erreicht sein tieferes Ziel häufiger und ist damit das belastbarere. Durch einen Test
> abgedeckt (Kap. 8.2).
Folge für die Darstellung: Die **Ruinwahrscheinlichkeit** ist zielbetrags-unabhängig und damit
die direkt vergleichbare Kennzahl; die Erfolgswahrscheinlichkeit bezieht sich je Zeile auf eine
andere Messlatte. Deshalb steht der Zielbetrag als **eigene Spalte** in der Vergleichstabelle.
**Darstellung:** eine Vergleichstabelle (Szenario, Ziel, Erfolg, Ruin, P10/Median/P90) als
Hauptinstrument, dazu ein Chart mit der **Median-Linie je Szenario**. Übereinandergelegte
1090 %-Bänder wären unlesbar; der vollständige Fächer inklusive deterministischer Linie erscheint
deshalb nur, wenn **genau ein** Szenario ausgewählt ist dann verhält sich der Dialog exakt wie
zuvor.
**Laufzeit:** Die Szenarien laufen sequenziell, der Fortschritt weist Szenario und Gesamtanteil
aus. Die Schätzung skaliert mit der Anzahl Szenarien.
Referenz: `src/lib/montecarlo.ts` (`resolveRootElementId`, `buildElementGroups`,
`paramsForScenario`, `runMonteCarloMulti`), `src/components/MonteCarloDialog.tsx`.
## 4.13 Sensitivitätsanalyse (Tornado)
Die Monte-Carlo-Simulation würfelt alle Unsicherheiten gleichzeitig und beantwortet „wie
wahrscheinlich geht mein Plan auf?". Die Sensitivitätsanalyse (Roadmap Nr. 20, `sensitivity.ts`)
beantwortet die komplementäre Frage: **„Welche meiner Annahmen entscheidet überhaupt über das
Ergebnis?"**
### 4.13.1 Verfahren
**One-at-a-time (OAT):**
```
base = Zielgrösse(Plan)
für jeden ausgewählten Treiber d:
lowResult = Zielgrösse(applyDriver(Plan, d, d.low))
highResult = Zielgrösse(applyDriver(Plan, d, d.high))
swing = |highResult lowResult|
sortiere absteigend nach swing → Trichterform, längster Balken zuoberst
```
Alle übrigen Parameter bleiben dabei auf dem Planwert. Das sind 2 Aufrufe je Treiber bei
sieben Treibern 14 `computePlan`-Aufrufe, also Millisekunden. Wie die Monte-Carlo-Simulation
läuft alles **im Browser**; `applyDriver` ist rein und lässt den Ausgangsplan unberührt.
**Zielgrösse** ist das Endvermögen der letzten Phase, wahlweise **real** (Default,
kaufkraftbereinigt) oder nominal. Das Ruinalter wäre als Balkengrösse untauglich, weil es in
vielen Plänen `null` ist.
### 4.13.2 Die Treiber und ihre Einheiten
Die Einheit ist je Treiber verschieden und lässt sich nicht vereinheitlichen, ohne fachlich
falsch zu werden:
| Treiber | Einheit | Wirkung |
|---|---|---|
| Ausgaben | **relativ %** | skaliert `amount` aller `EXPENSE`-Elemente |
| Rendite (PK, 3a, Sonstiges Vermögen) | **Δ Prozentpunkte** | verschiebt `expectedReturn` |
| Lebensdauer | **Δ Jahre** | verlängert/verkürzt die **letzte** Phase (min. 1 Jahr) |
| Inflation | **absolut %** | setzt `inflationRateDefault` |
| Einkommen | **relativ %** | skaliert `amount` aller `INCOME`-Elemente |
| Lohnentwicklung | **Δ Prozentpunkte** | verschiebt `teuerungsausgleich` der `INCOME`-Elemente |
| Wertsteigerung der Immobilie | **Δ Prozentpunkte** | verschiebt `valueGrowth` |
Die Begründungen im Einzelnen:
- **Absolut** nur bei der Inflation es gibt genau einen plan-weiten Wert.
- **Δ Prozentpunkte** bei den Renditen, weil die Elemente je eigene Sätze tragen. Ein absolutes
„3 % bis 7 %" würde die PK auf ETF-Rendite plätten.
- **Relativ %** bei Einkommen und Ausgaben, weil die Elemente je eigene Beträge tragen.
- Immobilien-Wertsteigerung ist ein **eigener** Treiber und nicht Teil von „Rendite", damit sie
nicht doppelt zählt.
Die Skalierung von Einkommen/Ausgaben greift nur dort, wo `amount` gesetzt ist. Das ist korrekt
und beabsichtigt: Ab Phase 2 ist der Wert in der Regel live vererbt ([4.6.1](#461-income--expense)),
und die Fortschreibung leitet ihn aus dem skalierten Basiswert ab die Skalierung wirkt damit
automatisch über alle Folgephasen.
Ein Treiber erscheint nur, wenn der Plan passende Elemente enthält (`applies`).
### 4.13.3 Bandbreiten sind Pflicht ohne Default
Je Treiber gibt der Benutzer eine tiefe und eine hohe Ausprägung an; **Vorgabewerte gibt es
bewusst nicht**. Grund: Die Balkenlänge hängt direkt von diesen Bandbreiten ab. Ein stiller
Default würde nicht hinterfragt, und das Ranking wäre dann eine Aussage über unsere Vorgabe
statt über den Plan dieselbe Begründung wie bei den Monte-Carlo-Mittelwerten
([9.15](#915-monte-carlo-misst-risiko-um-die-annahmen-nicht-deren-richtigkeit)).
Die Hilfe-Bubble je Treiber nennt stattdessen plausible Grössenordnungen. Entscheidend ist, die
Bandbreiten **ähnlich plausibel** zu wählen, nicht ähnlich gross: „±10 % Inflation" (1.5 → 1.65 %)
und „±10 % Ausgaben" sind völlig ungleich wahrscheinlich.
Zwei weitere Regeln: mindestens **zwei** Treiber (ein Tornado ist eine Rangliste ein einzelner
Balken ordnet nichts), und tiefer und hoher Wert dürfen nicht identisch sein (Spannweite 0).
### 4.13.4 Darstellung
Waagrechtes Balkendiagramm, je Balken die Spanne `min…max` der Zielgrösse, senkrechte
Referenzlinie beim Basisfall, sortiert nach Spannweite. Darunter eine Tabelle mit der
eingegebenen Bandbreite, den beiden Ergebniswerten und der Spannweite.
Die **Richtung kann sich umkehren** tiefe Ausgaben ergeben ein hohes Endvermögen. Der Balken
spannt deshalb über `min…max`; welche Eingabe zu welchem Ende gehört, zeigt die Tabelle.
Referenz: `src/lib/sensitivity.ts`, `src/components/SensitivityDialog.tsx`.
---
# 5. Technische Spezifikation
@@ -1624,7 +1792,7 @@ nicht.
FPT/
├── prisma/
│ ├── schema.prisma Datenmodell
│ └── migrations/ 8 Migrationen (chronologisch)
│ └── migrations/ 12 Migrationen (chronologisch, siehe 5.4.6)
├── src/
│ ├── app/
│ │ ├── api/ Route Handlers (siehe Kapitel 6)
@@ -1632,7 +1800,7 @@ FPT/
│ │ ├── page.tsx Einstiegsseite (lädt /api/auth/me → AppShell)
│ │ ├── layout.tsx Root-Layout, Theme-Init-Script, Metadata
│ │ └── globals.css Tailwind + semantische Farb-Tokens (3 Themes)
│ ├── components/ 12 React-Komponenten (alle "use client")
│ ├── components/ 15 React-Komponenten (alle "use client")
│ ├── generated/prisma/ Generierter Prisma-Client (nicht editieren)
│ ├── lib/ Domänenlogik (siehe 5.3)
│ └── middleware.ts Zugriffsschutz (Edge-Runtime)
@@ -1663,7 +1831,9 @@ 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. |
| `montecarlo.ts` | Monte-Carlo-Simulation (Sampler + Treiber), Szenario-Vergleich und Element-Gruppierung über die Herkunfts-Kette. Keine I/O, läuft im Browser. |
| `sensitivity.ts` | Sensitivitätsanalyse / Tornado: Treiber-Katalog, Parameter-Transformationen, `computeTornado`. Rein, läuft im Browser. |
| `diff.ts` | Abweichungs-Erkennung eines Szenarios gegen sein Eltern-Szenario (Kap. 3.2.6) |
| `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`) |
@@ -1916,7 +2086,9 @@ 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 |
| `MonteCarloDialog` | ~600 | Monte-Carlo-Dialog: Erklärung, Szenario-Auswahl, Eingaben, Lauf, Vergleichstabelle + Fächer |
| `SensitivityDialog` | ~330 | Einflussfaktoren: Erklärung, Treiber-Auswahl mit Bandbreiten, Tornado + Tabelle |
| `SpecView` | 50 | Rendert `SPEZIFIKATION.md` (via `/api/spec`) als lesbares Dokument |
| `InfoBubble` | 28 | Hilfe-Tooltip |
### 5.5.3 Wiederverwendungsmuster
@@ -2139,11 +2311,17 @@ 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` (41 Tests: AHV-Rentenformel, Immobilie,
Teilverkauf, Sonderamortisation, AHV einkommensabhängig, „V5 Golden Tests") und
`src/lib/montecarlo.test.ts` (7 Tests), `src/lib/diff.test.ts` (9 Tests) und `src/lib/migrations.test.ts` (1 Test, spielt alle Migrationen gegen echtes PostgreSQL ein) ergeben zusammen **60 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. Ausgeführt mit Vitest in der Node-Umgebung (`vitest.config.ts`,
Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-, API- oder E2E-Tests.
| Datei | Tests | Schwerpunkt |
|---|---|---|
| `calculations.test.ts` | 43 | AHV-Rentenformel, Immobilie, Teilverkauf, Sonderamortisation, AHV einkommensabhängig, „V5 Golden Tests" |
| `sensitivity.test.ts` | 14 | Treiber-Transformationen (Reinheit, Einheiten, Kappung), Verfügbarkeit, Tornado-Sortierung und -Richtung |
| `montecarlo.test.ts` | 13 | Determinismus, Volatilität/Vol-Drag, Böden, Reproduzierbarkeit; Element-Gruppierung und Szenario-Vergleich |
| `diff.test.ts` | 9 | Abweichungs-Erkennung gegen das Eltern-Szenario |
| `migrations.test.ts` | 1 | spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein |
| **Total** | **80** | |
## 8.2 Testfälle
@@ -2170,6 +2348,15 @@ API- oder E2E-Tests.
| **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 % |
| **MC: Herkunfts-Kette** | `resolveRootElementId` folgt der Kette bis zum Ursprung; Verweis ins Leere → eigenes Element ist Wurzel; defekte Kette terminiert |
| **MC: Element-Gruppen** | Kopie und Original ergeben **eine** Gruppe (nicht zwei); Auflösung auch über ein nicht ausgewähltes Zwischen-Szenario; ein nur in einem Szenario neues Element bildet eine eigene Gruppe |
| **MC: Parameter-Übersetzung** | `paramsForScenario` bildet die Gruppen-Parameter auf die Element-Ids des jeweiligen Szenarios ab |
| **MC: Szenario-Vergleich** | Zwei Szenarien, die sich nur in der **geplanten** Rendite unterscheiden: identischer Median/P10/Ruin (gleicher Seed, gleiche Struktur), aber **höhere Erfolgswahrscheinlichkeit** beim pessimistisch geplanten der einzige Unterschied ist der Zielbetrag |
| **Tornado: Reinheit** | `applyDriver` lässt den Ausgangsplan unverändert |
| **Tornado: Einheiten** | Inflation absolut gesetzt; Rendite/Lohnentwicklung in pp verschoben; Ausgaben relativ skaliert (Einkommen unberührt) |
| **Tornado: Lebensdauer** | verschiebt nur die letzte Phase; Kappung bei mindestens 1 Jahr |
| **Tornado: Verfügbarkeit** | Treiber ohne passende Elemente werden ausgeblendet (z. B. Immobilien-Wertsteigerung ohne Immobilie) |
| **Tornado: Sortierung/Richtung** | absteigend nach Spannweite; Treiber ohne Bandbreite hat Spannweite 0 und steht zuunterst; höhere Ausgaben → tieferes, höhere Rendite → höheres Endvermögen |
| **Vermögen je Jahr** | 100k @ 10 % über 3 J. → 110k/121k/133.1k je Jahrespunkt; Endjahr = Phasen-Endvermögen |
| **Vermögen real** | 100k bei 10 % Inflation → real 90'909 |
| Test 1 Ansparen | Einkommen +2 % nominal, Ausgaben real flach: Endvermögen 761'654 nominal / 565'928 real (±1 %), kein negatives Cash |
@@ -2393,6 +2580,45 @@ Verwandt: Wird ein Element in der Vorlage gelöscht, gilt das Gegenstück im Kin
(`react-hooks/set-state-in-effect`); an vergleichbaren Stellen ist die Regel andernorts
bewusst per `eslint-disable` deaktiviert.
## 9.18 Tornado: was der Chart nicht leistet
**Die Balkenlänge hängt von den eingegebenen Bandbreiten ab.** Wer Renditen mit ±2 Prozentpunkten
und Ausgaben mit ±20 % variiert, misst zu einem Teil die eigene Wahl dieser Bandbreiten. Deshalb
sind sie Pflichteingabe ohne Default und im Ergebnis sichtbar ([4.13.3](#4133-bandbreiten-sind-pflicht--ohne-default)).
Aussagekräftig ist die **Reihenfolge**, nicht der absolute Betrag.
**One-at-a-time sieht keine Wechselwirkungen.** Schlechte Renditen *und* hohe Ausgaben treffen
härter als die Summe der Einzelbalken weil in der Folge Kapital verzehrt wird, das später zur
Verzinsung fehlt. Für Kombinationen ist die Monte-Carlo-Simulation zuständig.
**Das Pensionsalter ist bewusst nicht enthalten.** Die Roadmap nennt es als Top-Hebel, aber
`retirementAge` lässt sich im aktuellen Datenmodell nicht isoliert variieren, ohne die
Phasengrenzen mitzuverschieben und das Ergebnis wäre nicht ungenau, sondern **irreführend**.
Beispiel: Person 45, Pension mit 65, Phase 1 = 20 Jahre (45→65), Phase 2 = 20 Jahre.
- **Pensionsalter auf 62:** Phase 1 beginnt mit 45, also `45 < 62` → die Phase bleibt vollständig
Erwerbsphase. Die Person arbeitet im Modell weiterhin bis 65, der Pensions-Übergang liegt an
derselben Grenze. Wirkung auf das Ergebnis: **praktisch null.**
- **Pensionsalter auf 68:** Phase 2 beginnt mit 65, also `65 < 68` → Phase 2 wird zur
**Erwerbsphase**, das Einkommen läuft weiter. Zugleich wird `ownerRetiresNext` an der Grenze
nach Phase 1 falsch (65 ≥ 68 trifft nicht zu), womit der **Pensions-Übergang komplett entfällt**:
keine PK-Verrentung, kein 3a-Bezug, keine AHV-Rente. Der Balken wäre riesig er misst aber den
Ausfall der Vorsorgelogik, nicht „drei Jahre länger arbeiten".
Fachlich korrekt wäre nur, `retirementAge` **und** die Phasengrenze gemeinsam zu verschieben
(Erwerbsphase kürzer, Pensionsphase länger). Das hat eigene Sonderfälle Paare mit
unterschiedlichem Pensionsalter, Grenzen abseits des Pensionsereignisses, Verschiebung grösser als
die Phasendauer und ist als eigener Arbeitsschritt offen. Der verwandte Treiber **Lebensdauer**
(Dauer der letzten Phase) ist dagegen sauber abgebildet und deckt einen Teil des Bedürfnisses ab.
## 9.19 Simulationsparameter werden nicht gespeichert
Weder die Monte-Carlo-Annahmen noch die Tornado-Bandbreiten werden persistiert; beide leben nur
im geöffneten Dialog. Das ist ein bewusster Entscheid (kein Datenmodell für Annahmen, keine
Migration), hat aber den Preis, dass eine Analyse bei jedem Öffnen neu parametrisiert werden muss.
Bei der Monte-Carlo-Simulation über mehrere Szenarien fällt das stärker ins Gewicht als zuvor,
weil dort mehr Eingaben zusammenkommen.
---
# 10. Glossar
@@ -2400,7 +2626,9 @@ Verwandt: Wird ein Element in der Vorlage gelöscht, gilt das Gegenstück im Kin
| Begriff | Bedeutung im FPT |
|---|---|
| **Plan** | Selbsttragende Planungseinheit: Grundprofil + Phasenkette + Elemente |
| **Szenario** | Deep-Copy eines Plans bis zu einer Verzweigungsphase; danach unabhängig |
| **Szenario** | Die berechenbare Einheit (seit V6): trägt Grundprofil, Phasenkette und Elemente. Jeder Plan hat genau ein Basisszenario; weitere entstehen als vollständige Kopie eines beliebigen Szenarios und hängen als Baum darunter |
| **Logisches Element** | Dasselbe finanzielle Element über Szenariogrenzen hinweg, erkannt über die Herkunfts-Kette `sourceElementId` Grundlage der einmaligen Parametereingabe im Szenario-Vergleich ([4.12.6](#4126-mehrere-szenarien-im-vergleich)) |
| **Spannweite (Tornado)** | Differenz zwischen dem Ergebnis beim tiefen und beim hohen Wert eines Treibers; bestimmt Balkenlänge und Rangfolge |
| **Grundprofil** | Haushaltsform, Personen (Alter, Pensionsalter, Name), Inflationsannahme |
| **Lebensphase** | Zeitabschnitt mit fester Dauer; darf keine Pensionierung überspannen |
| **Phasentyp** | `ERWERB` / `PENSION` / `MIXED`; abgeleitet, nie gespeichert |
@@ -2429,4 +2657,4 @@ Verwandt: Wird ein Element in der Vorlage gelöscht, gilt das Gegenstück im Kin
---
*Ende der Spezifikation v0.1*
*Ende der Spezifikation v1.0*