Szenario-Vergleich in der Monte-Carlo-Simulation + Sensitivitaetsanalyse (Tornado)
Deploy App / deploy (push) Successful in 53s
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:
+251
-23
@@ -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` (v1–v5) 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 620–648.
|
||||
|
||||
### 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
|
||||
10–90 %-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*
|
||||
|
||||
Reference in New Issue
Block a user