Live-Simulation: Was-waere-wenn-Regler (Roadmap 22)
Deploy App / deploy (push) Successful in 54s

Zweispalter: links Regler, rechts waehlbare Grafik, oben Kennzahlen mit
Differenz zum unveraenderten Plan. Keine eigene Rechenlogik -- die Regler
nutzen dieselben Transformationen wie der Tornado.

Neu: applyElementDriver / tunableElements fuer einzeln regelbare
Element-Renditen; livesim.ts; AllocationChart aus dem Dashboard geloest.

computePlan misst 0.2 ms auf 60 Jahren -> synchron, ohne Debounce.
Spezifikation 0.18 (4.15 und 9.27 neu), 15 Tests (124 -> 139).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 21:22:49 +02:00
parent 50e98122cf
commit d04e07fdfb
9 changed files with 1009 additions and 62 deletions
+118 -3
View File
@@ -4,7 +4,7 @@
| | |
|---|---|
| **Dokument** | Funktionale und Technische Spezifikation FPT |
| **Version** | 0.17 |
| **Version** | 0.18 |
| **Datum** | 2026-07-18 |
| **Status** | Lebendes Dokument |
| **Codestand** | Arbeitsstand nach `1d046e9` inkl. zwei Monte-Carlo-Fragestellungen (Branch `main`) |
@@ -17,6 +17,7 @@
| Version | Datum | Autor | Änderung |
|---|---|---|---|
| 0.18 | 2026-07-19 | Claude (Opus 4.8) | **Live-Simulation** (Roadmap Nr. 22). Neuer Button und Dialog als Zweispalter: links Schieberegler, rechts eine wählbare Grafik, darüber eine Kennzahlenleiste. Dreht man an einem Regler, wird der Plan **sofort** neu gerechnet ohne für jede Variante eine Szenario-Kopie anzulegen. **Keine eigene Rechenlogik:** Die Regler benutzen dieselben Transformationen wie der Tornado (`applyDriver`), können also gar nicht etwas anderes zeigen als die Einflussfaktoren-Analyse. Neu ist nur `applyElementDriver` dieselbe Verschiebung auf ein **einzelnes** Element statt auf eine ganze Kategorie: Standardmässig gibt es einen Sammelregler «Rendite», ein Klick auf «Renditen einzeln aufschlüsseln» ersetzt ihn durch je einen Regler pro Anlage (der Sammelregler wird dabei **entfernt**, nicht ergänzt, sonst zählte eine Bewegung doppelt; ein Test sichert ab, dass beide Ansichten denselben Plan beschreiben). Der unveränderte Plan wird als **Referenzlinie** mitgezeichnet, und die Kennzahlenleiste weist Endvermögen nominal/real **mit Differenz zum Plan** aus sowie als eigene Karte ob das Kapital reicht; ein gekippter Plan ist einer Verlaufslinie sonst nicht anzusehen. Gemessene Laufzeit von `computePlan`: **0.2 ms** auf einem 60-Jahres-Plan mit 10 Elementen, also rund 1 % des 16-ms-Frame-Budgets deshalb wird synchron gerechnet, **ohne Debounce und ohne Worker**. Anders als der Tornado haben die Regler **Standardbereiche** (Begründung des scheinbaren Widerspruchs zu 9.18: neues Kapitel 9.27), beide Enden editierbar. **Das Pensionsalter fehlt weiterhin** (9.18, eigener Roadmap-Punkt); «Als Szenario speichern» ist bewusst zurückgestellt, ersatzweise zeigt der Dialog die aktive Einstellung als lesbare Zeile. Die Vermögensaufteilung wurde als `AllocationChart` aus dem Dashboard herausgelöst, damit beide sie nutzen. Neue Kapitel 4.15 und 9.27; 15 Tests ergänzt (124 → 139). Keine API-, DB- oder Schreib-Änderung das Feature liest ausschliesslich. Nebenbei dieselbe vom Umlaut-Sweep verstümmelte Hex-Farbe (`#7c3aed`) wie in 0.17, diesmal im Dashboard. |
| 0.17 | 2026-07-19 | Claude (Opus 4.8) | **Monte-Carlo: zwei Welten, vier Fälle.** Behebt einen Darstellungs-Widerspruch: Zuvor konnte «Planung 69 % erreicht» neben «Ziel 3 Mio nur 41 %» stehen, obwohl 3 Mio unter dem Plan-Endbetrag von 3.7 Mio lag die beiden Zahlen stammten aus **verschiedenen simulierten Welten**. Neu läuft die Simulation **immer zweimal** (historische Renditen / geplante Werte, gemeinsamer Seed) und liest aus **jeder** Verteilung **beide** Schwellen ab: Plan-Endbetrag und Zielbetrag. Fall 1 und Fall 3 stammen damit aus derselben Verteilung, wodurch ein tieferes Ziel **nie** unwahrscheinlicher sein kann als ein höheres der Widerspruch ist strukturell ausgeschlossen (Test). Zweite Korrektur: Der Nullpunkt für das Urteil ist **nicht 50 %**, sondern **Fall 2** (derselbe Schwellwert in der eigenen geplanten Welt); durch den Volatilitäts-Drag liegt der je nach Streuung bei 2748 %. Verglichen wird Fall 1 gegen Fall 2 mit ± 5 pp Toleranzband → «zurückhaltend / realistisch / zu optimistisch». Darstellung: Fall 1 prominent mit Urteil, Fall 3+4 als Satzpaar untergeordnet, Fall 2 und die Mediane klein als Referenz. Der Drei-Wege-Umschalter aus 0.16 entfällt; historische Mittelwerte **und** Zielbetrag sind jetzt beide Pflicht. Technisch: `MonteCarloResult.finalWealthSorted` (alle Endvermögen sortiert) plus neuer Helfer `probabilityAtLeast` (Binärsuche) vier Zahlen aus zwei Läufen statt vier Läufen. Kapitel 4.12.7 und 9.26 neu gefasst; 3 Tests ergänzt (121 → 124). Keine Änderung am Rechenkern. |
| 0.16 | 2026-07-19 | Claude (Opus 4.8) | **Monte-Carlo mit zwei Fragestellungen** (Roadmap Nr. 46). Ein Umschalter oben trennt: **«Planung prüfen»** (Fall 1, wie bisher) würfelt um die **historischen** Renditen und prüft gegen den **Planungs-Endbetrag** (read-only) «wie realistisch ist meine Planung?». **«Ziel prüfen»** (Fall 2, neu) würfelt um die **geplanten** Werte aus dem Plan und prüft gegen einen **manuellen Zielbetrag** «erreiche ich mein Ziel?»; hier sind keine historischen Mittelwerte nötig, nur die Streuung. **«Beides»** rechnet beide Durchgänge gleichzeitig (gemeinsamer Seed). Neue Ergebnis-**Deutungstexte** je Fall (weich formuliert wegen des Volatilitäts-Drags). Der Fächer stammt aus dem historischen Durchgang; der Zielbetrag ist in Fall 2 **einer für alle** Szenarien. `runMonteCarloMulti` nimmt neu die Inflation **je Szenario** (`inflationMeanFor`); neuer Helfer `plannedReturnOf`. Kapitel 4.12 überarbeitet, 4.12.7 und 9.26 neu; 8 Tests ergänzt (119 → 121). Nebenbei eine vom Umlaut-Sweep verstümmelte Hex-Farbe (`#7c3aed`) korrigiert. Keine Änderung am Rechenkern. |
| 0.15 | 2026-07-19 | Claude (Opus 4.8) | **Plan-Assistent überarbeitet** (Schritt 2 und 4). Rein an der Oberfläche, keine Änderung an Berechnung, Datenmodell oder API. **(Schritt 2 Lebensphasen):** Die Lebenslinie zerfällt neu an den **fixen Pensionierungszeitpunkten** in Abschnitte (neues reines Modul `phaseplan.ts`, `planSegments`): Erwerb (alle arbeiten), Misch (eine pensioniert, eine arbeitet), Pension (alle pensioniert) jeweils mit **kurzer Definition**. In den durch eine Pensionierung **fest begrenzten** Abschnitten verteilt der Nutzer beliebig viele Phasen mit **+/Papierkorb** und **eigenem Namen je Phase**; eine Live-Summe erzwingt, dass die Phasendauern exakt aufgehen («Weiter» ist bis dahin gesperrt). Der letzte Pensions-Abschnitt ist **offen** (Lebensdauer frei). Die Anzahl Abschnitte wird **abgeleitet** Einzelplan: 2 (Erwerb, Pension); Paar mit unterschiedlichem Pensionsalter: 3. Neue **Zeitachse** mit Pensionierungs-Flaggen und nummerierter Beschriftung **unter** dem Balken (auch kurze Phasen bleiben lesbar). Behebt den Fehler, dass die Erwerbsphase zuvor beliebig über die Pensionierung hinaus gesetzt werden konnte. **(Schritt 4 Vorsorge & Vermögen):** bei Paaren aufgeteilt in **Gemeinsam / Person A / Person B**; PK und 3a sind je Person, Wertschriften/Wohneigentum/Schulden je Bereich (gemeinsam oder pro Person). Neue Kapitel 3.2.8 überarbeitet; 8 Tests ergänzt (111 → 119). |
@@ -2289,6 +2290,90 @@ Serverergebnis.
Referenz: `src/lib/calculations.ts`, `src/components/DetailView.tsx`.
## 4.15 Live-Simulation (Was-wäre-wenn-Regler)
Roadmap Nr. 22. Beantwortet weder «welche Annahme entscheidet» (das ist der Tornado,
[4.13](#413-sensitivitätsanalyse-tornado)) noch «wie wahrscheinlich ist das» (das ist
Monte-Carlo, [4.12](#412-monte-carlo-simulation)), sondern schlicht: **«Wie sieht mein Plan aus,
wenn ich hier drehe?»** sofort, und ohne für jede Variante eine Szenario-Kopie anzulegen.
Eigener Button **«Live-Simulation»** in der Szenario-Leiste, Dialog als Zweispalter: links die
Regler, rechts die Grafik, darüber eine Kennzahlenleiste.
### 4.15.1 Keine eigene Rechenlogik
Die Regler benutzen **dieselben Transformationen wie der Tornado** (`applyDriver`). Damit kann
die Live-Simulation gar nicht etwas anderes zeigen als die Einflussfaktoren-Analyse beide
bewegen den Plan identisch. Es entsteht kein zweiter, potenziell abweichender Rechenweg.
Neu hinzu kommt nur `applyElementDriver(plan, elementId, deltaPp)`: dieselbe Verschiebung, aber
auf **ein einzelnes** Element statt auf eine ganze Kategorie (bei Immobilien auf `valueGrowth`
statt `expectedReturn`). Element-IDs sind innerhalb eines Szenarios eindeutig; die
Herkunfts-Verkettung `sourceElementId` aus der Monte-Carlo-Simulation braucht es hier **nicht**,
weil die Live-Simulation immer nur auf **einem** Szenario läuft.
### 4.15.2 Sammelregler und Aufschlüsselung
Standardmässig gibt es **einen** Rendite-Regler für alle Anlagen das hält das Panel ruhig und
entspricht dem Tornado. Ein Klick auf **«Renditen einzeln aufschlüsseln»** ersetzt ihn durch je
einen Regler pro renditetragendem Element (PK, 3a, Sonstiges Vermögen, Immobilie). Erst dann
lässt sich die eigentliche Spielfrage stellen: *Was, wenn mein ETF schlechter läuft, die PK aber
wie geplant?*
Der Sammelregler wird beim Aufklappen **entfernt**, nicht bloss ergänzt sonst würde eine
Bewegung doppelt zählen. Aus demselben Grund werden die Rendite-Regler beim Umschalten
zurückgesetzt. Ein Test sichert ab, dass beide Ansichten denselben Plan beschreiben: Alle
Elemente einzeln um +1 pp zu heben ergibt exakt dasselbe Endvermögen wie der Sammelregler auf
+1 pp.
**Bewusst nicht aufschlüsselbar sind Ausgaben und Einkommen.** «Alle Ausgaben ±20 %» ist die
Frage, die man tatsächlich stellt; Ausgaben-Elemente sind typischerweise viele kleine Posten,
deren Einzelregler das Panel fluten würden, ohne eine bessere Frage zu ermöglichen.
**Das Pensionsalter fehlt weiterhin** aus demselben Grund wie beim Tornado
([9.18](#918-tornado-was-der-chart-nicht-leistet)): Es liesse sich nicht verschieben,
ohne die Phasengrenzen mitzuziehen. Ersatzweise gibt es **Lebensdauer** (letzte Phase
verlängern/verkürzen), was das Langlebigkeitsrisiko abdeckt, nicht aber die Frühpensionierung.
### 4.15.3 Referenz und Kennzahlen
Eine wandernde Linie ohne Anker ist wertlos «ist 2.9 Mio jetzt viel oder wenig?». Deshalb:
- Der **unveränderte Plan** wird im Vermögensverlauf als blasse Referenzlinie mitgezeichnet.
- Darüber steht eine **Kennzahlenleiste** mit Endvermögen nominal und real, jeweils mit der
Differenz zum Plan (`3'660'683 → 2'880'100, 780'583`).
- Eine dritte Karte meldet, ob das **Kapital reicht** oder in welchem Alter es aufgebraucht ist.
Das ist die wichtigste Einzelinformation und einer Verlaufslinie nicht zuverlässig anzusehen:
Ein Plan kann optisch plausibel aussehen und trotzdem zwischendurch unter null fallen.
Die Grafik zeigt **wann** sich etwas ändert, die Leiste **wie viel**.
Rechts stehen drei Grafiken zur Wahl: **Vermögensverlauf** (mit Referenzlinie),
**Vermögensaufteilung** je Phase und **Einkommen vs. Ausgaben**. Die beiden letzteren zeigen nur
den simulierten Stand ein zweiter gestapelter Balkensatz wäre nicht mehr lesbar; darauf weist
der Dialog hin.
### 4.15.4 Laufzeit: synchron, ohne Debounce
Gemessen an einem Plan über 60 Jahre mit 10 Elementen braucht `computePlan` rund **0.2 ms**.
Bei 60 fps stehen 16 ms je Bild zur Verfügung die Rechnung kostet also etwa **1 %** des
Budgets. Deshalb wird bei **jeder** Reglerbewegung synchron neu gerechnet: kein Debounce, kein
Web Worker, keine Ladeanzeige. Der Engpass ist das Neuzeichnen der Grafik, nicht die Mathematik.
### 4.15.5 Nichts wird gespeichert
Die Live-Simulation **schreibt nicht** keine API, keine Datenbank, kein Schreibpfad. Genau das
ist der Punkt der Roadmap-Anforderung («ohne für jede Variante eine Szenario-Kopie anzulegen»).
Ein **«Als neues Szenario speichern»** ist bewusst **noch nicht** umgesetzt: Reglerwerte in echte
Element- und Phasenwerte zurückzuschreiben hiesse viele einzelne Schreibvorgänge und einen neuen
Schreibpfad eine eigene Ausbaustufe. Als Behelf zeigt der Dialog die **aktive Einstellung** als
lesbare Zeile («Rendite 1.5 pp · Ausgaben +10 % · Lebensdauer +5 J.»), die sich von Hand in ein
echtes Szenario übertragen lässt.
Referenz: `src/lib/livesim.ts`, `src/lib/sensitivity.ts` (`applyElementDriver`,
`tunableElements`), `src/components/LiveSimDialog.tsx`, `src/components/AllocationChart.tsx`.
---
# 5. Technische Spezifikation
@@ -2361,7 +2446,8 @@ PlanComputed ← an den Client geliefert
| `types.ts` | Domänentypen für API und Berechnung |
| `constants.ts` | Schweizer Systemparameter |
| `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. |
| `sensitivity.ts` | Sensitivitätsanalyse / Tornado: Treiber-Katalog, Parameter-Transformationen, `computeTornado`; zusätzlich `applyElementDriver` / `tunableElements` für die einzeln regelbaren Element-Renditen. Rein, läuft im Browser. |
| `livesim.ts` | Live-Simulation: Reglerkatalog mit Standardbereichen, Anwenden mehrerer Regler, Kennzahlen (Kap. 4.15). Rein, läuft im Browser. |
| `distribution.ts` | Kapitaltopf und Quoten-Zerlegung, Anwenden von Entwurfswerten für die Verteil-Werkzeuge (Kap. 3.6.9/3.6.10). Rein. |
| `phaseplan.ts` | Ableitung der Lebensabschnitte (Erwerb/Misch/Pension) aus den fixen Pensionierungszeitpunkten für den Assistenten (Kap. 3.2.8). Rein. |
| `constants.ts` (erweitert) | zusätzlich `SYSTEM_PARAMETERS`: dieselben Werte maschinenlesbar mit Bedeutung, Herleitung, Quelle und Stand Grundlage der Systemparameter-Ansicht |
@@ -2619,6 +2705,8 @@ wird der Plan neu geladen; die Berechnung kommt immer vom Server.
| `PhaseDetail` | 95 | Phase bearbeiten/löschen |
| `SparquoteChart` | 64 | Einkommen/Ausgaben pro Jahr mit Spar-/Verzehrbändern |
| `MonteCarloDialog` | ~600 | Monte-Carlo-Dialog: Erklärung, Szenario-Auswahl, Eingaben, Lauf, Vergleichstabelle + Fächer |
| `LiveSimDialog` | ~330 | Live-Simulation: Regler links, Grafikwahl rechts, Kennzahlenleiste mit Differenz zum Plan ([4.15](#415-live-simulation-was-wäre-wenn-regler)) |
| `AllocationChart` | ~80 | Gestapelte Vermögensaufteilung je Phase; aus dem Dashboard herausgelöst, damit die Live-Simulation sie mitbenutzen kann |
| `SensitivityDialog` | ~330 | Einflussfaktoren: Erklärung, Treiber-Auswahl mit Bandbreiten, Tornado + Tabelle |
| `DetailView` | ~470 | Detailansichten Element/Phase, Wasserfall-Darstellung, Rechenweg-Blöcke, plan-weite Rechenwege |
| `SystemParametersView` | ~90 | Systemparameter mit Wert, Bedeutung, Herleitung, Quelle und Stand |
@@ -2861,11 +2949,12 @@ Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-, API- oder E2E-Tests
| `explain.test.ts` | 12 | Verlaufswerte je Element, Vollständigkeit beider Wasserfall-Zerlegungen, Rechenweg-Protokoll, Gültigkeit der Spezifikations-Verweise |
| `montecarlo.test.ts` | 18 | Determinismus, Volatilität/Vol-Drag, Böden, Reproduzierbarkeit; Element-Gruppierung, Szenario-Vergleich, Inflation je Szenario, plannedReturnOf; Schwellen-Ablesung (probabilityAtLeast), Monotonie über Schwellen, Fall 2 systematisch unter 50 % |
| `distribution.test.ts` | 8 | Kapitaltopf und Quoten-Zerlegung gegen die Cash-Brücke; Entwurfswerte anwenden ohne Verlust bestehender Felder |
| `livesim.test.ts` | 15 | Element-Regler bewegt genau ein Element; Aufschlüsselung deckt sich mit dem Sammelregler; neutrale Stellung verändert den Plan nicht; Ruinmeldung |
| `phaseplan.test.ts` | 8 | Ableitung der Lebensabschnitte aus den Pensionierungszeitpunkten (Einzel/Paar/bereits pensioniert); letzter Teil immer offen |
| `bridges.test.ts` | 10 | Vermögens- und Cash-Brücke gehen über sieben Plankonstellationen ohne Restgrösse auf; Umbuchungen bleiben aus der Vermögensbrücke heraus |
| `diff.test.ts` | 9 | Abweichungs-Erkennung gegen das Eltern-Szenario |
| `migrations.test.ts` | 1 | spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein |
| **Total** | **124** | |
| **Total** | **139** | |
## 8.2 Testfälle
@@ -3301,6 +3390,32 @@ dass die Simulation Risiko **um deine Annahmen** misst und nicht deren Richtigke
([9.15](#915-monte-carlo-misst-risiko-um-die-annahmen-nicht-deren-richtigkeit)), bleibt bestehen.
## 9.27 Warum die Regler Standardbereiche haben und der Tornado nicht
Zwei Kapitel dieser Spezifikation scheinen sich zu widersprechen:
[9.18](#918-tornado-was-der-chart-nicht-leistet) begründet, warum die
Sensitivitätsanalyse **bewusst keine** Default-Bandbreiten anbietet, während die Live-Simulation
([4.15](#415-live-simulation-was-wäre-wenn-regler)) für jeden Regler einen vorbelegten Bereich
mitbringt. Das ist kein Versehen.
**Beim Tornado bestimmt die Bandbreite das Ergebnis.** Die Balkenlänge ist die Spannweite
zwischen dem tiefen und dem hohen Wert wer «Rendite ±3 pp» gegen «Ausgaben ±5 %» stellt,
erzeugt eine Rangfolge, die er selbst vorgegeben hat. Ein Default wäre dort eine **frei erfundene
Aussage**: Das Werkzeug würde behaupten, ein Treiber sei wichtiger als ein anderer, obwohl der
Unterschied nur aus den voreingestellten Bereichen stammt. Deshalb ist die Bandbreite dort
Pflichteingabe ohne Vorschlag.
**Ein Regler vergleicht nichts.** Er zeigt genau einen Zustand: «bei dieser Rendite kommt dieses
Endvermögen heraus». Der Bereich bestimmt nur, wie weit sich der Schieber bewegen lässt er
verändert das angezeigte Ergebnis an keiner Stelle. Ein Standardbereich erfindet hier also keine
Aussage; er macht den Regler überhaupt erst bedienbar, denn ohne Ober- und Untergrenze gibt es
keinen Schieber.
Die Bereiche sind trotzdem **an beiden Enden editierbar** (Häkchen «Bereiche anpassen»), und
neben jedem Regler steht sein **Neutralpunkt** der Wert, bei dem der Plan unverändert bleibt.
Bei allen Verschiebungs-Treibern ist das 0, bei der Inflation der Planwert selbst, weil sie als
einzige absolut und nicht als Differenz eingegeben wird.
---
# 10. Glossar