Files
FPT/SPEZIFIKATION.md
T
admGitAICDS d9ef980edf
Deploy App / deploy (push) Successful in 3m5s
PDF-Berichte (Roadmap 11)
Neuer Unterpunkt "Berichte" je Plan: Liste plus Assistent (Titel, Notiz,
nominal ODER real, Plan-/Ist-Daten, bis zu drei Szenarien, gespeicherte
Analysen). Layout immer gleich, Auswahl bestimmt nur die Bausteine.

Die PDF-Datei wird ALS DATEI abgelegt (BYTEA in Postgres, nicht im
Container-Dateisystem): Ein Bericht muss in drei Jahren byte-identisch
wieder herunterladbar sein -- eine Neuerzeugung koennte das nach
Aenderungen an Plan, Rechenkern oder Layout nicht garantieren.

Kennzahlen je Szenario inkl. offener Entscheide. Deren Zaehlung liegt neu
als reine Funktion in decisions.ts, die Matrix UND Bericht benutzen --
sonst nennen beide verschiedene Zahlen.

Zu jeder Kennzahl ihre Grundlage als Verweis; die vollstaendigen Annahmen
einmal je Szenario. Haftungsausschluss ist verpflichtend (per Test).

Technik: pdfkit in der Node-Runtime statt Headless-Browser;
@react-pdf/renderer bricht mit React 19. Als externes Paket deklariert,
weil pdfkit Font-Metriken ueber Dateipfade laedt.

Spezifikation 0.25 (3.11 neu), 9 Tests (212 -> 221).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:30:18 +02:00

251 KiB
Raw Blame History

FPT Financial Planning Tool

Funktionale und Technische Spezifikation

Dokument Funktionale und Technische Spezifikation FPT
Version 0.25
Datum 2026-07-18
Status Lebendes Dokument
Codestand Arbeitsstand nach 1d046e9 inkl. zwei Monte-Carlo-Fragestellungen (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

Änderungshistorie

Version Datum Autor Änderung
0.25 2026-07-21 Claude (Opus 4.8) PDF-Berichte (Roadmap Nr. 11, neues Kapitel 3.11). Neuer Unterpunkt «Berichte» auf Plan-Ebene: Liste der erzeugten Berichte plus Assistent zum Anlegen (Titel, Notiz, nominal oder real, Plan- oder effektive Daten, bis zu drei Szenarien, beliebige gespeicherte Analysen). Das Layout ist immer gleich; die Auswahl bestimmt nur, welche Bausteine erscheinen: Deckblatt mit Zusammenfassung und drei Kernaussagen, dann je Szenario Kennzahlen, Vermögensverlauf, Lebensphasen und Annahmen, danach Vergleich, Plan/Ist, Analysen und die Hinweise. Die PDF-Datei wird als Datei abgelegt (BYTEA in Postgres, nicht im Container-Dateisystem, das jeder Deploy neu baut): Ein Bericht muss in drei Jahren byte-identisch wieder herunterladbar sein eine Neuerzeugung könnte das nach Änderungen an Plan, Rechenkern oder Layout nicht garantieren. Kennzahlen je Szenario: Endvermögen, Kapitalreichweite, Vermögen und Vorsorgekapital bei Pensionierung, AHV- und PK-Rente sowie die offenen Entscheide die einzige unmittelbar handlungsleitende Zahl. Damit Bericht und Matrix nie verschiedene Zahlen nennen, liegt deren Zählung neu als reine Funktion in decisions.ts, die beide benutzen. Zu jeder Kennzahl steht ihre Grundlage als kurzer Verweis; die vollständigen Annahmen (Startwerte, Renditen, Raten je Element) stehen einmal je Szenario, statt bei jeder Kennzahl wiederholt zu werden. Ein Haftungsausschluss ist verpflichtend und durch einen Test gesichert ein formal gesetztes PDF wird sonst als Beratung gelesen. Technik: pdfkit in der Node-Runtime statt Headless-Browser (kein Chromium im Image); @react-pdf/renderer schied aus, weil es mit React 19 / Next 16 bricht. Diagramme entstehen als echte Vektoren aus den gespeicherten Zahlen genau dafür wurden die Analysen in 0.24 als Zahlen und nicht als Bilder abgelegt. pdfkit ist als externes Paket deklariert, weil es Font-Metriken über Dateipfade lädt und gebündelt erst in der Produktion bräche. Neue Tabelle Report, Endpunkte unter /api/plans/<id>/reports, neue Module report.ts, report-pdf.ts, decisions.ts; 9 Tests ergänzt (212 → 221).
0.24 2026-07-20 Claude (Opus 4.8) Navigation auf Plan-Ebene und gespeicherte Analysen (neues Kapitel 3.10). Die Seitenleiste ist neu zweistufig: Unter jedem Plan liegen die Unterpunkte Szenarien, Effektive Werte und Analysen; ein Klick auf den Plan-Namen öffnet ein Plan-Dashboard (Kennzahlen Haushaltsdaten direkt, gerechnete Werte ausdrücklich «laut Basisszenario», dazu die Ist-Abweichung, falls erfasst). Die Szenario-Liste zeigt je Szenario Version, Elementzahl, Endvermögen und Ruinalter mit den Aktionen Historie und Matrix; das Basisszenario ist hervorgehoben, der Szenario-Baum in der Seitenleiste bleibt daneben erhalten. Die Analysen-Ansicht bietet vier umklappende Kacheln (Grafiken, Live-Simulation, Monte-Carlo, Einflussfaktoren Umklappen auch per Antippen für Touch). Grafiken öffnen neu nicht mehr alle drei Diagramme, sondern lassen zuerst eines wählen; der Szenario-Vergleich zieht mit zu den Grafiken, der CSV-Export auf die Matrix. Gespeicherte Analysen: Jede Grafik, MC-Simulation und Einflussfaktoren-Berechnung kann festgehalten werden als Zahlen, nicht als Bild (read-only, es wird nichts neu gerechnet). Das hält den Datensatz klein und macht ihn druckfähig: Der spätere PDF-Bericht (Roadmap Nr. 11) zeichnet daraus vektoriell in Druckauflösung, was ein Screenshot nicht könnte. Bewusst nicht gespeichert wird finalWealthSorted (megabyteweise). Jedes Werkzeug schreibt sein Ergebnis in dieselbe generische Form, sodass eine Nur-Lese-Ansicht genügt. Neue Tabelle SavedAnalysis, neue Endpunkte unter /api/plans/<id>/analyses und /dashboard; neue Komponenten PlanViews, SavedAnalysisView, SaveAnalysisButton, neues Modul analyses.ts. Kein Eingriff in den Rechenkern; Testbestand 212 (Migration um SavedAnalysis erweitert).
0.23 2026-07-20 Claude (Opus 4.8) V7: Der Haushalt liegt am Plan, die Annahmen am Szenario. Haushaltsform, Personen (Name, Alter) und Planstartjahr wandern vom Szenario auf den Plan; das Pensionsalter bleibt szenario-eigen (es ist der Kern jedes Früh-/Spätpensionierungs-Szenarios), ebenso Inflation und Cash-Anfangswert. Begründung: Diese Angaben beschreiben den Haushalt, nicht eine Planungsvariante unterscheiden sie sich, ist es ein anderer Plan, kein anderes Szenario. Neue Tabelle PlanPerson (Rolle, Name, Alter je Plan); Person behält nur noch Rolle und Pensionsalter; Plan bekommt householdType und startYear. Der Rechenkern bleibt unberührt: toPlanInput() fügt Plan- und Szenario-Ebene wieder zu einem unveränderten PlanInput zusammen, die 43 Golden Tests laufen durch. Nebeneffekt, der ein reales Problem löst: Weil das Startjahr nun plan-weit ist, landet ein erfasster Ist-Satz für 2031 in allen Szenarien zwingend auf demselben Planjahr vorher war das nicht garantiert. Das Wiederherstellen einer Szenario-Version setzt folgerichtig nur noch das Szenario-Eigene zurück; plan-weite Angaben über eine Version eines Szenarios zu überschreiben, hätte die übrigen stillschweigend mitverändert. Der Profil-Dialog kennzeichnet neu je Feld, ob es plan-weit oder nur dieses Szenario gilt. Die Migration übernimmt die Werte aus dem Basisszenario; zwei neue Tests spielen dafür echte V6-Daten ein und prüfen die Übernahme inkl. abweichender Nebenszenarien (210 → 212).
0.22 2026-07-20 Claude (Opus 4.8) Fehlerbehebung: Cash-Vorbelegung im Ist-Wizard. Der Wizard für die effektiven Werte zeigte als geplanten Cash-Bestand den Stand am Phasenende statt am gewählten Stichtag -- in einer Phase von 2026 bis 2036 also für 2031 den Wert von 2036. Ursache: Der Dialog las cashBridge.cashEnd, weil computePlan den Cash-Bestand bisher nur je Phase auswies. YearPoint trägt neu ein Feld cash (Stand am Jahresende), analog zu wealthNominal; der Wizard liest daraus. Die Vorbelegung der ELEMENTE war nie betroffen -- die stammte schon immer aus dem Jahresverlauf. Zwei Regressionstests decken den gemeldeten Fall ab (208 -> 210). Rein additiv, die 43 Golden Tests laufen unverändert.
0.21 2026-07-20 Claude (Opus 4.8) Effektive Werte / Plan-Ist-Vergleich (Roadmap Nr. 5, neue Kapitel 3.9 und 9.29). Macht aus dem Planer ein Monitoring-Werkzeug. Neuer Knopf «Effektive Werte» auf Plan-Ebene: Liste der Erfassungen plus Wizard in zwei Schritten (Stichtag, dann alle Elemente aller Szenarien inkl. Cash, Einkommen und Ausgaben, vorbelegt mit dem Planwert für dieses Jahr). Ein Ist-Satz hängt am Plan, nicht am Szenario die Wirklichkeit ist dieselbe, egal wogegen man sie hält; die Zuordnung läuft über die Herkunfts-Kette sourceElementId. Das exakte Datum steht in Liste und Zeitachse, für die Rechnung zählt nur die Jahreszahl. Zweiter Rechenlauf: computePlan nimmt neu { actuals }; die Werte schnappen in jedem erfassten Jahr auf die Realität und laufen von dort planmässig weiter (Lücken fallen auf die Plandaten zurück). Ohne die Option verhält sich die Funktion exakt wie bisher die 43 Golden Tests laufen unverändert. Der Sprung wird als eigene Brückenposition actualsCorrection geführt (Vermögens- und Cash-Brücke): Eine Planabweichung ist keine Rendite, und ohne diese Zeile ginge die Zerlegung im Ist-Jahr nicht mehr auf. Matrix: zweiter Umschalter «Plan» / «Effektiv»; im Ist-Modus steht neben dem Wert die Abweichung zum Plan, farbig bewusst kein «beide», das wären mit nominal/real acht Zahlen je Zelle (Begründung 9.29). Zeitachse: Marker je erfasstem Jahr, der jüngste farbig, ältere blass. Alle vier Analysewerkzeuge erhalten eine einheitliche Leiste (nominal/real als Einfachauswahl, Plan/Effektiv); im Vermögensverlauf kommt die Planlinie gestrichelt als Referenz dazu, max. vier Serien. Monte-Carlo: Der Zielbetrag dreht mit real/nominal mit und wird entsprechend beschriftet; eine Zeile weist aus, ab welchem Jahr simuliert wird die Jahre davor sind durch Ist-Werte belegt und werden nicht gewürfelt. Das Startjahr ist abgeleitet, nicht eingebbar: Ein frei gesetztes Jahr würde Jahre als sicher behandeln, die nie erfasst wurden. Ein Ist-Satz erzeugt keine Szenario-Version er ist eine Beobachtung, keine Planänderung. Neue Tabelle ActualsSet (Migration gegen echtes Postgres verifiziert), neue Endpunkte unter /api/plans/<id>/actuals, neue Module actuals.ts und dataview.ts; 27 Tests ergänzt (181 → 208).
0.20 2026-07-19 Claude (Opus 4.8) Raten über Lebensphasen übernehmen (neues Kapitel 3.6.11) und Rate in der Verlaufsgrafik. (1) Ändert man ein Ratenfeld, fragt das Bearbeitungspanel neu nach der Reichweite: nur diese Phase (Vorgabe, bisheriges Verhalten), diese + folgende oder alle Phasen. Anlass war, dass eine geänderte Rendite bisher nur für die eine Phase galt und viermal eingetippt werden musste. Als Ratenfelder gelten expectedReturn (PK, 3a, Sonstiges Vermögen), valueGrowth und interestRate (Immobilie) sowie teuerungsausgleich (Einkommen, Ausgaben); AHV und Schulden haben keine. Die Rückfrage erscheint inline und erst beim Speichern wirksam, nicht als Modal das Zahlenfeld löst bei jedem Tastendruck aus, ein Dialog erschiene bei «5.2» viermal. Sie erscheint nur bei tatsächlich veränderten Raten («nicht gesetzt» und 0 gelten als gleich). Beim Übertragen bleiben die übrigen Werte der Zielphasen erhalten der Endpunkt ersetzt den ganzen Werte-Satz, ein blosses Kopieren des Entwurfs hätte dort Beträge, Sparraten und Bezüge gelöscht (durch Test abgesichert). Phasen, in denen der Wert schon stimmt, werden übersprungen. Kein neuer Schreibpfad: ein PUT je Zielphase über den bestehenden Endpunkt, alle in einer Bearbeitungssitzung und damit in einer Nebenversion. (2) ElementYearPoint führt neu ein Feld rate mit additiv, es wird nur durchgereicht, was die Rechnung ohnehin benutzt; die 43 Golden Tests laufen unverändert. Die Verlaufsgrafik der Element-Detailansicht zeigt die Rate damit auf einer zweiten Y-Achse rechts in Prozent, als Stufenlinie (innerhalb einer Phase konstant, Sprung an der Phasengrenze). Neues Modul ratefields.ts; 17 Tests ergänzt (164 → 181). Keine DB- oder API-Änderung.
0.19 2026-07-19 Claude (Opus 4.8) Versionierung und Änderungshistorie je Szenario (neues Kapitel 3.8). Jedes Szenario trägt eine Version A.B: B entsteht automatisch, A manuell mit Pflichtkommentar. Der zentrale Entwurfsentscheid: FPT hat keinen Speichern-Knopf jede Änderung schreibt sofort, ein Assistenten-Durchlauf macht ~14 Schreibvorgänge, ein Verteil-Klick einen je Zielelement. Eine Version je Schreibvorgang wäre ein Tastenprotokoll gewesen; stattdessen werden alle Schreibvorgänge innerhalb von 10 Minuten zu einer Nebenversion zusammengefasst, inhaltlich unveränderte Stände erzeugen gar keine, und verschiedene Benutzer laufen nie in einer Version zusammen. Eine Version hält den vollständigen Zustand als JSON in der Form PlanInput dadurch ist die Versionsauswahl in allen vier Analysewerkzeugen (Grafiken, Live-Simulation, Monte-Carlo, Einflussfaktoren) fast kostenlos; bei Monte-Carlo je Szenario einzeln, weil dort mehrere gleichzeitig laufen. Wiederherstellen ist ungefährlich gebaut: Es legt den zurückgesetzten Stand selbst als neue Version an («Wiederhergestellt aus A.B»), löscht also nichts, und erhält die IDs von Phasen und Elementen sonst verlören alle Kind-Szenarien ihre Diff-Basis und zeigten schlagartig alles als «neu». Wo ein Bezug trotzdem bricht (der alte Stand kannte das Element noch nicht), warnt der Dialog vorher namentlich. Die destruktive Logik liegt als reine Funktion planRestore vor und ist dort getestet; versioning-db.ts führt sie nur aus. Ein statischer Wächter-Test liest alle Route-Dateien und verlangt, dass jeder schreibende Endpunkt eine Version auslöst eine vergessene Stelle wäre eine stille Lücke. Neue Tabelle ScenarioVersion + Scenario.currentMajor (Migration gegen echtes Postgres verifiziert), neue Endpunkte unter /api/scenarios/<id>/versions. Neue Kapitel 3.8 und 9.28; 25 Tests ergänzt (139 → 164).
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).
0.14 2026-07-18 Claude (Opus 4.8) Phasenkopf überarbeitet und zwei Verteil-Werkzeuge. (1) Zweizeilige Wertdarstellung: Im Anzeigemodus «Beide» steht der Realwert neu in Klammern in einer eigenen Zeile unter dem nominalen Wert statt daneben im Phasenkopf und in den Matrix-Zellen. Der Pfeil wiederholt sich auf der zweiten Zeile, damit der Bezug Start → Ende erhalten bleibt. Nebeneffekt: Die Spalten werden schmaler, wodurch jede Kennzahl umbruchfrei (whitespace-nowrap) dargestellt werden kann. (2) Die Kennzahl heisst korrekt «Sparquote» bzw. «Verzehrquote» statt «Quote»/«Verzehr». (3) Neuer Block «Verfügbares Kapital» im Phasenkopf (ab Phase 2, nur wenn > 0): der beim Übergang zugeflossene Topf mit «davon verteilt» und «Rest auf Cash». (4) Zwei neue Werkzeuge als eigene Popups: «Kapital verteilen» (Zusatzeinlagen in PK/3a/Vermögen, Sonderamortisation, Sofort-Tilgung) und «Sparquote/Bezug verteilen» (jährliche Raten), beide mit Live-Vorschau über eine erneute computePlan-Rechnung im Browser die angezeigte Wirkung ist dadurch per Konstruktion exakt die spätere, inklusive aller Kappungen. Der Quoten-Dialog weist neben erstem und letztem Jahr die absolute Quote über die ganze Phase aus und warnt, wenn die Quote über die Phase sinkt. Neues reines Modul distribution.ts. Neue Kapitel 3.6.9, 3.6.10, 9.25; 3.6.1 und 3.6.3 überarbeitet. 8 Tests ergänzt (103 → 111). Keine Änderung an Berechnung, Datenmodell oder API beide Werkzeuge schreiben ausschliesslich bestehende Felder über bestehende Endpunkte.
0.13 2026-07-18 Claude (Fable 5) UI-Gesamtumbau rein an der Oberfläche, Berechnung, Datenmodell und API-Semantik unverändert. (A) Fundament: durchgehende Du-Form und echte Umlaute in allen sichtbaren Texten (inkl. API-Fehlermeldungen); neue UI-Primitiven in ui.tsx (Button, Modal mit ESC/Fokus-Falle/Animation, Bestätigungs-Dialog statt window.confirm, Toasts statt alert, Skeleton-Loader, EmptyState); eigene Attention-Farbe (Amber) für offene Entscheide, getrennt vom Akzent; Micro-Interactions mit prefers-reduced-motion-Fallback. (B) Onboarding (Roadmap Nr. 10): geführter Plan-Assistent in fünf Schritten (reine Orchestrierung bestehender Endpunkte, Einkommen bewusst pro Person räumt die 9.9-Falle aus), Beispielplan mit einem Klick (Übergänge absichtlich offen die Ampel lehrt sich selbst), interaktive Tour über die Planansicht, abgeleitete «Nächste Schritte»-Karte. (C) Struktur: Einzel-Bearbeitungen laufen neu über ein rechtes Inspector-Panel statt Modals (Matrix bleibt sichtbar; Klick auf andere Zelle wechselt den Inhalt); Phasenkopf entschlackt auf vier Kern-Infos (Rest wohnt in der Detailansicht aus 0.11); Matrix mit eigenem Scrollbereich und beidachsig fixierten Köpfen; Sidebar-Gruppen «Meine Pläne»/«Wissen» («So rechnet FPT», Systemparameter); Terminologie-Fix «Szenario-Profil» statt «Plan-Einstellungen»; Aktions-Icons auch ohne Hover sichtbar (Touch). (D) Extras: Sparklines je Element-Zeile (aus den 0.11-Verlaufswerten, keine Neuberechnung), Befehls-Palette (Ctrl/Cmd+K), Ruin-Banner verlinkt auf die Einflussfaktoren. Neue Kapitel 3.2.8, 3.7.63.7.9, 9.23, 9.24; 9.17 bereinigt (der Selection-Rest und der ProfileMenu-Lint-Fehler sind behoben npm run lint ist erstmals fehlerfrei). Testbestand unverändert 103.
0.12 2026-07-18 Claude (Opus 4.8) Lesbarkeit der Wasserfälle, Verkaufspreis-Abgleich und Erklärung wirkungsloser Tornado-Treiber. (1) Die beiden Wasserfälle werden nicht mehr mit Recharts gezeichnet, sondern als eigene liegende Darstellung: Verbindungslinien zwischen den Balken, Wertbeschriftung an jedem Schritt, Abschnitts-Überschriften („Am Übergang" / „Innerhalb der Phase") und eine aufklappbare Tabelle mit laufendem Zwischenstand. Anlass war, dass die bisherige Darstellung faktisch nicht lesbar war die Zahlen waren korrekt, die Grafik nicht. (2) Der Restposten beider Brücken wird bei Abweichung neu als Fehlermeldung ausgewiesen statt als beiläufige „Rundungsdifferenz"; eine nicht aufgehende Zerlegung ist ein Rechenfehler und kein Schönheitsproblem. (3) Verkaufspreis einer Immobilie wird beim Wechsel auf „Verkaufen" neu mit dem modellierten Verkehrswert vorbelegt; der Dialog weist Verkehrswert und Abweichung aus und warnt ab 10 % Differenz (Kap. 3.5.8, 9.22). Damit fällt auf, wenn angenommene Wertsteigerung und erwarteter Verkaufspreis nicht zusammenpassen. (4) Der Tornado erklärt neu Nullbalken statt sie stumm zu zeigen insbesondere den Fall, dass die Immobilien-Wertsteigerung bei einem Verkauf nachweislich wirkungslos ist (ineffectiveReason, Kap. 4.13.5). Neue Kapitel 3.5.8, 4.13.5, 9.22; 11 Tests ergänzt (92 → 103), darunter die Invariante residual === 0 über sieben Plankonstellationen. Keine DB-Änderung, keine Änderung an der Berechnung.
0.11 2026-07-18 Claude (Opus 4.8) Detailansichten (Roadmap Nr. 43) und vollständige Offenlegung der Berechnungslogiken (Roadmap Nr. 41). (1) Neue Systemparameter-Ansicht in der Seitenleiste: alle fest hinterlegten Grössen mit Wert, Bedeutung, Herleitung, Quelle und Stand als strukturierte Daten aus constants.ts, also aus derselben Quelle, aus der gerechnet wird. (2) Nur-Lese-Detailansicht je Element und je Lebensphase über ein Expand-Icon: Element mit Verlaufsgrafik über alle Planjahre (dafür führt computePlan neu ElementPhaseComputed.yearly je Element mit), Phase mit Vermögensaufteilung und zwei Wasserfällen. (3) Die Wasserfälle sind bewusst getrennt: Der Vermögens-Wasserfall zeigt nur echte Zu- und Abgänge (Quote, Kapitalerträge, Wertsteigerung, PK-Beiträge, Steuern, Verrentung, Einmalposten); Sparraten, Amortisationen und Investitionen sind Umbuchungen und erscheinen ausschliesslich im Cash-Wasserfall als Vermögensabgang gezeichnet würden sie einen Verlust vortäuschen, den es nicht gibt. Neue Strukturen WealthBridge / CashBridge inkl. Restposten als Kontrollgrösse. (4) Rechenweg-Protokoll: computePlan(plan, sample?, { explain }) protokolliert die Schritte, die es ohnehin ausführt Formel, eingesetzte Zahlen, Ergebnis und Hinweis auf geltende Vereinfachungen. Abdeckung über alle Ebenen (Element je Phase, Element je Übergang, Phasen-Kennzahlen, Plan-Ebene). Standardmässig aus, damit die Monte-Carlo-Simulation unberührt bleibt. Jeder Rechenweg verlinkt in das passende Kapitel dieser Spezifikation; ein Test prüft, dass alle Verweise eine existierende Überschrift treffen. Neue Kapitel 3.6.7, 3.6.8, 4.14, 9.20, 9.21; 12 Tests ergänzt (80 → 92). Keine DB-Änderung; die 43 Golden Tests laufen unverändert.
0.10 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 PlanScenario (IDs erhalten), planIdscenarioId 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.
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.
0.3 2026-07-16 Claude (Opus 4.8) Einmalige Sonderein-/ausgaben umgesetzt (Roadmap Nr. 1). Der Cash-Übergang zwischen zwei Phasen ist neu ein eigener Entscheid: 1:1 übernehmen · einmaliger Zufluss · einmalige Kosten · beides. Beträge gehen direkt aufs Cash-Konto. Zufluss wird nominal erfasst (real angezeigt) mit optionalem Steuersatz (Default 0 %), Kosten werden real erfasst (nominal angezeigt); beide bleiben aus der Sparquote heraus. Der Entscheid startet unbeantwortet und zählt im „offen"-Badge mit. Neue Spalte Phase.cashTransition (JSON) + Migration, neue Route PUT /api/phases/<id>/cash-transition, eigene Kennzahlen im Phasenkopf. Fünf Regressionstests ergänzt (13 → 18). Neue Kapitel 3.5.5, 4.9.6; Abschnitt 9 um zwei Grenzen ergänzt.
0.2 2026-07-16 Claude (Opus 4.8) Drei Fixes umgesetzt und dokumentiert: (1) Phase.inflationRate ersatzlos entfernt (DB-Migration, API, Typen, UI) die Inflation liegt seit V5 plan-weit; das Feld war wirkungslos. (2) Amortisation und Tilgung stoppen neu, sobald Hypothek bzw. Schuld abbezahlt sind sie belasten danach weder Cash noch Sparquote (Kap. 4.6.5, 4.6.7, 4.7, 4.8). (3) Kapitalbezugssteuer greift neu auch bei PK-/3a-Vorbezügen vor der Pensionierung, inkl. Steuerfeld und Netto-Vorschau im UI (Kap. 3.5.2, 4.9.1, 4.9.2). Kennzahl plannedSaveRate ist neu die Rate des ersten Phasenjahres. Drei Regressionstests ergänzt (10 → 13). Abschnitt 9 neu nummeriert (erledigte Punkte entfernt, „Sparraten werden nicht indexiert" ergänzt).
0.1 2026-07-16 Claude (Opus 4.8) Erstfassung. Vollständige Neuerstellung aus dem Code (Stand f768e01). Ersetzt die bisherigen FDD/TDD-Dokumente v1v5 vollständig.

Pflegehinweis: Dieses Dokument ist ein lebendes Dokument. Bei jeder Aktualisierung wird (a) die Änderungshistorie um eine Zeile ergänzt, (b) die Versionsnummer im Kopf hochgezählt und (c) der referenzierte Codestand (Commit) aktualisiert.

Die Versionsnummer ist ein Zähler nach dem Punkt, keine Dezimalzahl: auf 0.9 folgt 0.10, darauf 0.11 und so weiter. Ein Sprung auf 1.0 ist damit dem Erreichen eines echten Meilensteins vorbehalten und passiert nicht als Nebeneffekt des Hochzählens.


Inhaltsverzeichnis

  1. Einleitung und Abgrenzung
  2. Fachliche Grundkonzepte
  3. Funktionale Spezifikation
  4. Berechnungsmodell
  5. Technische Spezifikation
  6. API-Referenz
  7. Betrieb und Deployment
  8. Qualitätssicherung
  9. Bekannte Einschränkungen und Modellentscheide
  10. Glossar

1. Einleitung und Abgrenzung

1.1 Zweck des Tools

Das FPT ist eine Webanwendung zur persönlichen Finanzplanung über Lebensabschnitte hinweg, ausgelegt auf Schweizer Verhältnisse (AHV, Pensionskasse, Säule 3a, Grundstückgewinnsteuer). Ein Benutzer bildet seine finanzielle Situation als Kette von Lebensphasen ab, hinterlegt finanzielle Elemente (Einkommen, Ausgaben, Vorsorge, Immobilien, Vermögen, Schulden) und trifft an den Übergängen zwischen den Phasen bewusste Entscheide (halten, verkaufen, Kapital beziehen, verrenten). Das Tool rechnet daraus Jahr für Jahr das Vermögen, den Cash-Bestand, Spar- bzw. Verzehrquoten sowie ein allfälliges Ruinalter fort.

1.2 Was das Tool nicht ist

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 (Begründung: 9.14).
  • Keine Nebenkosten, kein Eigenmietwert, keine Mieteinnahmen bei Immobilien (siehe 9.3).
  • Keine automatische Deckung von Liquiditätslücken. Negatives Cash wird gemeldet, aber nicht korrigiert (siehe 9.1).
  • 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, 4.6.5, 4.12); die Abgrenzung war versehentlich stehen geblieben.

Die deterministische Rechnung bleibt der Kern des Tools; Stochastik kommt ausschliesslich in der Monte-Carlo-Simulation (4.12) und als reine Was-wäre-wenn- Rechnung in der Sensitivitätsanalyse (4.13) 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 eigenes Grundprofil: Haushaltsform, Personen (Alter, Pensionsalter, Name) und Inflationsannahme. Es gibt keine übergeordnete Haushalts-Entität mehr. Ein Szenario ist deshalb eine vollständige Deep-Copy und vom Ursprungsplan unabhängig veränderbar.

Referenz: prisma/schema.prisma Zeilen 810, src/lib/types.ts Zeilen 3849.


2. Fachliche Grundkonzepte

2.1 Die Ebenen

User
 └── Plan                         (Behälter: nur Name — KEINE Finanzdaten)
      └── Scenario[]              (die berechenbare Einheit)
           │  Grundprofil: Haushaltsform, Personen, Inflation, Cash-Startwert
           │  isBase = genau eines je Plan · parentScenarioId = Baum + Vergleichsbasis
           ├── Person[]           (1 bei SINGLE, 2 bei COUPLE — je Szenario eigen!)
           ├── Phase[]            (Kette 1..n; sourcePhaseId = Gegenstück in der Vorlage)
           └── FinancialElement[] (szenario-weit; sourceElementId = Gegenstück in der Vorlage)
                ├── ElementPhaseValue[]      (Werte je Phase, JSON)
                └── ElementTransitionValue[] (Entscheide je Übergang, JSON)

Zwei Designprinzipien tragen dieses Modell:

(1) Ein finanzielles Element ist über alle Lebensphasen hinweg dieselbe Entität. Eine Pensionskasse „PK Arbeitgeber" existiert einmal pro Szenario; sie hat pro Phase einen Werte-Satz und pro Phasenübergang einen Entscheid-Satz. Dadurch bleibt die Identität eines Vermögensgegenstands über die Zeit erhalten Voraussetzung für die Fortschreibung (Carry) und die Vermögensaufteilungs-Grafik.

(2) Das Grundprofil liegt am Szenario, nicht am Plan (V6). Nur so lassen sich die wertvollsten Szenario-Fragen abbilden allen voran ein abweichendes Pensionsalter („Was, wenn ich mit 62 statt 65 aufhöre?"), das in Person steckt. Wäre das Profil geteilt, wären Frühpensionierungs-Szenarien unmöglich.

Der Plan ist damit ein reiner Behälter: Er bündelt Szenarien und trägt den Eigentümer (userId). Ownership von Szenario/Phase/Element läuft über die Kette Scenario → Plan → User.

Referenz: prisma/schema.prisma.

2.2 Die Matrix als Leitmetapher

Die Hauptansicht ist eine Tabelle:

  • Zeilen = finanzielle Elemente, gruppiert nach Kategorie; zuoberst die systemseitige, read-only Zeile Cash.
  • Spalten = abwechselnd Phasenspalten und Übergangsspalten (Phase 1 → Übergang → Phase 2 → Übergang → Phase 3 …). Der letzten Phase folgt keine Übergangsspalte.
  • Zellen = anklickbar; Phasenzellen öffnen die Werte-Eingabe, Übergangszellen den Entscheid-Dialog.

Referenz: src/components/PlanView.tsx Zeilen 111120 (Spaltenaufbau), 333477 (Matrix).

2.3 Phasentyp abgeleitet, nicht gespeichert

Der Typ einer Phase wird nie gespeichert, sondern in jeder Berechnung aus Alter und Pensionsalter der Personen abgeleitet:

Bedingung (zu Phasenbeginn) Typ
alle Personen startAge < retirementAge ERWERB
alle Personen startAge >= retirementAge PENSION
gemischt MIXED

Referenz: src/lib/calculations.ts Zeilen 158172; Kommentar in prisma/schema.prisma Zeilen 9495.

2.4 Cash als Ausgleichskonto

Cash ist kein vom Benutzer erfassbares Element, sondern das systemseitige Ausgleichskonto:

  • Es startet mit Plan.initialCash (Phase 1).
  • Es nimmt jährlich die Spar-/Verzehrquote (Einkommen nominale Ausgaben) auf.
  • Es finanziert die geplanten Sparraten (3a-Beiträge, Sparbeiträge, Amortisationen, Tilgungen).
  • Es empfängt die Bezugsraten aus Sonstigem Vermögen.
  • Es empfängt an Übergängen Kapitalzuflüsse (Verkäufe, PK-/3a-Bezüge) und finanziert Sofort-Tilgungen sowie Zusatzinvestitionen der Folgephase.
  • Es nimmt an Übergängen einmalige Sonderein-/ausgaben auf (Erbschaft, Poolbau, Autokauf) siehe 3.5.5.
  • Es darf negativ werden dies ist die Definition einer Liquiditätslücke und wird rot markiert, aber nicht automatisch korrigiert.

Cash ist die einzige Zeile der Matrix ohne FinancialElement-Datensatz. Zwei Zellen sind dennoch bearbeitbar: die erste Phasenzelle (Cash-Anfangswert) und jede Übergangszelle (einmalige Sonderein-/ausgaben).

Referenz: src/lib/calculations.ts (Jahresschleife und Übergang).


3. Funktionale Spezifikation

3.1 Authentifizierung und Benutzerkonto

3.1.1 Registrierung

  • Öffentlich zugänglich (kein Einladungscode, keine Freischaltung).
  • Benutzername: Muster ^[a-zA-Z0-9._-]{3,32}$ 3 bis 32 Zeichen, Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich. Muss plan-übergreifend eindeutig sein (User.username @unique).
  • Passwort: mindestens 6 Zeichen; wird mit bcrypt (Cost-Faktor 12) gehasht.
  • Bei Erfolg wird sofort eine Session gesetzt (Login inbegriffen), HTTP 201.
  • Bei belegtem Benutzernamen: HTTP 409 mit Meldung „Dieser Benutzername ist bereits vergeben."

Referenz: src/lib/users.ts Zeilen 622, src/app/api/auth/register/route.ts.

3.1.2 Anmeldung

  • Benutzername + Passwort; Prüfung via bcrypt.compare.
  • Fehlermeldung ist bewusst unspezifisch: „Benutzername oder Passwort falsch." (HTTP 401) verrät nicht, ob der Benutzer existiert.
  • Bei Erfolg: JWT (HS256, Payload { userId }, Gültigkeit 30 Tage) im HttpOnly-Cookie fpt_session (sameSite=lax, secure nur in Produktion, maxAge 30 Tage).

Referenz: src/app/api/auth/login/route.ts, src/lib/auth.ts.

3.1.3 Abmeldung

POST /api/auth/logout löscht das Cookie. Da die Session zustandslos ist (JWT), bleibt ein bereits kopiertes Token bis zum Ablauf technisch gültig es gibt keine serverseitige Token-Sperrliste.

3.1.4 Passwortänderung

Im Profilmenü. Erfordert das aktuelle Passwort; das neue Passwort muss ≥ 6 Zeichen haben und wird im Dialog gegen eine Wiederholung geprüft (Client-seitig). Nach Erfolg erscheint 1.2 s lang „Passwort geändert.", dann schliesst der Dialog.

Referenz: src/app/api/auth/change-password/route.ts, src/components/ProfileMenu.tsx Zeilen 117140.

3.1.5 Zugriffsschutz

Zweistufig:

  1. Middleware (src/middleware.ts, Edge-Runtime): schützt alle Pfade ausser /login, /api/auth/login, /api/auth/register, /_next/*, /favicon*. Ohne gültiges Token → API-Aufrufe erhalten HTTP 401, Seitenaufrufe werden nach /login?next=<pfad> umgeleitet.
  2. Ownership-Check in jeder API-Route: getCurrentUserId() plus eine Abfrage, die den Datensatz nur zurückgibt, wenn er dem Benutzer gehört (getOwnedPlan, getOwnedPhase, getOwnedElement in src/lib/queries.ts). Ein fremder Datensatz führt zu HTTP 404 (nicht 403) die Existenz wird nicht preisgegeben.

3.2 Plan-Verwaltung

3.2.1 Plan erstellen

Der «+»-Knopf öffnet eine Auswahl mit drei Wegen:

Weg Für wen Was passiert
Geführt erstellen (empfohlen) neue Nutzer der Assistent aus 3.2.8
Leer starten geübte Nutzer der bisherige Dialog (unten) nur Grundprofil, keine Phasen/Elemente
Beispielplan ansehen Erkunden legt einen fiktiven, voll ausgefüllten Plan an (3.2.8)

Der Dialog «Leer starten» fragt Name plus das vollständige Grundprofil:

Feld Typ Default Wertebereich
Name des Plans Text „Basisplan" 1120 Zeichen
Haushaltsform Auswahl SINGLE SINGLE / COUPLE
Name je Person Text (optional) leer ≤ 60 Zeichen
Aktuelles Alter Zahl 35 0120
Pensionierungsalter Zahl 65 30100
Erwartete Inflationsrate (%) Zahl 1.5 20 bis 50

Konsistenzregel: SINGLE erfordert genau eine Person, COUPLE genau zwei (Person A und B). Verletzung → HTTP 400 mit Klartextmeldung.

Referenz: src/components/PlanProfileFields.tsx Zeilen 10121018, src/app/api/plans/route.ts Zeilen 635.

Ein neu erstellter Plan hat keine Phasen und keine Elemente; initialCash ist 0.

3.2.2 Grundprofil ändern

Über „Einstellungen" in der Planansicht. Wichtig: Die Personen werden serverseitig in einer Transaktion gelöscht und neu angelegt (deleteMany + create). Die Person-IDs ändern sich dadurch. Da Elemente über ownerRole (nicht über personId) zugeordnet sind, bleibt die Zuordnung erhalten.

Ein Wechsel von COUPLE auf SINGLE entfernt Person B. Elemente mit ownerRole = PERSON_B bleiben in der Datenbank bestehen, finden aber keinen Owner mehr siehe 9.2.

Referenz: src/app/api/plans/[planId]/route.ts Zeilen 5570.

3.2.3 Cash-Anfangswert

Klick auf die erste Zelle der Cash-Zeile öffnet den Dialog „Cash-Anfangswert". Wertebereich 0 bis 1'000'000'000, wird auf ganze Franken gerundet.

Referenz: src/components/PlanView.tsx Zeilen 11301156, src/app/api/plans/[planId]/route.ts Zeile 36.

3.2.4 Plan löschen

Aus der Übersichtskachel oder der Planansicht, mit Browser-confirm(). Löscht per Datenbank-Cascade Personen, Phasen, Elemente und alle Werte. Szenarien, die auf diesen Plan als parentPlanId zeigen, werden nicht gelöscht ihre parentPlanId wird auf NULL gesetzt (onDelete: SetNull).

Referenz: prisma/schema.prisma Zeile 82.

3.2.5 Szenarien

Beim Anlegen eines Plans entsteht automatisch das Basisszenario (isBase = true, Name „Basisszenario"). Der beim Anlegen erfasste Profilteil (Haushaltsform, Personen, Inflation) landet dort, der Name am Plan.

Ein weiteres Szenario ist eine vollständige Kopie eines beliebigen bestehenden Szenarios (nicht nur des Basisszenarios). Kopiert werden Grundprofil, alle Personen, alle Phasen, alle Elemente sowie sämtliche Phasen- und Übergangswerte.

Gesetzt werden dabei:

  • parentScenarioId = das kopierte Szenario → ergibt den Baum in der Seitenleiste und die Vergleichsbasis für die Abweichungs-Markierung
  • sourcePhaseId / sourceElementId je kopierter Phase bzw. Element → die Identität, über die der Diff „dieselbe Zelle" wiederfindet

Da jede Kopie wieder kopierbar ist, entstehen Sub-Szenarien beliebiger Tiefe; die Seitenleiste rückt sie entsprechend ein.

Löschen: Ein Szenario lässt sich löschen, das Basisszenario nicht (dafür den ganzen Plan löschen). Das Löschen eines Plans entfernt per Cascade alle seine Szenarien.

Referenz: src/app/api/scenarios/[scenarioId]/copy/route.ts.

3.2.6 Abweichungs-Markierung (Diff)

Jedes Szenario ausser dem Basisszenario wird live gegen sein Eltern-Szenario verglichen. Abweichende Stellen sind in der Matrix farblich markiert:

Zustand Darstellung
geändert gelb hinterlegt (--diff / --diff-soft)
neu (in der Vorlage nicht vorhanden) grün hinterlegt + Badge „neu" (--diff-added)
entfernt (in der Vorlage vorhanden, hier gelöscht) graue Geisterzeile, durchgestrichen (--diff-removed)

Markiert werden: Phasenzellen, Übergangszellen, Element-Zeilen (Name/Zuordnung), Phasenköpfe (Name/Dauer), der Cash-Anfangswert, Cash-Übergänge und das Grundprofil-Banner. Über der Matrix steht die Gesamtzahl der Abweichungen.

Die Farben sind semantische Tokens und für Hell, Dunkel und Warm getrennt abgestimmt ein fester Gelbwert würde im Dunkelschema unbrauchbar aussehen.

Zwei bewusste Eigenschaften:

  • Der Vergleich läuft gegen das direkte Eltern-Szenario, nicht immer gegen die Basis. Bei einem Sub-Szenario ist das genau „was habe ich gegenüber der Vorlage geändert".
  • Der Diff ist live: Ändert man die Vorlage, verschiebt sich die Markierung im Kind rückwirkend (siehe 9.16).

Referenz: src/lib/diff.ts.

3.2.7 Planstart (Kalenderjahr)

Das Grundprofil enthält das Feld Planstart (Jahr) das Kalenderjahr, in dem Jahr 1 der Planung liegt (Scenario.startYear). Es dient ausschliesslich der Darstellung: Zeitachse und Grafiken beschriften damit Jahre statt nur Alter. Die Berechnung rechnet unverändert in relativen Jahren ab Planbeginn startYear fliesst in keine Formel ein.

Beim Anlegen wird das laufende Jahr vorbelegt; bestehende Szenarien wurden per Migration darauf gesetzt. Kalenderjahr eines Planjahrs: startYear + (Jahr 1).

3.2.8 Geführter Assistent und Beispielplan

Der Plan-Assistent (Roadmap Nr. 10: «Schritt für Schritt statt leerer Matrix») fragt in fünf Schritten in Alltagssprache: (1) Grundprofil, (2) Lebensphasen, (3) Einkommen und Ausgaben plus Kontostand, (4) Vorsorge und Vermögen, (5) Zusammenfassung.

Schritt 2 ist an den fixen Pensionierungszeitpunkten ausgerichtet. Das Pensionsalter jeder Person ist ein Fixpunkt auf der Lebenslinie; dazwischen entstehen Abschnitte mit konstantem Erwerbsstatus (reines Modul phaseplan.ts, planSegments):

Abschnitt Bedeutung Länge
Erwerb alle arbeiten fest (bis zur ersten Pensionierung)
Misch eine Person pensioniert, eine arbeitet fest (zwischen den Pensionierungen)
Pension alle pensioniert offen (Lebensdauer frei)

Die Anzahl Abschnitte wird abgeleitet, nicht vorgegeben: Einzelplan → 2 (Erwerb, Pension); Paar mit gleichem Pensionsalter → 2 (keine Mischzeit); Paar mit unterschiedlichem Pensionsalter → 3. Ist eine Person bei Planbeginn bereits pensioniert, beginnt die Linie mit einem Misch- oder Pensions-Abschnitt.

In jedem fest begrenzten Abschnitt verteilt der Nutzer beliebig viele Phasen (mit +/Papierkorb und eigenem Namen je Phase); eine Live-Summe erzwingt, dass die Phasendauern exakt der festen Länge entsprechen «Weiter» bleibt gesperrt, bis es aufgeht. Das ist zwingend: Die Berechnung leitet den Phasentyp am Phasenbeginn ab und kappt jede Phase am nächsten Pensionsereignis (2.3) eine Phase, die eine Pensionierung überspannt, gäbe es nicht. Der frühere Assistent liess die Erwerbsphase beliebig über das Pensionsalter hinaus setzen; das ist damit behoben. Der offene Pensions-Abschnitt nimmt beliebige Dauern (Summe = geplante Restlebensdauer).

Eine Zeitachse zeigt den proportionalen Verlauf mit den Pensionierungs-Fixpunkten als Flaggen; die Phasen sind nummeriert und unter dem Balken beschriftet, damit auch kurze Phasen lesbar bleiben.

Schritt 4 (Vorsorge & Vermögen) ist bei Paaren in Gemeinsam / Person A / Person B aufgeteilt. Pensionskasse und Säule 3a sind immer persönlich (personengebundene Kategorien); Wertschriften, Wohneigentum und Schulden lassen sich gemeinsam oder je Person erfassen.

Zwei bewusste Entscheide bleiben:

  • Einkommen wird pro Person erfasst, nie als «Gemeinsam» in Paar-Plänen zählt Haushalts-Einkommen nicht für die AHV (9.9); der Assistent räumt diese Falle von Anfang an aus.
  • Technisch ist der Assistent reine Orchestrierung bestehender Endpunkte (Plan → Phase 1 → Elemente samt Werten → Folgephasen; die Reihenfolge stellt sicher, dass die Phasen-Route die Folgephasen korrekt vorbelegt). Kein neuer Endpunkt, keine Berechnungsänderung. Grenze: 9.23.

Der Beispielplan («Beispiel: Alex Muster», src/lib/demoplan.ts) ist ein fiktiver, voll ausgefüllter Plan per Ein-Klick ebenfalls reine Orchestrierung. Die Übergangs-Entscheide bleiben absichtlich offen: Der neue Nutzer sieht die Ampel («N offen») in Aktion und lernt das wichtigste Konzept am Beispiel statt aus einer Erklärung.

Nach dem ersten Öffnen eines Plans mit Phasen startet einmalig die Tour (3.7.8).

3.3 Lebensphasen

3.3.1 Phase anlegen

Neue Phasen werden immer am Ende der Kette angehängt (sequenceNumber = Anzahl + 1).

Automatische Dauer-Kappung: Die Dauer wird ans nächste Pensionsereignis gekappt. Formel (maxPhaseDuration): für jede Person, die zu Phasenbeginn noch erwerbstätig ist, gilt retirementAge (age + yearsBefore); das Minimum dieser Werte ist die Obergrenze. Ist keine Person mehr erwerbstätig, gibt es keine Obergrenze (null). Diese Kappung ist im Dialog sichtbar („max. N") und wird serverseitig erzwungen.

Fachliche Begründung: Eine Phase darf keine Pensionierung überspannen, weil der Phasentyp und die AHV-/PK-Renten am Phasenbeginn ausgewertet werden.

Default-Dauer: die Kappung, sonst 10 Jahre. Default-Name: Phase 1 → „Erste Lebensphase"; sonst „Pensionsphase" wenn zu Phasenbeginn mindestens eine Person pensioniert ist, sonst „Erwerbsphase".

Vorbelegung der Elemente: Beim Anlegen einer Phase wird für jedes noch aktive Element (nicht SOLD, nicht SETTLED) ein ElementPhaseValue mit den editierbaren Feldern der Vorphase erzeugt (buildCarryData):

Kategorie Übernommene Felder
INCOME, EXPENSE nur teuerungsausgleich (Basis wird live fortgeschrieben)
AHV gapYears: 0
PENSION_FUND, PILLAR_3A, OTHER_ASSET annualContribution, expectedReturn
REAL_ESTATE purchasePrice, amortization (Resthypothek wird live fortgeschrieben)
OTHER_DEBT annualRepayment

Bestände (PK-/3a-/Vermögenswert, Resthypothek, Restschuld) werden bewusst nicht als Snapshot gespeichert, sondern in jeder Berechnung live aus der Vorphase fortgeschrieben. Damit wirken sich nachträgliche Änderungen an frühen Phasen automatisch auf alle Folgephasen aus.

Referenz: src/app/api/plans/[planId]/phases/route.ts.

3.3.2 Phase bearbeiten

Klick auf einen Phasenkopf öffnet ein Popup („Lebensphase: ") mit Bezeichnung und Dauer konsistent zu allen anderen Eingaben (Element-Zellen, Übergänge). Speichern schliesst das Popup. Die Dauer wird auch hier gekappt. Eine phasenspezifische Inflationsrate gibt es nicht; das Panel weist darauf hin: „Die Inflationsrate gilt plan-weit und wird in den Plan-Einstellungen gesetzt."

Referenz: src/components/PhaseDetail.tsx.

3.3.3 Phase löschen

Nur die letzte Phase kann gelöscht werden serverseitig geprüft (HTTP 400: „Nur die letzte Phase kann gelöscht werden."). Damit bleibt die Kette der sequenceNumber lückenlos. Der Löschen-Button erscheint im Detail-Panel nur bei der letzten Phase.

Referenz: src/app/api/phases/[phaseId]/route.ts Zeilen 5577.

3.4 Finanzielle Elemente

3.4.1 Kategorien und Personenzuordnung

Kategorie Label Zuordnung
INCOME Einkommen frei: Gemeinsam / Person A / Person B
EXPENSE Ausgaben frei
AHV AHV zwingend genau eine Person
PENSION_FUND Pensionskasse zwingend genau eine Person
PILLAR_3A Säule 3a zwingend genau eine Person
REAL_ESTATE Immobilie frei
OTHER_ASSET Sonstiges Vermögen frei
OTHER_DEBT Sonstige Schulden frei

Die Vorsorge-Kategorien (PERSON_ONLY_CATEGORIES) sind zwingend personengebunden, weil AHV-Rente, PK-Verrentung und 3a-Bezug am individuellen Pensionierungszeitpunkt hängen. Wird für diese Kategorien kein PERSON_A/PERSON_B übergeben → HTTP 400.

Für die übrigen Kategorien gilt: fehlt die Zuordnung, wird serverseitig HOUSEHOLD gesetzt.

Referenz: src/lib/elements.ts Zeilen 1923, src/app/api/plans/[planId]/elements/route.ts Zeilen 4354.

3.4.2 Element anlegen

Der Dialog „Finanzielles Element" fragt Kategorie, Zuordnung, Bezeichnung und direkt die Werte der ersten Lebensphase ab. Nach dem Anlegen (POST .../elements) werden die Werte per PUT .../phase/<firstPhaseId> gespeichert, sofern mindestens ein Feld gesetzt wurde. Der Bezeichnungs-Default ist das Kategorie-Label.

orderIndex = bisheriges Maximum + 1; bestimmt die Reihenfolge innerhalb der Kategoriegruppe.

Referenz: src/components/PlanView.tsx Zeilen 799947.

3.4.3 Element bearbeiten und löschen

Ein Klick auf eine Phasenzelle öffnet den Dialog „Lebensphase: " mit den kategorie- und kontextabhängigen Feldern (siehe 3.4.4) sowie dem Button „Element löschen". Löschen entfernt das Element aus allen Phasen (Browser-confirm(), dann Cascade auf ElementPhaseValue und ElementTransitionValue).

Umbenennen ist per API möglich (PATCH /api/elements/<id>), im aktuellen UI aber nicht angebunden.

3.4.4 Feldkatalog je Kategorie

Die angezeigten Felder hängen von drei Kontextgrössen ab:

  • carried ob der Basiswert aus der Vorphase fortgeschrieben wird (ab Phase 2)
  • ownerWorking ob der zugeordnete Besitzer in dieser Phase erwerbstätig ist
  • durationYears Phasendauer (begrenzt z. B. die Ausfalljahre)

Referenz: src/components/ElementDetail.tsx Zeilen 124340.

INCOME (Einkommen)

Einkommen ist der NETTOLOHN der Betrag, der nach allen Lohnabzügen (AHV/ALV, Pensionskasse, NBU) tatsächlich aufs Konto kommt. Das ist die für den Cash-Fluss richtige Grösse und die, in der man denkt.

Feld JSON Semantik
Jahreseinkommen NETTO, nominal (erstes Jahr) amount Basiswert Jahr 1. Ab Phase 2 vorbelegt mit dem fortgeschriebenen Wert, bewusst änderbar (Teilzeit, Beförderung).
≈ real (heutige Kaufkraft) Read-only Info: amount / deflatorStart
Nominale Lohnerhöhung (%/Jahr) teuerungsausgleich Default 0 %. 0 % = nominal gleichbleibend, real sinkend.

Für die AHV rechnet das Tool intern auf den Bruttolohn zurück siehe 4.4.5. Warum die Definition überhaupt nötig war: Für den Cash-Fluss sind beide Konventionen gleichwertig (brutto Ausgaben inkl. Abzügenetto Ausgaben ohne Abzüge), weshalb die Ambiguität lange folgenlos blieb. Mit der einkommensabhängigen AHV hängt daran aber eine Rente.

EXPENSE (Ausgaben)

Hinweistext: „Ausgaben werden REAL erfasst (in heutiger Kaufkraft)."

Feld JSON Semantik
Jahresausgaben REAL (erstes Jahr) amount Basiswert in heutiger Kaufkraft.
≈ nominal (in diesem Jahr) Read-only Info: amount * deflatorStart
Reale Mehrausgaben (%/Jahr) teuerungsausgleich Zusätzlich zur Inflation. 0 % = gleicher Lebensstandard.

Die Asymmetrie (Einkommen nominal, Ausgaben real) ist der Kern des V5-Modells: Man kennt seinen Lohn nominal, aber seinen Lebensstandard real.

AHV

Zustand Anzeige
Besitzer erwerbstätig Eingabefeld Geplante Ausfalljahre (gapYears), 0 bis Phasendauer. Hilfetext: „Jedes Ausfalljahr kuerzt die spaetere Rente um 1/44."
Besitzer pensioniert Nur Hinweistext die Rente wird automatisch berechnet.

PENSION_FUND (Pensionskasse)

Zustand Felder
erwerbstätig, Phase 1 Aktueller PK-Wert (currentValue), Jährliche Einzahlung (annualContribution), Erwartete Rendite (expectedReturn)
erwerbstätig, ab Phase 2 Startwert (fortgeschrieben) (read-only), Zusatzeinlage aus Kapital (additionalInvestment), Einzahlung, Rendite
pensioniert Hinweistext (Rente aus Umwandlungssatz bzw. „Vollständig bezogen")

Wichtig (Hilfetext im UI): Die PK-Einzahlung „Fliesst NICHT in die Sparquote ein (bereits in den Ausgaben beruecksichtigt)" Lohnabzüge sind im Nettoeinkommen bereits weg.

PILLAR_3A (Säule 3a)

Wie PK, aber:

  • Die Einzahlung zählt zur Sparquote (verlässt das Cash).
  • Das Feld ist auf PILLAR_3A_MAX_ANNUAL = 7'258 CHF (2026, mit PK) hart geklammert.
  • Bei Pensionierung: „Die Saeule 3a wird beim Pensions-Uebergang vollstaendig bezogen."

REAL_ESTATE (Immobilie)

Feld JSON Semantik
Kaufpreis purchasePrice nur Phase 1 / Neukauf; ab Phase 2 read-only fortgeschrieben
Hypothek mortgage dito
Startwert Netto (fortgeschrieben) read-only ab Phase 2: Verkehrswert Resthypothek
Resthypothek (fortgeschrieben) read-only ab Phase 2
Amortisation (CHF/Jahr) amortization endet, sobald die Hypothek abbezahlt ist
Hypothekarzins (%/Jahr) interestRate Zinssatz auf der Restschuld
Hypothekarzins-Betrag (Beginn → Ende) read-only: Zinsbetrag im ersten und im letzten Jahr der Phase
Geschätzte Wertsteigerung (%/Jahr) valueGrowth wirkt auf die Liegenschaft
Sind die Zinsen bereits in den Ausgaben enthalten? interestHandling INCLUDED (Default) / ADD

Der Zinsbetrag sinkt automatisch mit der Amortisation das kann kein manueller Ausgabenposten. Beispiel: Hypothek 1'000'000, Amortisation 10'000/Jahr, Zins 1 %, 10 Jahre → Anzeige 10'000 → 9'100 (Jahr 1 auf 1'000'000, Jahr 10 auf 910'000).

Der Schalter interestHandling verhindert die Doppelzählung: Bestehende Pläne haben die Zinsen im Ausgabenbetrag, deshalb ist INCLUDED der Default und das Tool zieht nichts ab. Erst ADD rechnet die Zinsen dazu dann gehören sie aus dem Ausgabenbetrag entfernt. Der Schalter sitzt bewusst auf der Immobilie und nicht auf dem Ausgaben-Element: Bei mehreren Ausgaben-Elementen wäre sonst unklar, welches die Zinsen trägt (und zwei auf „Ja" würden doppelt zählen).

OTHER_ASSET (Sonstiges Vermögen)

Feld JSON Semantik
Startwert / Startwert (fortgeschrieben) startValue ab Phase 2 read-only
Zusatzinvestition aus Kapital additionalInvestment nur ab Phase 2
Erwartete Rendite (%/Jahr) expectedReturn
Jährlicher Sparbeitrag annualContribution fliesst ins Vermögen, verlässt das Cash → geplante Sparrate
Jährliche Bezugsrate annualWithdrawal Entnahme aus dem Vermögen ins Cash → geplante Verzehrrate

Die Bezugsrate ist das Instrument für den laufenden Kapitalverzehr innerhalb einer Phase. Für einen einmaligen Zugriff am Übergang gibt es zusätzlich den Teilverkauf (3.5.2, 4.9.3).

OTHER_DEBT (Sonstige Schulden)

Feld JSON Semantik
Restschuld / (fortgeschrieben) startValue ab Phase 2 read-only
Jährliche Tilgung annualRepayment verlässt das Cash → geplante Sparrate

Schulden gehen mit negativem Vorzeichen ins Vermögen ein.

3.5 Übergänge

3.5.1 Konzept

Zwischen zwei Phasen liegt ein Übergang. Er ist der Ort, an dem einmalige Entscheide getroffen werden. Fünf Kategorien haben Übergangs-Entscheide (TRANSITION_CATEGORIES): PENSION_FUND, PILLAR_3A, REAL_ESTATE, OTHER_ASSET, OTHER_DEBT. Dazu kommt der Cash-Entscheid (siehe 3.5.5), der an jedem Übergang zu treffen ist.

Für INCOME, EXPENSE und AHV erscheint: „Fuer diese Kategorie gibt es im Uebergang keine Eingaben."

3.5.2 Normaler Übergang vs. Pensions-Übergang

Ein Übergang ist für ein Element ein Pensions-Übergang, wenn dessen Besitzer in der Von-Phase erwerbstätig und in der Ziel-Phase pensioniert ist (isRetirementTransition). Nur personenzugeordnete Elemente (nicht HOUSEHOLD) können das auslösen.

Kategorie Normaler Übergang (Vorbezug) Pensions-Übergang
PENSION_FUND Bezug? Kein Bezug / Bezug + Bruttobetrag (max. Endwert) + Kapitalbezugssteuer (%) Bezugsart: Rente / Kapitalbezug / Kombination
PILLAR_3A Bezug? Kein Bezug / Bezug + Bruttobetrag + Kapitalbezugssteuer (%) Nur Kapitalbezugssteuer (%) Bezug ist zwingend vollständig
REAL_ESTATE Halten (+ optionale Sonderamortisation) / Verkaufen (+ Verkaufspreis, Grundstückgewinnsteuer) identisch
OTHER_ASSET Halten / Verkaufen / Teilverkauf (+ Betrag) identisch
OTHER_DEBT Sofortige Tilgung (CHF) identisch

Ein Vorbezug (vor der Pensionierung, z. B. für Wohneigentum oder Selbstständigkeit) ist wie der Bezug bei Pensionierung kapitalbezugssteuerpflichtig. Der eingegebene Betrag ist der Bruttobezug: er wird in voller Höhe dem Vorsorgekapital entnommen, ins Cash fliesst der Betrag nach Abzug der Steuer. Der Dialog zeigt die resultierende Netto-Auszahlung als read-only Vorschau an.

Bei PK-Bezugsart:

  • Rente: Feld Umwandlungssatz (Default 6 %)
  • Kapitalbezug: Feld Kapitalbezugssteuer (Default 8 %)
  • Kombination: beide Felder plus „Davon Kapitalbezug (CHF)", geklammert am Endwert

Referenz: src/components/ElementDetail.tsx Zeilen 342462.

3.5.3 Ampel-Logik: „offene" Entscheide

Ein Entscheid gilt als beantwortet (isTransitionAnswered), wenn das jeweilige Entscheidungsfeld gesetzt ist:

Kategorie Beantwortet, wenn
REAL_ESTATE, OTHER_ASSET decision gesetzt (inkl. PARTIAL)
PENSION_FUND Pensions-Übergang: payoutMode gesetzt; sonst: withdrawalMode gesetzt
PILLAR_3A Pensions-Übergang: immer beantwortet; sonst: withdrawalMode gesetzt
Cash mode gesetzt (isCashTransitionAnswered)
alle anderen immer beantwortet

Der Übergangs-Spaltenkopf zeigt entweder „N offen" (Akzentfarbe) oder „geprüft" (grün, Häkchen). Offene Zellen sind farblich hervorgehoben und zeigen „?".

Ein Element ist am Übergang inaktiv (transitionInactive), wenn es bereits verkauft/getilgt ist oder wenn es eine PK/3a ist, deren Besitzer schon zu Beginn der Von-Phase pensioniert war (dann ist bereits bezogen/verrentet). Inaktive Zellen zeigen „–" und sind nicht anklickbar.

Referenz: src/components/PlanView.tsx Zeilen 200232, src/components/ElementDetail.tsx Zeilen 107120.

3.5.4 Geführter Übergang (Review-Dialog)

Klick auf einen Übergangs-Spaltenkopf öffnet „Übergang prüfen: → ". Der Dialog listet zuoberst den Cash-Entscheid (einmalige Sonderein-/ausgaben, betrifft jeden Übergang) und darunter alle noch aktiven Elemente der Übergangs-Kategorien mit ihren Entscheidfeldern und kontextabhängigen Hinweisen:

  • PK/3a, normaler Übergang: „Hier könnten Sie optional Kapital beziehen."
  • PK, Pensionierung: „Pensionierung: Bezugsart wählen (Rente / Kapital / Kombination)."
  • 3a, Pensionierung: „Wird bei Pensionierung vollständig bezogen."

„Alle speichern" schreibt jeden Entscheid einzeln per PUT. Entscheidend: Die Formulare sind mit withTransitionDefaults vorbelegt (Halten / Kein Bezug / Rente), damit ein blosses Speichern den sichtbaren Default auch tatsächlich persistiert und die Ampel auf grün geht.

Referenz: src/components/PlanView.tsx (TransitionReviewDialog), src/components/ElementDetail.tsx (withTransitionDefaults).

3.5.5 Cash-Übergang: einmalige Sonderein-/ausgaben

Einmalige Ereignisse (Erbschaft, Poolbau, Autokauf, grössere Anschaffung) werden nicht als finanzielles Element modelliert, sondern als Entscheid auf dem Cash-Konto am Phasenübergang. Sie belasten bzw. speisen das Cash direkt.

Der Entscheid hat vier Ausprägungen (CashTransitionMode):

Modus Bedeutung Felder
NONE 1:1 übernehmen Cash läuft unverändert weiter (Default)
INFLOW Einmaliger Zufluss Bezeichnung, Betrag (nominal), Steuer (%)
OUTFLOW Einmalige Kosten Bezeichnung, Betrag (real)
BOTH Zufluss und Kosten am selben Übergang beide Feldgruppen

Erfassungs-Konventionen bewusst analog zu den laufenden Flows:

  • Zufluss: nominal erfasst, real angezeigt (wie Einkommen). Man kennt den Betrag, der effektiv aufs Konto kommt. Der Realwert erscheint read-only als Info.
  • Kosten: real erfasst, nominal angezeigt (wie Ausgaben). Man denkt „ein Pool kostet heute 20'000"; die Inflation rechnet daraus den Betrag zum Ereigniszeitpunkt. Der Nominalwert erscheint read-only als Info.
  • Steuersatz nur beim Zufluss, Default 0 % (Erbschaften an direkte Nachkommen sind in den meisten Kantonen steuerfrei). Ins Cash fliesst der Betrag nach Abzug der Steuer.

Weder Zufluss noch Kosten gehen in die Spar-/Verzehrquote. Sie sind keine laufenden Flows; eine Erbschaft von 250'000 würde die Quote zu einem sinnlosen Ausschlag treiben. Sie wirken ausschliesslich auf das Cash und damit auf Vermögensverlauf, Endvermögen und Ruinalter.

Bedienung: Klick auf eine Übergangszelle der Cash-Zeile öffnet den Dialog „Uebergang: Cash". Die Zelle zeigt 1:1, +100'000, 20'000 bzw. +100'000 / 20'000, solange offen ein ?. Der Entscheid ist Teil des geführten Übergangs (3.5.4) und zählt im „offen"-Badge mit eine neu angelegte Phase erzeugt damit automatisch einen offenen Cash-Entscheid am neuen Übergang.

Referenz: src/components/ElementDetail.tsx (CashTransitionFields), src/components/PlanView.tsx (CashTransitionDialog).

3.5.6 AHV-Prüfung am Pensions-Übergang

Die AHV-Rente hängt vom massgebenden durchschnittlichen Jahreseinkommen (mdJE) über die ganze Beitragsdauer ab. Diese Grösse kann das Tool nicht allein aus dem Plan bestimmen: Die Beitragspflicht beginnt mit 21, der Plan aber erst beim heutigen Alter. Bei einer 45-jährigen Person liegen 24 Beitragsjahre vor dem Planbeginn.

Deshalb ist die Beitragskarriere am Pensions-Übergang zu prüfen dort, wo bereits die PK-Bezugsart und der 3a-Bezug entschieden werden. Der Dialog zeigt:

Feld Art
Geplantes Durchschnittseinkommen (aus dem Plan) read-only, real
Beitragsjahre im Plan read-only
Durchschnittseinkommen vor Planbeginn (real) Eingabe nur wenn Alter bei Planbeginn > 21
Ausfalljahre vor Planbeginn Eingabe nur wenn Alter bei Planbeginn > 21
Massgebendes durchschnittliches Jahreseinkommen read-only, live berechnet
Resultierende AHV-Rente pro Jahr read-only, live berechnet

Die beiden Eingabefelder erscheinen also nur, wenn sie fachlich gebraucht werden. Die Zelle zeigt Geprueft bzw. ?; der Entscheid zählt im „offen"-Badge mit. An allen anderen Übergängen ist die AHV-Zelle inaktiv ().

Wichtig zum Feld „vor Planbeginn": Der Wert ist real (heutige Kaufkraft). Der AHV-Kontoauszug listet Einkommen historisch-nominal ein Lohn von 2008 steht dort mit dem Betrag von 2008 und wäre zu tief. Die Zahl stammt idealerweise aus der Rentenvorausberechnung, dort ist die Aufwertung bereits enthalten. Der Hilfetext im Feld sagt das.

Sonderfall „bei Planbeginn bereits pensioniert": Dann gibt es keinen Pensions-Übergang. Die gleichen Felder erscheinen stattdessen in der AHV-Phasenzelle der ersten Phase, zusammen mit der resultierenden Rente als Live-Vorschau.

Referenz: src/components/ElementDetail.tsx (AhvReviewFields), Formeln in src/lib/calculations.ts (ahvMdje, ahvAnnualPension).

3.5.7 Warnhinweis bei Änderungen in früheren Phasen

Seit dem V3-Rework werden Werte live fortgeschrieben (Endwert = Startwert der Folgephase). Eine Änderung in einer frühen Phase wirkt damit bis ans Planende durch oft unbemerkt. Seit Roadmap Nr. 3 gilt das verschärft: Ein geändertes Einkommen in Phase 1 verschiebt über das mdJE auch die AHV-Rente in Phase 5.

Beim Bearbeiten einer Phase, der noch Phasen folgen, erscheint deshalb ein rot abgesetzter Warnhinweis mit der Anzahl betroffener Folgephasen. Er erscheint in:

  • Phasenzellen (Werte eines Elements in einer Phase)
  • Phasen-Detail (Bezeichnung/Dauer eine geänderte Dauer verschiebt alle Folgephasen)

Nicht in Übergangs-Dialogen (dort ist die Wirkung auf die Folgephase offensichtlich) und nicht beim Anlegen eines Elements (dort gibt es noch nichts zu überschreiben).

Referenz: src/components/ElementDetail.tsx (CarryWarning).

3.5.8 Verkaufspreis und modellierter Verkehrswert

Das Modell führt zwei Immobilienwerte getrennt: den Verkehrswert, der mit valueGrowth wächst, und den ursprünglichen Kaufpreis als Basis der Grundstückgewinnsteuer (4.6.5). Beim Verkauf zählt jedoch ausschliesslich der vom Benutzer erfasste Verkaufspreis (4.9.4).

Daraus ergab sich eine stille Inkonsistenz: Man konnte 2 % jährliche Wertsteigerung annehmen und die Immobilie trotzdem zum Kaufpreis verkaufen, ohne dass das Tool widersprach.

Deshalb gilt seit Version 0.12:

  • Beim Wechsel auf Verkaufen wird der Verkaufspreis mit dem modellierten Verkehrswert am Phasenende vorbelegt aber nur, wenn noch keiner erfasst ist (bestehende Pläne bleiben unverändert).
  • Der Dialog zeigt den Verkehrswert daneben read-only an und beziffert die Abweichung in Franken und Prozent.
  • Ab 10 % Abweichung wird der Hinweis rot abgesetzt, mit der Aufforderung zu prüfen, ob Wertsteigerungsannahme und erwarteter Verkaufspreis zusammenpassen.

Der erfasste Preis bleibt massgebend die Vorbelegung ist eine Hilfe, keine Bevormundung. Ein bewusst abweichender Preis (Notverkauf, Liebhaberpreis, Verkauf an Nachkommen) bleibt möglich. Die Berechnung ist unverändert.

Referenz: src/components/ElementDetail.tsx (ElementTransitionFields, REAL_ESTATE).

3.6 Auswertung und Visualisierung

3.6.1 Anzeigemodus nominal / beide / real

Ein Umschalter oben in der Planansicht steuert die Darstellung aller Geldbeträge in Matrix und Phasenköpfen:

Modus Darstellung
Nominal 1'234'567
Beide nominal oben, real in Klammern darunter (siehe unten)
Real 890'123

Im Modus Beide steht der Realwert seit 0.14 in einer eigenen Zeile unter dem nominalen Wert, nicht mehr daneben. Bei Start-/Endwerten wiederholt sich der Pfeil, damit der zeitliche Bezug erhalten bleibt:

30'000 → 10'000
(29'557) → (7'430)

Grund: Nebeneinander wird die Zeile so lang, dass die Spalten unnötig breit werden und Kennzahlen umbrechen. Untereinander bleiben die Spalten schmal erst dadurch lässt sich jede Kennzahl umbruchfrei darstellen. Die Regel gilt im Phasenkopf und in den Matrix-Zellen; in den Modi «Nominal» und «Real» bleibt alles einzeilig.

„real" bedeutet kaufkraftbereinigt auf den Planbeginn: nominal / Deflator. Die Wahl wird in localStorage unter fpt-value-mode gespeichert.

Es werden zwei verschiedene Deflatoren verwendet siehe 4.5.3.

Referenz: src/components/PlanView.tsx Zeilen 665699.

3.6.2 Zeitachse

Horizontale Achse über das Alter (von jüngster Person bis Planende) mit:

  • den Lebensphasen als Segmente: Breite proportional zur Dauer, Einfärbung nach Phasentyp (Erwerb kräftig, Misch mittel, Pension hell), beschriftet mit Name, Dauer und sofern ein Planstart gesetzt ist der Jahresspanne (z. B. „20262046")
  • Flaggen-Marker je Person am Pensionsalter (Farbe: Person A indigo, Person B hellblau)
  • rotem „Ruin "-Marker, falls zutreffend
  • Alters- und Jahres-Beschriftung an beiden Enden

Die Kalenderjahre stammen aus dem Profilfeld Planstart (Scenario.startYear). Ist es nicht gesetzt, zeigt die Achse nur Alter die Berechnung ist davon nie betroffen (siehe 3.2.7).

Referenz: src/components/Timeline.tsx.

3.6.3 Phasenkopf-Kennzahlen

Seit 0.13 ist der Phasenkopf bewusst schlank zwölf Kennzahlen in 10-Pixel-Schrift liest niemand (Progressive Disclosure):

Kennzahl Bedeutung
Name + Status-Icon grünes Häkchen oder rotes Warnsymbol (Liquiditätslücke)
Typ-Badge + Dauer Erwerb / Pension / Misch, „N J."
Alter je Person <Name> <StartAlter> → <EndAlter>
Verfügbares Kapital ab Phase 2 und nur wenn > 0: Topf, davon verteilt, Rest auf Cash (3.6.9)
Sparquote bzw. Verzehrquote Einkommen Ausgaben; Label wechselt auf „Verzehrquote", wenn Jahr 1 negativ; rot bei Verzehr
Vermögen Start → Ende (inkl. Cash), hervorgehoben
Einmalposten nur als Kurzhinweis (Bezeichnung), wenn vorhanden

Die beiden Blöcke «Verfügbares Kapital» und «Sparquote» tragen je einen Knopf, der das passende Verteil-Werkzeug öffnet (3.6.10).

Alles Weitere Einkommen/Ausgaben Jahr 1 → letztes Jahr, geplante Spar-/Verzehrrate, Kapitalzufluss und -investitionen, die vollen Einmalposten wohnt in der Phasen-Detailansicht (seit 0.11, 3.6.7), erreichbar über das Expand-Icon im Kopf. Die Kennzahlen selbst sind unverändert berechnet; nur der Anzeigeort hat gewechselt.

Referenz: src/components/PlanView.tsx Zeilen 701769.

3.6.4 Analyse-Bereich „Grafiken"

Die Auswertungen liegen nicht unter der Matrix, sondern in einem eigenen Bereich: Der Button Grafiken in der oberen Aktionsleiste (neben „Neues Szenario aus diesem" und „Monte-Carlo-Simulation") öffnet sie als breiten Dialog. So bleibt die Matrix die ruhige Hauptansicht.

Kennzahl-Karten: Endvermögen nominal und Endvermögen real. (Die frühere Karte „Geschätzter Nachlass" ist entfallen sie war rechnerisch identisch mit dem nominalen Endvermögen und suggerierte eine zusätzliche Information, die es nicht gab.)

Grafik 1 Einkommen vs. Ausgaben pro Jahr (SparquoteChart): Ein Datenpunkt pro Jahr über alle Phasen. Grüne Linie = Einkommen nominal (inkl. Renten), rote Linie = Ausgaben nominal, graue gestrichelte Linie = Ausgaben real. Die Fläche zwischen Einkommen und nominalen Ausgaben ist grün (Sparquote) oder rot (Verzehr) eingefärbt. Der Keil zwischen roter und grauer Linie ist anschaulich „das, was die Inflation frisst".

Grafik 2 Vermögensverlauf nach Alter (WealthChart): Liniendiagramm über das Alter von Person A. Je Szenario eine durchgezogene Linie (nominal) und eine gestrichelte (real). Datenpunkte: jedes Planjahr (nicht nur die Phasengrenzen) dadurch werden Verläufe innerhalb einer Phase sichtbar, etwa das Abschmelzen im Kapitalverzehr. Grundlage ist YearPoint.wealthNominal/wealthReal (4.10). Über Checkboxen lassen sich die Geschwister- Szenarien überlagern; deren Daten werden bei Bedarf nachgeladen und im Client zwischengespeichert.

Grafik 3 Vermögensaufteilung pro Phase: Gestapeltes Balkendiagramm mit zwei Balken je Phase (Beginn / Ende). Gestapelt werden alle Elemente der Kategorien PK, 3a, Immobilie, Sonstiges Vermögen, die irgendwann einen positiven Wert haben. Stapelung nach elementId (nicht Name), damit gleichnamige Elemente nicht kollidieren. Negative Werte werden auf 0 geklammert.

Referenz: src/components/Dashboard.tsx, src/components/WealthChart.tsx, src/components/SparquoteChart.tsx.

3.6.5 CSV-Export

GET /api/plans/<planId>/export liefert eine semikolon-getrennte CSV, eine Zeile pro Phase:

Phase;Typ;Dauer;Einkommen (Beginn);Ausgaben (Beginn);Quote (Beginn);Quote (Ende);Cash (Ende);Endvermoegen (nominal);Endvermoegen (real)

Bei vorhandenem Ruin folgt eine Schlusszeile Ruin: Kapital aufgebraucht mit Alter <N>. 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).

Referenz: src/components/SensitivityDialog.tsx, src/components/AppShell.tsx.

3.6.7 Detailansichten je Element und je Lebensphase

Jede Element-Zeile und jeder Phasenkopf trägt oben rechts ein Expand-Icon (erscheint beim Überfahren). Es öffnet eine Nur-Lese-Detailansicht mit Reitern.

Element zwei Reiter:

Reiter Inhalt
Verlauf Liniendiagramm über alle Planjahre, nicht nur die Phasengrenzen. Bei einer Immobilie drei Reihen (Eigenkapital, Verkehrswert, Restschuld). Zusätzlich die wirksame Rate auf einer zweiten Y-Achse rechts in Prozent als Stufenlinie, weil sie innerhalb einer Phase konstant ist und an der Phasengrenze springt; eine interpolierte Kurve würde einen gleitenden Übergang suggerieren, den die Berechnung nicht macht. Die Achse erscheint nur, wenn das Element überhaupt eine Rate trägt. Darunter eine Tabelle Beginn/Ende je Lebensphase.
Rechenweg Die Herleitung je Phase und je Übergang (siehe 4.14)

Lebensphase drei Reiter:

Reiter Inhalt
Übersicht Typ, Dauer, Vermögen Beginn/Ende; Vermögensaufteilung als gestapelter Balken (Beginn und Ende)
Wasserfall Vermögens- und Cash-Wasserfall (4.14.2)
Rechenweg Quote, Spar-/Verzehrrate, Cash-Fortschreibung, Vermögen, Phasentyp, Cash-Übergang

Der Phasenkopf reagiert bereits auf Klick (öffnet das Bearbeiten-Popup); das Expand-Icon stoppt deshalb die Ereignis-Weitergabe, sonst gingen beide Dialoge gleichzeitig auf.

Zusätzlich öffnet der Button Rechenwege in der Aktionsleiste die plan-weiten Grössen: Deflatoren, AHV-Beitragskarriere je Person und Ruinalter.

Referenz: src/components/DetailView.tsx, src/components/PlanView.tsx.

3.6.8 Systemparameter-Ansicht

Eigener Eintrag in der Seitenleiste (über der SPEZIFIKATION). Zeigt alle fest hinterlegten Systemgrössen, gruppiert nach AHV, Vorsorge und Steuern, je mit Wert, Bedeutung, Herleitung, Quelle und Stand.

Die Einträge stammen aus SYSTEM_PARAMETERS in src/lib/constants.ts derselben Datei, aus der die Berechnung liest. Ein Auseinanderlaufen von angezeigtem und gerechnetem Wert ist damit konstruktiv ausgeschlossen. Abgeleitete Grössen wie AHV_MAX_ANNUAL_SINGLE zeigen ihre Herleitung (2 × R0 × 13) statt nur das Ergebnis.

Referenz: src/components/SystemParametersView.tsx, src/lib/constants.ts.

3.6.9 Verfügbares Kapital im Phasenkopf

Für die Planung einer Phase ist die zentrale Frage: Wie viel Kapital steht überhaupt zur Verfügung? Der Phasenkopf weist das ab Phase 2 als eigenen Block aus (nur wenn > 0):

Zeile Bedeutung
Verfügbares Kapital der gesamte Topf beim Übergang in diese Phase
davon verteilt Zusatzeinlagen + Sonderamortisation + Sofort-Tilgung
Rest auf Cash was auf dem Cash-Konto liegen bleibt (= cashStart)

Der Topf ist vollständig aus der Cash-Brücke (4.14.2) ableitbar es braucht keine zusätzliche Berechnung:

Topf = Cash-Ende der Vorphase + Kapitalzufluss + einmaliger Zufluss  einmalige Kosten
     = cashStart + Investitionen + Sofort-Tilgungen

Nur ab Phase 2: In der ersten Phase ignoriert die Berechnung additionalInvestment dort tragen die Elemente ihren Startwert direkt (4.6.3). Ein «Verteilen» hätte dort eine andere Bedeutung, deshalb wird es gar nicht erst angeboten.

Referenz: src/lib/distribution.ts (capitalPot).

3.6.10 Verteil-Werkzeuge

Zwei Popups, erreichbar über je einen Knopf im Phasenkopf. Beide schreiben ausschliesslich bestehende Felder über die bestehenden Endpunkte an der Berechnung ändert sich nichts.

«Kapital verteilen» verteilt den Topf aus 3.6.9 auf:

Ziel geschriebenes Feld liegt an
PK, Säule 3a, Sonstiges Vermögen additionalInvestment dieser Phase
Immobilie extraAmortization dem Übergang davor
Sonstige Schulden immediateRepayment dem Übergang davor

Dass zwei verschiedene Objekte beschrieben werden, ist eine Folge des Datenmodells: Die Zusatzeinlage ist ein Phasenwert, Sonderamortisation und Sofort-Tilgung sind Übergangs-Entscheide. Beide zehren aber vom selben Topf. Betragsfelder werden dabei in die bestehenden Daten hineingemischt vorhandene Entscheide (decision, salePrice, payoutMode …) bleiben erhalten.

Was nicht verteilt wird, bleibt automatisch auf dem Cash dafür braucht es keine Logik, das ist das Verhalten des Modells. Wird mehr verteilt als vorhanden, startet die Folgephase mit negativem Cash; der Dialog weist das als Liquiditätslücke aus.

«Sparquote verteilen» (bzw. «Bezug verteilen» bei Verzehr) verteilt die laufende Quote auf jährliche Raten: annualContribution (3a, Sonstiges Vermögen), annualWithdrawal (Sonstiges Vermögen), amortization (Immobilie), annualRepayment (Schulden).

Die Pensionskasse fehlt hier bewusst. Ihr Beitrag stammt aus dem Bruttolohn und belastet das Cash-Konto nicht (4.6.3) er lässt sich also gar nicht aus der Quote verteilen.

Der Dialog weist drei Bezugsgrössen aus: Quote im ersten Jahr, im letzten Jahr und entscheidend die absolute Quote über die ganze Phase. Letztere ist die Grösse, gegen die sich eine flache Jahresrate sinnvoll verteilen lässt (siehe 9.25).

Live-Vorschau: Beide Dialoge kopieren den Plan mit den Entwurfswerten und rechnen ihn erneut durch computePlan im Browser, ohne API-Aufruf. Die angezeigte Wirkung ist dadurch per Konstruktion exakt die spätere, inklusive aller Kappungen (Bezugsrate am Bestand, Amortisation an der Restschuld). Eine Nebenrechnung im UI hätte hier dieselbe Driftgefahr wie bei den Rechenwegen (4.14.3).

Beide Dialoge zeigen den Fortschreibungs-Warnhinweis (3.5.7), wenn Folgephasen existieren.

Referenz: src/components/DistributionDialogs.tsx, src/lib/distribution.ts.

3.6.11 Raten über Lebensphasen übernehmen

Werte liegen je Lebensphase vor. Bei Beträgen ist das richtig das Einkommen ändert sich, der Vermögensstand ohnehin. Bei Raten ist es meist nicht gemeint: Wer die erwartete Rendite seines ETF auf 5 % setzt, meint fast nie «nur in Phase 2». Ohne Übernahme muss derselbe Wert vier- oder fünfmal eingetippt werden, und dabei wird zuverlässig eine Phase übersehen.

Ändert man ein Ratenfeld, erscheint deshalb im Bearbeitungspanel eine Rückfrage mit drei Möglichkeiten:

Auswahl Wirkung
Nur diese Phase (Vorgabe) bisheriges Verhalten, andere Phasen bleiben unberührt
Diese + folgende ab der bearbeiteten Phase vorwärts Vergangenes bleibt stehen
Alle Phasen der Wert gilt für den ganzen Plan

Als Ratenfelder gelten:

Feld Kategorien
expectedReturn Pensionskasse, Säule 3a, Sonstiges Vermögen
valueGrowth Immobilie (Wertsteigerung)
interestRate Immobilie (Hypothekarzins)
teuerungsausgleich Einkommen (Lohnentwicklung), Ausgaben (reale Mehrausgaben)

AHV und Schulden haben keine: Die AHV-Rente folgt der amtlichen Formel (4.4), Schulden tragen ihren Zins nicht als eigenes Feld.

Vier Entwurfsentscheide:

Die Rückfrage erscheint beim Bearbeiten, nicht als Modal. Das Zahlenfeld löst bei jedem Tastendruck aus ein Dialog erschiene bei der Eingabe «5.2» viermal. Stattdessen taucht die Auswahl inline unter den Feldern auf, sobald sich eine Rate tatsächlich vom gespeicherten Wert unterscheidet, und wird beim Speichern ausgeführt.

Nur veränderte Raten lösen sie aus. Wer bloss einen Betrag anpasst, bekommt keine Rückfrage. «Nicht gesetzt» und «0» gelten dabei als gleich sonst meldete schon das Öffnen eines Panels mit leerem Feld eine Änderung.

Die Zielphasen behalten ihre übrigen Werte. Der Endpunkt ersetzt den ganzen Werte-Satz einer Phase. Würde man den Entwurf der bearbeiteten Phase einfach hinüberkopieren, verlöre jede andere Phase ihre Beträge, Sparraten und Bezüge. Übertragen wird deshalb ausschliesslich das geänderte Ratenfeld, in die bestehenden Werte hineingemischt. Ein Test sichert genau das ab.

Phasen, in denen der Wert schon stimmt, werden übersprungen das spart Schreibvorgänge und verhindert eine Version, obwohl sich inhaltlich nichts geändert hat.

Technisch ist die Übernahme ein Schreibvorgang je Zielphase über den bestehenden Endpunkt; es gibt keinen neuen Schreibpfad. Alle fallen in dieselbe Bearbeitungssitzung und ergeben deshalb eine Nebenversion, nicht eine je Phase (3.8.1).

Referenz: src/lib/ratefields.ts, src/components/ElementDetail.tsx.

3.7 Bedienoberfläche

3.7.1 Layout

  • Sidebar (Desktop ab lg fix, mobil als Overlay): Logo, Ctrl-K-Hinweis, „Übersicht", Gruppe «Meine Pläne» (Szenario-Baum, «+»), Gruppe «Wissen» («So rechnet FPT» = gerenderte Spezifikation, «Systemparameter»).
  • Header: Menü-Button (mobil), Plantitel, Profilmenü.
  • Hauptbereich: Übersicht (Plan-Kacheln bzw. EmptyState mit «Geführt starten» / «Beispielplan») oder Planansicht (Umschalter, Zeitachse, Szenario-Profil-Leiste, «Nächste Schritte», Ruin-Banner, Matrix, Inspector-Panel rechts).
  • Die Matrix hat einen eigenen Scrollbereich (max. 75 vh): Phasen-Köpfe (oben) und Elementnamen (links) bleiben beim Scrollen in beiden Achsen fixiert.
  • Alle Aktions-Icons (Kopieren, Löschen, Detailansicht) sind auch ohne Hover leicht sichtbar auf Touch-Geräten gibt es kein Hover.

3.7.2 Farbschemata

Drei Themes: Hell, Dunkel, Warm (cremefarben, Koralle-Akzent). Wahl im Profilmenü, persistiert in localStorage (fpt-theme), gesetzt als data-theme am <html>. Ohne gespeicherte Wahl folgt die Oberfläche prefers-color-scheme. Ein Inline-Script im <head> setzt das Attribut vor dem ersten Paint (verhindert FOUC).

Alle Farben laufen über semantische CSS-Variablen (--bg, --surface, --accent, --danger, …).

Referenz: src/lib/theme.ts, src/app/globals.css, src/app/layout.tsx Zeilen 11811196.

3.7.3 Geldeingabefelder

Die MoneyInput-Komponente ist ein spezialisiertes Betragsfeld:

  • Unfokussiert Anzeige mit Apostroph-Tausendertrennung (1'234'567), fokussiert reine Ziffern
  • Ein Default-Wert 0 wird beim Fokussieren geleert, sonst der Text markiert
  • Pfeiltasten: ↑/↓ = ±1, Shift+↑/↓ = ±100
  • Pfeil-Buttons mit Klick-und-Halten-Beschleunigung: nach 400 ms Wiederholung im 70-ms-Takt, Schrittweite wächst mit der Haltedauer (1 → 10 → 100 → 1'000 → 10'000)
  • Optionale harte Klammerung über min (Default 0) und max

Zahlenformat: Bewusst nicht über toLocaleString("de-CH"), weil dessen Trennzeichen das typografische Apostroph (U+2019) ist. Das Tool verwendet durchgehend das gerade Apostroph.

Referenz: src/components/FormField.tsx Zeilen 60200, src/lib/format.ts.

3.7.4 Hilfe-Bubbles

Feldbeschriftungen können ein Info-Icon tragen (InfoBubble), das per Hover oder Klick einen Erklärtext einblendet. Wird durchgehend für die fachlich heiklen Felder verwendet.

3.7.5 Reaktivität

Die Anwendung rechnet nicht im Client. Jede Änderung führt zu einem PUT/PATCH/POST, gefolgt von onChanged()loadDetail(planId, silent = true). Der „silent"-Refresh lädt Plan und Berechnung neu, ohne die PlanView zu demontieren so bleibt die Scrollposition nach dem Schliessen eines Popups erhalten.

Referenz: src/components/AppShell.tsx.

3.7.6 Sprache und UI-Primitiven

Seit 0.13 gilt durchgehend die Du-Form (wie moderne CH-Finanz-Apps) und es werden überall echte Umlaute verwendet zuvor mischte das UI «Übergang prüfen» mit «Uebersicht» und Sie- mit Du-Anrede. Der Sweep umfasst auch die API-Fehlermeldungen (z. B. „Nur die letzte Phase kann gelöscht werden.").

Alle Bausteine liegen zentral in src/components/ui.tsx:

Baustein Ersetzt / leistet
Button vier Varianten (primary/secondary/danger/ghost), einheitliche Hover-/Active-Zustände
Modal Dialog mit ESC, Fokus-Falle, aria-modal, Einblend-Animation
ConfirmProvider / useConfirm ersetzt window.confirm() Browser-Systemdialoge folgen keinem Farbschema
ToastProvider / useToast ersetzt alert(); Erfolgs- und Fehlermeldungen unten rechts
Skeleton / PlanSkeleton Struktur-Platzhalter statt «Lädt…»-Text
EmptyState einheitliche leere Zustände mit Erklärung und Aktion
InspectorShell das rechte Bearbeiten-Panel (3.7.7)

Mehrere Schichten (Panel + Dialog) teilen sich einen ESC-Stapel: ESC schliesst immer nur die oberste. Animationen (150220 ms) respektieren prefers-reduced-motion.

Attention-Farbe: Offene Entscheide tragen neu einen eigenen Amber-Ton (--attention, je Farbschema abgestimmt) statt der Akzentfarbe «hier fehlt eine Eingabe» und «hier kannst du klicken» sind damit unterscheidbar.

3.7.7 Inspector-Panel statt Modals

Alle Einzel-Bearbeitungen Phasenzelle, Übergangszelle, Cash-Übergang, Cash-Anfangswert, Phase (Name/Dauer), Szenario-Profil öffnen sich als rechtes Seitenpanel (InspectorShell, 26 rem, mobil volle Breite) statt als Modal. Bewusst ohne Backdrop: Die Matrix bleibt sichtbar und klickbar, ein Klick auf eine andere Zelle wechselt einfach den Panel-Inhalt der Kontext, den Modals nehmen, bleibt erhalten. Genau ein Panel kann offen sein (eine Panel-Union ersetzt die früheren Einzel-Zustände); der key erzwingt beim Wechsel den Neuaufbau des Formulars wie zuvor bei den Dialogen.

Modals bleiben für Erstell-Flows (Element, Phase, Plan, Assistent), den geführten Übergang (mehrere Objekte auf einmal), die Analysen und die Detailansichten.

Der frühere Dialog «Plan-Einstellungen» heisst im Panel korrekt «Szenario-Profil» er bearbeitet seit V6 das Szenario, nicht den Plan.

3.7.8 Tour und «Nächste Schritte»

Die Tour (Tour.tsx) startet einmalig beim ersten Öffnen eines Plans mit Phasen (localStorage fpt-tour-done) und führt in bis zu sechs Schritten über Profil, Zeitachse, Matrix, Übergänge, Cash und Analysen als Karte am unteren Rand plus pulsierender Rahmen um das Ziel (data-tour-Attribute). Schritte ohne vorhandenes Ziel werden übersprungen; der «Tour»-Knopf in der Werkzeugleiste startet sie jederzeit neu. Grenze: 9.24.

Die Karte «Nächste Schritte» über der Matrix leitet aus den vorhandenen Daten ab, was sinnvollerweise als Nächstes ansteht offene Übergangs-Entscheide (mit Direktsprung in den geführten Übergang), fehlendes Einkommen/fehlende Ausgaben, ein Plan, der vor der Pensionierung endet, und bei Ruin der Sprung zu den Einflussfaktoren. Kein eigener Zustand, kein API-Aufruf; die Karte verschwindet von selbst, wenn nichts offen ist. Auch das Ruin-Banner verlinkt neu auf die Einflussfaktoren («Wirksamsten Hebel finden»).

3.7.9 Befehls-Palette und Sparklines

Ctrl/Cmd+K öffnet die Befehls-Palette (CommandPalette.tsx): Springen zu jedem Plan/Szenario und die wichtigsten Aktionen (Analysen, neues Szenario, neuer Plan), mit Tastatur-Navigation der Effizienz-Pfad für geübte Nutzer.

Jede Element-Zeile der Matrix trägt eine Sparkline (Sparkline.tsx): der Wertverlauf über alle Planjahre als Mini-SVG, gespeist aus den per-Jahr-Werten der Detailansichten (ElementPhaseComputed.yearly, seit 0.11) keine Neuberechnung, reine Darstellung. Flache Verläufe werden nicht gezeichnet (keine Information).

3.8 Versionierung und Änderungshistorie

Jedes Szenario trägt eine Version A.B und eine vollständige Änderungshistorie. Zugang über den Knopf «Änderungshistorie» in der Szenario-Leiste.

Bedeutung Entsteht
B (Nebenversion) ein Bearbeitungsstand automatisch, eine je Bearbeitungssitzung
A (Hauptversion) ein bewusst gesetzter Meilenstein manuell, mit Pflichtkommentar; setzt B auf 0

3.8.1 Eine Version je Sitzung nicht je Änderung

FPT hat keinen Speichern-Knopf: Jede Änderung schreibt sofort. Eine Version je Schreibvorgang wäre deshalb ein Tastenprotokoll und keine Historie ein Durchlauf des Plan-Assistenten macht rund 14 Schreibvorgänge, ein Klick im Verteil-Dialog einen je Zielelement.

Stattdessen werden alle Schreibvorgänge innerhalb eines Zeitfensters von 10 Minuten zu einer Nebenversion zusammengefasst: Der erste legt sie an, alle weiteren aktualisieren sie. Zwei Sicherungen ergänzen das:

  • Unverändert = keine Version. Ergibt ein Schreibvorgang inhaltlich denselben Stand (Dialog geöffnet und unverändert geschlossen), entsteht nichts. Verglichen wird über eine Serialisierung mit sortierten Schlüsseln ohne das würden identische Stände als verschieden gelten, weil die Reihenfolge der Werte aus der Datenbank-Zeilenfolge stammt.
  • Benutzerwechsel trennt immer. Änderungen verschiedener Benutzer laufen nie in einer Version zusammen, auch nicht innerhalb des Fensters Vorbereitung auf die spätere Freigabe an einen Finanzberater.

Eine Hauptversion wird nie zusammengefasst und nie nachträglich verändert; die nächste Änderung beginnt bei A.1.

3.8.2 Was die Historie zeigt

Je Version: wer (Benutzername), wann (Zeitpunkt der letzten Änderung dieser Sitzung) und bei Hauptversionen der Kommentar. Die oberste Zeile ist der aktuelle Stand.

Zwei Aktionen je Version:

  • Anzeigen ein Nur-Lese-Fenster mit der Matrix dieses Standes (VersionMatrix), Phasen als Spalten, Elemente als Zeilen, dazu Endvermögen und Ruinalter. Bewusst nicht die Bearbeitungs-Matrix: Inspector, Übergangs-Ampeln und Diff-Markierung haben für einen alten Stand keine Bedeutung.
  • Wiederherstellen setzt das Szenario vollständig auf diesen Stand zurück.

3.8.3 Wiederherstellen

Zwei Eigenschaften machen den Knopf ungefährlich:

Es geht nichts verloren. Der wiederhergestellte Stand wird selbst als neue Version festgehalten, mit dem Kommentar «Wiederhergestellt aus A.B». Die bisherige Historie bleibt vollständig auch alles, was nach dem Zielstand kam.

IDs bleiben erhalten. Phasen und Elemente behalten ihre IDs (der Snapshot trägt sie mit). Das ist keine Kosmetik: Kind-Szenarien zeigen über sourceElementId / sourcePhaseId auf genau diese IDs, und daran hängt die Abweichungs-Markierung (3.2.6). Würden neue IDs entstehen, erschiene in jedem Kind-Szenario schlagartig alles als «neu» statt als «geändert».

Restlos vermeiden lässt sich das nicht: Geht man auf einen Stand zurück, in dem ein Element noch gar nicht existierte, auf das ein Kind verweist, bricht dieser eine Bezug zwangsläufig. Der Dialog prüft das vorher und benennt die betroffenen Szenarien namentlich, statt es stillschweigend zu tun.

3.8.4 Analyse auf einer bestimmten Version

Alle vier Analysewerkzeuge Grafiken, Live-Simulation, Monte-Carlo und Einflussfaktoren haben oben einen Wähler «Berechnungsgrundlage»: aktueller Stand (Vorgabe) oder eine festgehaltene Version.

In der Monte-Carlo-Simulation ist die Auswahl zweidimensional: Sie rechnet mehrere Szenarien gleichzeitig, und der Wähler hängt deshalb an der jeweiligen Szenario-Zeile man kann Szenario A in Version 1.2 gegen Szenario B in Version 2.0 stellen.

Das war fast kostenlos: Der Snapshot ist bereits ein PlanInput, also genau der Typ, den alle Werkzeuge ohnehin entgegennehmen. Es genügt, den Arbeitsstand durch den Snapshot zu ersetzen; das Gerechnete entsteht lokal über computePlan.

Eine Ausnahme ist beschriftet: Der CSV-Export liest immer das Szenario aus der Datenbank und liefert deshalb den aktuellen Stand, nicht die betrachtete Version. Der Knopf sagt das in diesem Fall ausdrücklich.

3.8.5 Vollständigkeit der Historie

Die Versionierung hängt daran, dass jeder inhaltsverändernde Endpunkt sie auslöst (touchScenario). Ein vergessener Pfad fiele nicht auf er erzeugte still keine Version, und die Lücke bemerkte man erst Wochen später. Ein Test liest deshalb alle Route-Dateien und verlangt, dass jeder schreibende Endpunkt entweder touchScenario aufruft oder mit Begründung in einer Ausnahmeliste steht; verwaiste Ausnahmen meldet er ebenfalls.

Das Festhalten einer Version ist bewusst fehlertolerant: Schlägt es fehl, wird es protokolliert, die eigentliche Änderung des Nutzers aber nicht zurückgewiesen. Die Historie ist Begleitinformation, nicht der Zweck der Anfrage.

Referenz: src/lib/versioning.ts (reine Logik), src/lib/versioning-db.ts (Datenbank), src/components/VersionHistoryDialog.tsx, src/components/VersionMatrix.tsx, src/components/VersionPicker.tsx.

3.9 Effektive Werte (Plan-/Ist-Vergleich)

Roadmap Nr. 5. Macht aus dem Planer ein Monitoring-Werkzeug: Was ist tatsächlich eingetreten, und was heisst das für den Rest der Planung?

Knopf «Effektive Werte» auf Plan-Ebene → Liste der bisherigen Erfassungen → Wizard in zwei Schritten.

3.9.1 Ein Ist-Satz gehört zum Plan, nicht zum Szenario

Das tatsächliche PK-Guthaben am 18.8.2026 ist eine Zahl unabhängig davon, gegen welches Szenario man sie hält. Ein Ist-Satz hängt deshalb am Plan und wird über dieselbe Herkunfts-Kette (sourceElementId) auf die szenario-eigenen Element-IDs abgebildet, die auch der Diff (3.2.6) und die Monte-Carlo-Gruppierung benutzen. Erfasst wird also je Wurzel-Element.

3.9.2 Der Wizard

Schritt 1 Stichtag. Exaktes Datum (z. B. 18. August 2026) plus optionale Notiz. Das Datum erscheint in der Liste und auf der Zeitachse; für die Rechnung zählt nur die Jahreszahl, weil der Rechenkern in ganzen Jahren ab Planbeginn arbeitet. Der Dialog sagt das ausdrücklich.

Schritt 2 Werte. Alle Elemente aller Szenarien dieses Plans, zusammengefasst auf ihre Wurzel, dazu das Cash-Konto. Vorbelegt mit dem Stand, den der Plan für dieses Jahr vorsieht (aus dem Basisszenario; fehlt das Element dort, aus dem erstbesten Szenario, das es kennt). Der Nutzer überschreibt nur, was tatsächlich abweicht.

Zwei Arten von Werten, die sich verschieden verhalten:

Art Elemente Wirkung
Bestand PK, 3a, Sonstiges Vermögen, Schulden, Cash ersetzt den laufenden Stand
Verkehrswert + Schuld Immobilie zwei Felder: Wert und Resthypothek getrennt
Fluss Einkommen, Ausgaben nominaler Jahresbetrag; ersetzt die Basis für alle Folgejahre

Die AHV erscheint nur, wenn die Rente zum Stichtag bereits läuft. Vorher gibt es keinen Stand, den man ablesen könnte die Rente folgt der amtlichen Formel aus der Beitragskarriere (4.4).

Ist-Werte erfassen Werte, keine Entscheide. Wenn der Plan die Immobilie verkauft, du sie aber behalten hast, lässt sich das hier nicht ausdrücken dafür ist ein Szenario da.

3.9.3 Die zweite Berechnung

Der Plan-Lauf bleibt unangetastet. Parallel läuft ein zweiter mit derselben Mechanik, aber korrigierter Ausgangsbasis: In jedem Jahr, für das ein Ist-Satz erfasst wurde, schnappen die Werte auf die Realität und laufen von dort planmässig weiter. Alle Sätze gehen ein, nicht nur der jüngste.

Beispiel aus der Anforderung: Fonds startet 2020 mit 100'000 bei 5 %. Ohne Ist-Daten steht 2022 rechnerisch 115'763. Wird für 2022 ein Ist-Wert von 120'000 erfasst, rechnet die Ist-Sicht ab dort weiter und steht 2024 bei 132'300. Kommt für 2024 ein Wert von 140'000 dazu, springt sie erneut. Genau das ist als Test hinterlegt.

Lücken fallen auf die Plandaten zurück. Ein Element ohne erfassten Ist-Wert läuft unverändert auf seiner Planlinie weiter man muss nicht alles wissen, um etwas zu erfassen.

Technisch: computePlan(plan, sample?, { actuals }). Ohne die Option verhält sich die Funktion exakt wie bisher; die 43 Golden Tests laufen unverändert.

Der Sprung ist keine Rendite. Die Differenz zwischen Plan und Wirklichkeit wird als eigene Brückenposition actualsCorrection geführt (Vermögens- und Cash-Brücke). Würde man sie den Kapitalerträgen zuschlagen, erschiene ein Planrückstand als Anlageverlust und die Zerlegung ginge im Ist-Jahr nicht mehr auf (3.6.8).

3.9.4 Anzeige in der Matrix

Ein zweiter Umschalter neben nominal/real/beide, aber mit nur zwei Möglichkeiten:

Auswahl Wirkung
Plan wie bisher
Effektiv die Ist-Zahlen, jeweils mit der Abweichung zum Plan daneben

Warum keine dritte Möglichkeit «beide»: Plan und Ist als Rohwerte nebeneinander wären bei zusätzlich aktivem nominal/real acht Zahlen je Zelle. Stattdessen zeigt die Ist-Ansicht den Wert und daneben klein die Differenz, grün oder rot (9.29). Der Umschalter erscheint nur, wenn überhaupt Ist-Werte erfasst sind.

Nur die Abweichung trägt Farbe. Die Beträge selbst bleiben neutral sonst wird die Matrix zum Ampelteppich, in dem nichts mehr heraussticht.

3.9.5 Zeitachse

Je erfasstem Jahr ein Marker. Der jüngste ist farbig, ältere blass sie sind überholt, aber nicht bedeutungslos. Ohne gesetztes Planstartjahr entfallen die Marker, weil es dann keinen Kalenderbezug gibt.

3.9.6 Die vier Analysewerkzeuge

Grafiken, Live-Simulation, Monte-Carlo und Einflussfaktoren erhalten dieselbe Leiste: Werte (nominal/real) und Grundlage (Plan/Effektiv), dazu die schon bestehende Versionswahl. «Effektiv» ist deaktiviert, solange nichts erfasst ist.

Nominal/real ist in den Werkzeugen eine Einfachauswahl, kein «beide» anders als in der Matrix. Begründung siehe 9.29.

Besonderheiten:

  • Vermögensverlauf: Im Ist-Modus kommt die reine Planlinie gestrichelt als Referenz dazu. Maximal vier Serien.
  • Monte-Carlo: Der Zielbetrag dreht mit der gewählten Grösse (real/nominal) und wird entsprechend beschriftet sonst prüft man einen nominalen Zielbetrag gegen ein reales Endvermögen. Ausserdem eine Zeile: Simuliert ab Jahr; die Jahre davor sind durch deine effektiven Werte belegt und werden nicht gewürfelt. Das Startjahr ist abgeleitet, nicht eingebbar ein frei gesetztes Jahr würde Jahre als sicher behandeln, die nie erfasst wurden.
  • Einflussfaktoren: Mit Ist-Werten wirken die Treiber nur noch auf die nicht belegten Jahre. Die Balken fallen dadurch zu Recht kürzer aus.

3.9.7 Verhältnis zur Versionierung

Ein Ist-Satz ist eine Beobachtung, keine Planänderung: Er erzeugt keine Szenario-Version (3.8), und es gibt kein Wiederherstellen. Löschen entfernt ihn ersatzlos; der Plan bleibt unberührt. Die beiden Historien bleiben getrennt.

Referenz: src/lib/actuals.ts, src/lib/dataview.ts, src/components/ActualsDialog.tsx, src/components/AnalysisControls.tsx.

3.10 Navigation auf Plan-Ebene und gespeicherte Analysen

Die Seitenleiste ist zweistufig: Unter jedem Plan liegen drei leicht eingezogene Unterpunkte. Ein Klick auf den Plan-Namen öffnet dessen Dashboard.

Ort Führt zu
Plan-Name Plan-Dashboard (Kennzahlen)
Szenarien Szenario-Liste darunter bleibt der Szenario-Baum, dessen Einträge direkt in die Matrix führen
Effektive Werte Liste + Wizard der Ist-Werte (3.9)
Analysen Vier Werkzeug-Kacheln + Liste gespeicherter Analysen

3.10.1 Plan- vs. Szenario-Ebene der Kennzahlen

Seit V7 (9.30) liegen Haushaltsform, Personen und Startjahr am Plan; Endvermögen, Ruinalter, Phasen und Elemente sind dagegen szenario-eigen. Das Plan-Dashboard zeigt deshalb die Haushaltsdaten direkt, alle gerechneten Kennzahlen aber ausdrücklich als «laut Basisszenario» das Basisszenario ist der kanonische Vertreter. Falls Ist-Werte erfasst sind, weist es zusätzlich die Abweichung des Endvermögens gegenüber dem Plan aus.

Die Szenario-Liste zeigt je Szenario Name, aktuelle Hauptversion, Anzahl Elemente, das Endvermögen und ob das Kapital reicht; das Basisszenario ist farblich hervorgehoben, und die Herkunft (aus welchem Szenario kopiert) steht darunter. Zwei Aktionen je Zeile: Historie und Matrix.

3.10.2 Analysen: vier Kacheln

Die Werkzeuge Grafiken, Live-Simulation, Monte-Carlo und Einflussfaktoren liegen als Kacheln vor, die beim Darüberfahren oder Antippen (Touch hat kein Hover) umklappen und einen Erklärtext zeigen. Ein Klick startet das jeweilige Werkzeug auf dem Basisszenario; Szenario und Version lassen sich darin weiterhin umstellen.

Grafiken öffnen neu nicht mehr alle drei Diagramme, sondern lassen zuerst eine wählen (Vermögensverlauf / Einkommen vs. Ausgaben / Vermögensaufteilung), dazu wie bisher nominal/real und Plan/effektiv. Der Szenario-Vergleich wandert mit zu den Grafiken; der CSV-Export zieht dorthin, wo er hingehört auf die Matrix.

3.10.3 Gespeicherte Analysen

Jede Grafik, Monte-Carlo-Simulation und Einflussfaktoren-Berechnung kann gespeichert werden (die Live-Simulation vorerst nicht sie ist bewusst flüchtig). Festgehalten werden Eingaben und Ergebnis als Zahlen, read-only: Beim Öffnen wird nichts neu gerechnet, die gespeicherten Werte werden nur gezeichnet.

Zahlen statt Bild und zwar bewusst: Ein Bildschirm-Abbild wäre im Druck unscharf (300 dpi gegen 96), im Seitenformat fix und im Dunkelmodus falsch eingefärbt. Aus Zahlen zeichnet der spätere PDF-Bericht (Roadmap Nr. 11) die Grafik vektoriell und in Druckauflösung neu und braucht die Zahlen für Tabellen und Fliesstext ohnehin. Bewusst nicht gespeichert wird finalWealthSorted aus der Monte-Carlo-Simulation (ein Eintrag je Lauf, megabyteweise); für die Anzeige genügen die abgelesenen Wahrscheinlichkeiten und die Bänder.

Jedes Werkzeug schreibt sein Ergebnis in dieselbe generische Form (Parameter, Kernzahlen, Tabelle, Grafik). Dadurch braucht die Nur-Lese-Ansicht nur einen Renderer, und der PDF-Bericht findet überall dieselbe Struktur vor. Der Name wird automatisch vorgeschlagen («Typ · Szenario Version · Datum»), ist aber überschreibbar.

Die Liste je Plan zeigt Typ, Zeitpunkt, Szenario/Version, nominal/real und Plan/effektiv. Ein gespeicherter Datensatz ist eine Momentaufnahme, keine Planänderung: Er erzeugt keine Version, und Löschen entfernt ihn ersatzlos.

Referenz: src/components/PlanViews.tsx, src/components/SavedAnalysisView.tsx, src/components/SaveAnalysisButton.tsx, src/lib/analyses.ts.

3.11 PDF-Berichte

Roadmap Nr. 11. Neuer Unterpunkt «Berichte» auf Plan-Ebene: eine Liste der erzeugten Berichte, von dort aus lässt sich ein neuer anlegen.

3.11.1 Was ein Bericht ist

Ein Bericht ist ein festes Dokument, kein Blick auf den aktuellen Stand. Die erzeugte PDF-Datei wird als Datei abgelegt und lässt sich jederzeit unverändert wieder herunterladen auch nachdem der Plan weiterentwickelt wurde. Das ist der Zweck: ein verlässlicher Audit-Trail. Wer heute einen Bericht verschickt, muss in drei Jahren exakt dasselbe Dokument vorweisen können.

Deshalb wird nicht die Definition gespeichert und das PDF bei Bedarf neu erzeugt: Eine Neuerzeugung könnte nach Änderungen an Plan, Rechenkern oder Layout nicht mehr dieselben Bytes liefern.

Abgelegt werden drei Dinge: die gewählten Parameter, das eingefrorene Berichtsmodell (alle Zahlen, für die Liste und zur Nachvollziehbarkeit) und die PDF-Datei selbst. In der Datenbank, nicht im Dateisystem der Anwendungscontainer wird bei jedem Deploy neu gebaut, nur das Datenbank-Volume überlebt.

3.11.2 Auswahl beim Anlegen

Angabe Möglichkeiten
Titel und Notiz frei; die Notiz erscheint auf dem Deckblatt
Werte nominal oder real eine Leitgrösse für den ganzen Bericht
Grundlage Plandaten oder effektive Werte (nur wenn erfasst)
Szenarien 1 bis 3 (Konstante MAX_REPORT_SCENARIOS)
Analysen beliebig viele der gespeicherten Auswertungen

Zwei bewusste Begrenzungen:

Nur eine Leitgrösse. «Beides» würde jede Tabellenspalte verdoppeln dasselbe Problem wie in der Matrix (9.29).

Höchstens drei Szenarien. Darüber wird die Vergleichstabelle unlesbar; vergleichbare Werkzeuge stellen bewusst nur zwei gegenüber. Das Basisszenario führt den Bericht an und liefert die Zusammenfassung.

3.11.3 Aufbau des Dokuments

Das Layout ist immer gleich; die Auswahl bestimmt nur, welche Bausteine erscheinen.

Seite Inhalt
1 Deckblatt · Das Wichtigste in Kürze (Kennzahlen + drei Kernaussagen) · Ausgangslage
je Szenario Kennzahlen · Vermögensverlauf · Lebensphasen · Annahmen
bei ≥ 2 Szenarien Vergleichstabelle
bei Ist-Daten Plan gegenüber effektiven Werten inkl. Abweichung
bei gewählten Analysen die eingefrorenen Auswertungen
Schluss Wichtige Hinweise (Haftungsausschluss)

Die Zusammenfassung zuerst ist kein Geschmacksentscheid: Überkomplexität ist die häufigste Kritik an Beraterberichten, weshalb sich die einseitige Übersicht als eigenes Format etabliert hat.

3.11.4 Kennzahlen und ihre Grundlage

Je Szenario: Endvermögen · Kapital reicht bis · Vermögen bei Pensionierung · Vorsorgekapital bei Pensionierung (PK + 3a) · AHV-Rente · PK-Rente · offene Entscheide.

Die letzte ist die einzige unmittelbar handlungsleitende Zahl sie zählt die noch nicht getroffenen Übergangs-Entscheide. Damit Bericht und Matrix nie verschiedene Zahlen nennen, liegt die Zählung neu als reine Funktion in lib/decisions.ts, die beide benutzen.

Zu jeder Kennzahl steht ihre Grundlage aber als kurzer Verweis, nicht als wiederholte Tabelle. Die vollständigen Annahmen (Inflation, Pensionsalter, Startwerte, Renditen, Sparraten je Element) stehen einmal je Szenario in einem eigenen Abschnitt. Sie bei jeder Kennzahl zu wiederholen würde den Bericht aufblähen, ohne etwas hinzuzufügen.

3.11.5 Haftungsausschluss

Ein formal gesetztes PDF wird als Beratung gelesen. Der Bericht schliesst deshalb verpflichtend mit Hinweisen: dass es sich um eine Projektion auf Basis eigener Annahmen handelt, keine Anlage-, Steuer- oder Vorsorgeberatung; dass Abweichungen erheblich sein können; dass laufende Einkommens- und Vermögenssteuern nicht modelliert sind (9.14); und dass Monte-Carlo-Wahrscheinlichkeiten die Streuung um die Annahmen messen, nicht deren Richtigkeit. Ein Test stellt sicher, dass der Abschnitt nicht wegfallen kann.

3.11.6 Technik

Erzeugt wird mit pdfkit in der Node-Runtime. Bewusst kein Headless-Browser: Chromium würde das Image um Hunderte Megabyte und Systembibliotheken aufblähen. @react-pdf/renderer schied aus, weil es React-Interna nutzt, die mit React 19 / Next 16 brechen.

Weil das Layout ohnehin fix ist, kostet programmatisches Setzen nichts und die Diagramme entstehen als echte Vektoren aus den gespeicherten Zahlen. Genau dafür wurden die gespeicherten Analysen als Zahlen und nicht als Bilder abgelegt (3.10.3): Ein Bildschirm-Abbild wäre im Druck unscharf.

pdfkit ist in next.config.ts als externes Paket deklariert. Es lädt seine Font-Metriken zur Laufzeit über Dateipfade; gebündelt stimmt __dirname nicht mehr und der Bericht bräche erst in der Produktion.

Die Trennung ist zweistufig: report.ts baut aus Plan und Konfiguration ein reines Berichtsmodell (ohne PDF-Kenntnisse, testbar), report-pdf.ts zeichnet es. Damit ist das, was eingefroren wird, dasselbe, was geprüft wird.

Referenz: src/lib/report.ts, src/lib/report-pdf.ts, src/lib/decisions.ts, src/components/ReportsView.tsx.


4. Berechnungsmodell

Dies ist der fachliche Kern des Tools. Der gesamte Abschnitt beschreibt src/lib/calculations.ts, Funktion computePlan(plan: PlanInput): PlanComputed. Die Funktion ist rein (keine I/O, keine Datenbank) und dadurch isoliert testbar.

4.1 Ablauf im Überblick

für jede Phase i (nach sequenceNumber sortiert):
  1. Personen-Infos berechnen (Alter, erwerbstätig?), Phasentyp ableiten
  2. AHV: Ausfalljahre kumulieren, Renten der Pensionierten berechnen, plafonieren
  3. Element-Setup: je Element Startwerte/Raten bestimmen, in Arbeitslisten einordnen
  4. Investitionen vom Cash abziehen  → cashStart
  5. Jahresschleife t = 1..duration:
       Flows indexieren, Vermögen verzinsen, Bezugsraten entnehmen,
       Cash fortschreiben, Ruin prüfen, YearPoint anfügen
  6. Endwerte je Element setzen, Phasen-Kennzahlen zusammenstellen
  7. Übergang: Entscheide anwenden, Carry aktualisieren, Cash der Folgephase bilden

4.2 Zustand über Phasengrenzen: der Carry

Zwischen den Phasen wird pro Element ein Carry-Objekt fortgeschrieben:

Feld Bedeutung
status ACTIVE / SOLD / SETTLED
value Aktiven-Saldo (PK / 3a / Sonstiges Vermögen) am Ende der Vorphase
mortgage Immobilie: Resthypothek
owed Schulden: Restschuld (positiv geführt)
pkPensionAnnual PK: jährliche Rente nach Verrentung
flowBasis Einkommen/Ausgaben: indexierter Basiswert der nächsten Phase
hasCarry ob überhaupt eine Vorphase existiert (steuert read-only vs. Eingabe im UI)

Zusätzlich laufen phasenübergreifend mit: cashCarryIn, cumulativeInflation, gapYearsByPerson, yearsBefore, ruinAge, incomingInflow, incomingImmediateRepay.

Referenz: src/lib/calculations.ts Zeilen 103115, 132147.

4.3 Personen und Phasentyp

Für jede Person und jede Phase:

startAge         = person.age + yearsBefore
endAge           = startAge + duration
working          = startAge < retirementAge
retiresAtStart   = startAge === retirementAge

yearsBefore ist die Summe der Dauern aller vorangehenden Phasen. Alter wird also relativ gezählt es gibt keine Kalenderdaten im Modell, nur „Jahre ab Planbeginn".

maxDurationYears = Minimum über retirementAge startAge aller noch erwerbstätigen Personen (nur positive Werte), sonst null.

4.4 AHV-Rente

Die Rente hängt an zwei Achsen: der Beitragsdauer (Rentenskala 44) und dem massgebenden durchschnittlichen Jahreseinkommen (mdJE).

4.4.1 Die amtliche Rentenformel (Skala 44)

Alle Schwellen sind Vielfache von R0, dem Mindestbetrag der vollen Monatsrente (AHV_MIN_MONTHLY_FULL = 1'260). Mit x = mdJE / (12 × R0):

mdJE ≤ 12 × R0  (= 15'120)  →  Rente = R0                      (Mindestrente 1'260)
12 × R0 < mdJE ≤ 36 × R0    →  Rente = R0 × (0.74 + 0.26 × x)   Formel 1
36 × R0 < mdJE < 72 × R0    →  Rente = R0 × (1.04 + 0.16 × x)   Formel 2
mdJE ≥ 72 × R0  (= 90'720)  →  Rente = 2 × R0                   (Maximalrente 2'520)

Der Wendepunkt liegt bei 36 × R0 = 45'360 → 1'915/Monat; beide Formelteile sind dort stetig.

Quelle und Verifikation: BSV/MAS „Berechnungsvorschriften der AHV/IV-Renten", gültig ab 1.1.2026 (liefert die Schwellenstruktur 12/36/72 × R0), und die amtliche Tabelle 318.117.1 Monatliche Vollrenten, Skala 44. Die implementierte Formel reproduziert alle 51 Zeilen dieser Tabelle exakt; Stützstellen sind als Golden Tests hinterlegt (Kap. 8.2).

Die Funktion ahvMonthlyFullPension(mdJE) rechnet bewusst stetig. Amtlich wird das mdJE auf Vielfache von 1.2 × R0 gestuft (daher die 51 Tabellenzeilen); für eine Planung ist der stetige Wert näher an der Wahrheit, die Abweichung liegt unter 20/Monat.

4.4.2 Beitragskarriere und mdJE

Pro Person wird über die Phasen hinweg akkumuliert (AhvCareer):

Feld Bedeutung
plannedAvgGrossIncome reales Brutto-Durchschnittseinkommen der Beitragsjahre im Plan (siehe 4.4.5)
planYears Beitragsjahre im Plan = Σ (Phasendauer Ausfalljahre der Phase)
yearsBeforePlan max(0, Alter bei Planbeginn 21)
gapYearsInPlan Summe der Ausfalljahre im Plan

Das mdJE ist der gewichtete Mittelwert über alle Beitragsjahre:

yearsBefore = max(0, yearsBeforePlan  gapYearsBefore)
mdJE = (avgIncomeBefore × yearsBefore + plannedAvgIncome × planYears)
       / (yearsBefore + planYears)

Ausfalljahre reduzieren die Gewichtung (und die Skala), nicht das Durchschnittseinkommen genau wie in der echten AHV: Wer zwei Jahre aussetzt, hat deswegen kein tieferes Durchschnittseinkommen, aber weniger Beitragsjahre.

4.4.3 Warum real gerechnet wird

Sämtliche Einkommen gehen real (Kaufkraft bei Planbeginn) in das mdJE ein, und die Schwellen sind heutige Werte. Das ist kein Vereinfachungs-, sondern ein Genauigkeitsentscheid: Die echte AHV wertet vergangene Einkommen auf (Lohnindex) und indexiert die Schwellen (Mischindex). Beide Bewegungen heben sich in realer Betrachtung weitgehend auf wer nominal mittelt und gegen heutige Schwellen hält, vergleicht Franken von 2046 mit Schwellen von 2026 und überschätzt die Rente systematisch.

Grössenordnung: 45-jährig, 85'000 Lohn, +1.5 %/Jahr bei 2 % Inflation, 20 Erwerbsjahre. Nominal gemittelt: mdJE 98'276 → Maximalrente 32'760. Real gemittelt: mdJE 81'156 → 31'096. Differenz 1'664/Jahr, über 25 Rentenjahre rund 41'600 und der Fehler geht immer nach oben.

Die reale Berechnung eines Phasen-Durchschnitts erfolgt analytisch (avgRealFlow) als geometrische Reihe mit q = (1 + Lohnerhöhung) / (1 + Inflation).

Bekannte Unschärfe: Die Schwellen folgen dem Mischindex, die Aufwertung dem Lohnindex. Da Löhne langfristig schneller steigen als Preise, ist die Deflationierung mit der Preisinflation leicht konservativ. Bewusst in Kauf genommen, statt eine dritte Indexannahme einzuführen.

4.4.4 Woher die Karriere-Werte kommen

Situation Quelle
Person retires innerhalb des Plans transitionValues des AHV-Elements am Pensions-Übergang
Person bei Planbeginn bereits pensioniert phaseValues des AHV-Elements in der ersten Phase
Prüfung noch nicht erfolgt Fallback: avgIncomeBefore = plannedAvgIncome

Der Fallback ist bewusst gewählt: Ohne erfassten Wert gilt der geplante Durchschnitt als Schätzung für die Jahre davor exakt der Wert, den der Dialog vorbelegt. Ein Fallback auf 0 würde die Rente still und massiv zu tief rechnen (bei einer 45-jährigen Person auf rund 45 %).

Einkommen wird einer Person nur zugerechnet, wenn das INCOME-Element ihr zugeordnet ist. Bei einem Einzelplan zählt „Gemeinsam" (HOUSEHOLD) zur Person A; bei einem Paar-Plan nicht (siehe 9.9).

4.4.5 Netto-Brutto-Umrechnung für die AHV

Das Tool erfasst das Einkommen netto, die AHV bemisst sich am Bruttolohn. Beim Aufbau der Karriere wird deshalb hochgerechnet:

plannedAvgGrossIncome = avgRealFlow(...) × AHV_GROSS_FROM_NET_FACTOR   // = 1.12

Sämtliche Werte in AhvCareer, im Prüf-Dialog und im mdJE sind damit brutto auch das Feld „Durchschnittliches Bruttoeinkommen vor Planbeginn", das der Benutzer aus der Rentenvorausberechnung übernimmt (die ohnehin brutto-basiert ist). Eine Einheit im ganzen Ablauf, keine Umrechnung an der Feldgrenze.

Herleitung des Faktors (Arbeitnehmer-Abzüge in % des Bruttolohns):

Abzug Satz Bemerkung
AHV/IV/EO 5.30 % 10.6 % total, hälftig geteilt
ALV 1.10 % 2.2 % total, hälftig geteilt, bis 148'200
NBU ~1.00 % variiert 0.41.6 %, teils vom Arbeitgeber getragen
PK ~25 % BVG-Altersgutschrift 7/10/15/18 % auf den koordinierten Lohn (Brutto 26'460, max. 90'720), Arbeitnehmer ≥ die Hälfte

Total ~913 % je nach Alter und Lohn → Faktor 1/(1q) = 1.10 bis 1.16, Mittel 1.12.

Warum ein fixer Faktor genügt: Das mdJE ist selbst ein Durchschnitt über die ganze Beitragskarriere (44 Jahre). Der altersabhängige PK-Satz mittelt sich dabei heraus ein Karriere-Faktor gegen einen Karriere-Durchschnitt ist konzeptionell stimmig, nicht bloss eine Näherung. Die Restunschärfe (~3 %) ist deutlich kleiner als der Fehler, den die Umrechnung behebt (~11 %, siehe 9.13).

Grenzen: Überobligatorische PK-Pläne, vom Arbeitgeber getragene NBU, Selbstständige und Löhne über 148'200 (ALV sinkt auf 0.5 %) weichen ab. Der Faktor ist heute eine nicht sichtbare Konstante; mit Roadmap Nr. 41 wird er in der Formel-Erklärung ausgewiesen.

Quellen: BSV „Beträge gültig ab 1.1.2026" (Koordinationsabzug 26'460, obere Limite 90'720, „keine Änderung gegenüber 2025"); AHV-Merkblätter 2.01 (AHV/IV/EO) und 2.08 (ALV).

4.4.6 Jahresrente, Skala und Plafonierung

factor = max(0, (44  Ausfalljahre total) / 44)          // Rentenskala 44
rente  = round(ahvMonthlyFullPension(mdJE) × 13 × factor)

AHV_PENSION_MONTHS = 13 seit 1.1.2026 gibt es die 13. Altersrente (Art. 34bis AHVG). Die Formel liefert Monatsrenten; der Jahresbetrag ist deshalb × 13, nicht × 12. AHV_MAX_ANNUAL_SINGLE ist neu abgeleitet: 2 × R0 × 13 = 32'760.

Ehepaar-Plafonierung nur bei householdType = COUPLE und wenn für beide Personen eine Rente vorliegt:

cap = 32'760 × 1.5 = 49'140
falls (renteA + renteB) > cap:
    beide Renten proportional kürzen: rente × cap / summe

Die Rente ist danach nominal fix sie wird über die Phasen hinweg nicht indexiert und verliert damit real an Kaufkraft (siehe 9.11). Sie fliesst in renteTotal und wird im Einkommen mitgeführt.

Referenz: src/lib/calculations.ts (ahvMonthlyFullPension, ahvMdje, ahvAnnualPension), src/lib/constants.ts.

4.5 Nominal, real und die Deflatoren

4.5.1 Das V5-Modell

Grösse Erfassung Indexierung über die Phasenjahre
Einkommen nominal basis × (1 + Lohnerhöhung)^(t1)
Renten (AHV, PK) nominal keine konstant
Ausgaben real real: basis × (1 + reale Mehrausgaben)^(t1), dann × kumulierte Inflation

Die Inflation ist seit V5 plan-weit (plan.inflationRateDefault) und gilt einheitlich für alle Phasen; eine phasenspezifische Überschreibung existiert nicht (mehr).

4.5.2 Kumulierte Inflation

cumInflStart(Phase 1) = 1
cumInflStart(Phase n) = cumInflStart(Phase n1) × (1 + infl/100)^duration(n1)

4.5.3 Die drei Deflatoren

Ein subtiler, aber wichtiger Punkt: Bestandswerte und Flow-Werte haben am Phasenende nicht denselben Deflator, weil ein Flow im Jahr duration anfällt, ein Bestand aber nach dem Jahr duration gemessen wird.

Deflator Formel Verwendung
cumulativeInflationStart s. o. Bestände zu Phasenbeginn, Flows im Jahr 1
cumulativeInflationEnd cumInflStart × (1+infl)^duration Bestände am Phasenende (Cash, Vermögen)
flowDeflatorEnd cumInflStart × (1+infl)^(duration1) Flow-Endwerte (Einkommen, Ausgaben, Quote) eine Kaufkraft-Stufe weniger

Referenz: src/lib/calculations.ts Zeilen 427429; Anwendung in src/components/PlanView.tsx Zeilen 686687, 716717.

4.6 Element-Setup je Kategorie

Für jedes Element (sortiert nach orderIndex) wird ein ElementPhaseComputed erzeugt. Vorab-Abbruch: Ist der Carry-Status SOLD → Notiz „Verkauft"; ist er SETTLED und die Kategorie OTHER_DEBT → „Getilgt". Solche Elemente werden nicht weiter gerechnet.

4.6.1 INCOME / EXPENSE

idx       = phaseData.teuerungsausgleich ?? 0
baseValue = hasCarry ? round(carry.flowBasis) : round(phaseData.amount)
basis     = !hasCarry            → round(phaseData.amount)
            phaseData.amount ist Zahl → round(phaseData.amount)   // bewusster Override
            sonst                → baseValue                       // live vererbt
carry.flowBasis = basis × (1 + idx/100)^duration                   // für die Folgephase

Die Vererbungsregel (src/components/ElementDetail.tsx Zeilen 480490): Beim Speichern wird das Feld amount gelöscht, wenn es exakt dem fortgeschriebenen Wert entspricht. Dadurch bleibt der Wert „live vererbt" eine spätere Änderung in einer früheren Phase wirkt sich weiter durch. Nur ein bewusst abweichender Wert wird fix gespeichert.

Man beachte den Exponenten-Unterschied: der Endwert der Phase nutzt duration 1 (letztes Jahr), der Carry für die Folgephase nutzt duration (ein Jahr weiter).

4.6.2 AHV

Erwerbstätig → nur Zusammenfassung („N Ausfalljahre" / „Keine Ausfalljahre"), kein Wert. Pensioniert → startValue = endValue = rente, Summand in renteTotal.

4.6.3 PENSION_FUND

Fall Verhalten
pensioniert, pkPensionAnnual > 0 Rente: startValue = endValue = pkPensionAnnual, Summand in renteTotal
pensioniert, keine Rente Notiz „Vollständig bezogen"
erwerbstätig base = hasCarry ? carry.value : phaseData.currentValue; topUp = hasCarry ? additionalInvestment : 0; start = base + topUp; Rate = annualContribution

Die PK-Rate wird nicht zu fixedRatesTotal addiert sie belastet das Cash nicht. topUp wird ab Phase 2 zu investmentsFromCash addiert.

4.6.4 PILLAR_3A

Identisch zu PK, mit zwei Unterschieden:

  • Die Rate wird zu fixedRatesTotal addiert (belastet das Cash).
  • Ist der Besitzer pensioniert, gibt es nie eine Rente, nur „Vollständig bezogen".

4.6.5 REAL_ESTATE (Immobilie)

purchase   = hasCarry ? carry.propertyPurchase : round(phaseData.purchasePrice)  // Kaufpreis
valueStart = hasCarry ? carry.propertyValue    : round(phaseData.purchasePrice)  // Verkehrswert
mortgage   = hasCarry ? carry.mortgage         : round(phaseData.mortgage)
amort      = round(phaseData.amortization)      // wird JÄHRLICH am Restsaldo gekappt
equity     = valueStart  mortgage              → startValue, wealthStart

Kaufpreis und Verkehrswert laufen getrennt. Der Verkehrswert wächst mit valueGrowth, der Kaufpreis bleibt der ursprüngliche er ist die Basis der Grundstückgewinnsteuer beim Verkauf (4.9.4). Beide werden über die Phasen fortgeschrieben (carry.propertyValue, carry.propertyPurchase); die Hypothek läuft als Saldo in der Jahresschleife (4.7).

Die Wertsteigerung wirkt auf die Liegenschaft, nicht auf das Eigenkapital. Das ist der Hebel, der Wohneigentum finanziell auszeichnet: 1 % von 1'000'000 sind 10'000 im Jahr bei einem Eigenkapital von 100'000 also 10 % darauf. Würde man die Wertsteigerung auf das Eigenkapital rechnen, ergäbe dieselbe Annahme nur 1'000 im Jahr:

Kaufpreis 1'000'000 · Hypothek 900'000 · EK 100'000 · 1 %/J. · Amortisation 10'000/J. · 10 J. → korrekt 304'622; auf das Eigenkapital gerechnet nur 210'462. Differenz 94'160, und sie wächst mit dem Belehnungsgrad.

Der angezeigte Elementwert bleibt das Eigenkapital (Verkehrswert Restschuld); nur die 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.

4.6.6 OTHER_ASSET

base       = hasCarry ? carry.value : round(phaseData.startValue)
topUp      = hasCarry ? round(phaseData.additionalInvestment) : 0
start      = base + topUp
rate       = round(phaseData.annualContribution)  → fixedRatesTotal      += rate
withdrawal = round(phaseData.annualWithdrawal)    → plannedWithdrawTotal += withdrawal

4.6.7 OTHER_DEBT

owedStart  = hasCarry ? carry.owed : round(phaseData.startValue)
repay      = round(phaseData.annualRepayment)  // wird JÄHRLICH am Restsaldo gekappt
startValue = owedStart                        // negatives Vorzeichen im Vermögen

Wie bei der Immobilie ist die Restschuld ein laufender Saldo; ist sie getilgt, entfällt die Tilgungsrate.

4.7 Die Jahresschleife

Zunächst wird der Cash-Startwert gebildet:

cash = cashCarryIn  (isFirstPhase ? 0 : investmentsFromCash)
cashStart = cash

Die Investitionen werden also am Phasenanfang abgezogen. Grund (Kommentar im Code, Fix in Commit f768e01): Der Cash-Startwert zeigt damit den Bestand nach den Investitionen die investierten Mittel erscheinen im Vermögen und nicht doppelt auch im Cash.

Dann für t = 1 .. duration:

// 1. Einkommen (nominal)
incomeFlow = renteTotal + Σ (inc.basis × (1 + inc.idx/100)^(t1))

// 2. Ausgaben (real → nominal) + Hypothekarzins
inflFactor      = cumInflStart × (1 + infl/100)^(t1)
expenseRealBase = Σ (exp.basis × (1 + exp.idx/100)^(t1))
interestNominal = Σ (re.mortgage × re.interestRate/100)   // nur wo interestHandling = ADD
expenseNominal  = expenseRealBase × inflFactor + interestNominal
expenseReal     = expenseRealBase + interestNominal / inflFactor

// 3. Quote
quote = incomeFlow  expenseNominal

// 4. YearPoint anlegen (year, age Person A, income, expenseNominal, expenseReal);
//    wealthNominal/wealthReal werden nach Schritt 8 nachgetragen

// 5. Vermögen: verzinsen, Sparbeitrag, Bezugsrate
für jedes Asset a:
    grown = a.value × (1 + a.r/100) + a.rate      // Zins zuerst, dann Einzahlung (nachschüssig)
    w     = min(a.withdrawal, max(0, grown))      // Bezug am Bestand gekappt
    a.value = grown  w
    cashFromWithdraw += w

// 6. Amortisation und Tilgung  jeweils am Restsaldo gekappt
debtRates = 0
für jede Immobilie re:
    pay = min(re.amort, re.mortgage)              // nie mehr als die Restschuld
    re.mortgage = pay
    debtRates   += pay
    re.value    ×= (1 + re.growth/100)            // Wertsteigerung auf die LIEGENSCHAFT
für jede Schuld d:
    pay = min(d.repay, d.owed)
    d.owed    = pay
    debtRates += pay
falls t === 1 → plannedSaveRate = fixedRatesTotal + debtRates

// 7. Cash fortschreiben
cash += quote  fixedRatesTotal  debtRates + cashFromWithdraw
falls cash < 0 → cashNegative = true

// 8. Ruin prüfen (Gesamtvermögen zum Jahresende)
total = cash + Σ asset.value + Σ (re.value  re.mortgage) + Σ (d.owed)
yearPoint.wealthNominal = round(total)                    // Grundlage des Verlaufs je Jahr
yearPoint.wealthReal    = round(total / cumInfl[Jahr])
falls ruinAge === null und total < 0 → ruinAge = age(Person A) + yearsBefore + t

Wichtige Details:

  • Verzinsung ist nachschüssig: der Sparbeitrag des Jahres wird nicht mitverzinst.
  • Amortisation und Tilgung enden mit der Schuld. Hypothek und Restschuld sind laufende Salden; die Rate ist pro Jahr auf den Restsaldo gekappt (min(rate, saldo)). Ist die Schuld abbezahlt, fliesst kein Franken mehr ab weder aus dem Cash noch in die Sparrate. Im letzten Zahlungsjahr wird nur noch der Restbetrag fällig, nicht die volle Rate.
  • plannedSaveRate (Kopf-Kennzahl) ist die tatsächliche Rate des ersten Phasenjahres. In späteren Jahren kann sie tiefer liegen, wenn eine Schuld ausläuft.
  • cashNegative wird gesetzt, sobald der Cash-Bestand irgendwann innerhalb der Phase unter 0 fällt auch wenn er am Phasenende wieder positiv ist.
  • Der Ruin bezieht sich auf das Gesamtvermögen inkl. Immobilien, nicht auf das Cash.
  • ruinAge wird nur einmal gesetzt (erstes Auftreten, plan-weit).

Referenz: src/lib/calculations.ts Zeilen 365425.

4.8 Endwerte und Phasen-Kennzahlen

Einkommen:  startValue = basis
            endValue   = basis × (1 + idx/100)^(duration1)
Ausgaben:   startValue = basis × cumInflStart
            endValue   = basis × (1 + idx/100)^(duration1) × flowDeflatorEnd
Assets:     endValue   = a.value    (nach der Jahresschleife)
Immobilie:  endValue   = re.value  re.mortgage   (Verkehrswert inkl. Wertsteigerung, minus Restschuld)
Schulden:   endValue   = d.owed                  (0, falls getilgt; + Notiz „Wird getilgt")

Aggregate:

startWealthNominal = Σ Element-Startwerte + cashStart
endWealthNominal   = Σ Element-Endwerte   + cashEnd
endWealthReal      = endWealthNominal / cumulativeInflationEnd
isConsumption      = quotaStart < 0
incomplete         = cashNegative              // „roter Status" = Liquiditätslücke
capitalInflow      = incomingInflow            // aus dem Übergang IN diese Phase
capitalInvest      = investmentsFromCash + incomingImmediateRepay
oneOffInflow       = incomingOneOffInflow      // einmaliger Zufluss (netto nach Steuer)
oneOffOutflow      = incomingOneOffOutflow     // einmalige Kosten (nominal)

4.9 Der Übergang

Nach jeder Phase (auch nach der letzten) läuft die Übergangs-Logik. Sie liest transitionValues[phase.id] der Übergang ist also am Von-Phasen-Schlüssel gespeichert.

ownerRetiresNext = owner existiert
                 ∧ nextPhase existiert
                 ∧ owner ist in DIESER Phase erwerbstätig
                 ∧ owner.age + yearsBefore + duration >= owner.retirementAge

INCOME/EXPENSE sowie nicht-aktive Elemente überspringen die Logik (nur hasCarry = true).

4.9.1 PENSION_FUND

Pensions-Übergang (ownerRetiresNext), value = ec.endValue, Default-Modus PENSION:

payoutMode Wirkung
CAPITAL txInflow += round(value × (1 capitalTaxRate/100)); carry.value = 0; pkPensionAnnual = 0
PENSION carry.pkPensionAnnual = round(value × conversionRate / 100); carry.value = 0
COMBI capital = min(value, capitalAmount); txInflow += round(capital × (1 tax/100)); pkPensionAnnual = round((value capital) × conversionRate / 100); carry.value = 0

Normaler Übergang (Vorbezug) brutto entnommen, netto ins Cash:

withdrawal  = min(ec.endValue, round(td.withdrawal))     // brutto
carry.value = ec.endValue  withdrawal
txInflow   += round(withdrawal × (1  capitalTaxRate/100))

4.9.2 PILLAR_3A

Pensions-Übergang: immer vollständiger Bezug txInflow += round(ec.endValue × (1 capitalTaxRate/100)); carry.value = 0. Normaler Übergang (Vorbezug): wie PK Bruttoentnahme, Netto-Zufluss nach Kapitalbezugssteuer.

4.9.3 OTHER_ASSET

decision Wirkung
SELL txInflow += ec.endValue; carry.status = "SOLD" (kein Steuerabzug)
PARTIAL part = min(ec.endValue, partialSaleAmount); txInflow += part; carry.value = ec.endValue part Rest bleibt aktiv und wächst weiter
HOLD / sonst carry.value = ec.endValue

Der Teilverkauf (Roadmap Nr. 42) fliesst über txInflow ins Cash der Folgephase und erscheint dort im Phasenkopf als „Kapitalzufluss" (capitalInflow) das „zu investierende Kapital". Er ist die einmalige Entnahme am Übergang; die laufende Entnahme während einer Phase bleibt die jährliche Bezugsrate (annualWithdrawal, 4.6.6). Beide dürfen koexistieren.

4.9.4 REAL_ESTATE

Gelesen werden die laufenden Werte aus der Jahresschleife (re.value, re.mortgage, re.purchase).

decision = "SELL":

gain     = max(0, salePrice  re.purchase)     // URSPRÜNGLICHER Kaufpreis, nicht der Verkehrswert
tax      = gain × (saleTaxRate / 100)
txInflow += round(salePrice  re.mortgage  tax)
carry.status = "SOLD"

Der Nettoerlös ist Verkaufspreis minus Hypothekenablösung minus Grundstückgewinnsteuer. Ein Verlustverkauf erzeugt keine Steuer (gain bei 0 geklammert).

Die Steuer bemisst sich am ursprünglichen Kaufpreis, nicht am zwischenzeitlich gestiegenen Verkehrswert deshalb führt das Modell beide getrennt (4.6.5). Nicht modelliert sind wertvermehrende Investitionen und die Haltedauer-Abstufung (Roadmap Nr. 23).

Halten (decision ≠ SELL) mit optionaler Sonderamortisation (Roadmap Nr. 15):

extra = min(re.mortgage, extraAmortization)     // am Restsaldo gekappt
txImmediateRepay += extra                        // Einmaltilgung aus dem Cash
carry.mortgage = re.mortgage  extra
carry.propertyValue / carry.propertyPurchase     // fortgeschrieben

Die Einmaltilgung bringt REAL_ESTATE auf Augenhöhe mit OTHER_DEBT (dort schon immediateRepayment, 4.9.5): Sie senkt die Restschuld und damit die künftige Zinslast und erscheint im Phasenkopf als „Kapitalinvestition" (capitalInvest). Damit lässt sich die indirekte Amortisation mechanisch nachbilden 3a wachsen lassen, bei Pensionierung ins Cash beziehen, dann per Sonderamortisation die Hypothek tilgen.

Die Steuerwirkung der indirekten Amortisation (3a-Abzug, erhaltener Zinsabzug) ist bewusst nicht modelliert; sie gehört zum zurückgestellten Steuer-Bündel (siehe 9.14).

4.9.5 OTHER_DEBT

carry.owed = ec.endValue
immediate  = min(carry.owed, round(td.immediateRepayment))
falls immediate > 0:
    carry.owed = immediate
    txImmediateRepay += immediate
falls carry.owed === 0 → carry.status = "SETTLED"

4.9.6 Cash: einmalige Sonderein-/ausgaben

Gelesen wird phase.cashTransition der Entscheid hängt an der Von-Phase. Er wird nur ausgewertet, wenn eine Folgephase existiert; nach der letzten Phase gibt es keinen Übergang, ein dort erfasster Betrag bleibt wirkungslos.

Der Umrechnungskurs zwischen real und nominal ist an dieser Grenze cumulativeInflation, also der Bestands-Deflator am Phasenende (vgl. 4.5.3) denn Cash ist ein Bestand, und das Ereignis fällt exakt auf die Grenze.

mode = cashTransition.mode ?? "NONE"

falls mode ∈ {INFLOW, BOTH}:                  // nominal erfasst
    brutto        = round(inflowAmount)
    txOneOffInflow = round(brutto × (1  inflowTaxRate/100))

falls mode ∈ {OUTFLOW, BOTH}:                 // real erfasst
    txOneOffOutflow = round(outflowAmount × cumulativeInflation)

Beide Grössen fliessen ausschliesslich ins Cash der Folgephase (4.9.7) und nicht in die Jahresschleife damit bleiben sie per Konstruktion aus Einkommen, Ausgaben und Quote heraus. Ein Zufluss/eine Kostenposition, die das Cash unter 0 drückt, wird über den bestehenden Startwert-Check der Folgephase (cashNegative = cash < 0) automatisch als Liquiditätslücke erkannt.

4.9.7 Abschluss des Übergangs

cashCarryIn            = cashEnd + txInflow + txOneOffInflow  txImmediateRepay  txOneOffOutflow
incomingInflow         = txInflow            // Kopf-Kennzahlen der Folgephase
incomingImmediateRepay = txImmediateRepay
incomingOneOffInflow   = txOneOffInflow      // inkl. Bezeichnung
incomingOneOffOutflow  = txOneOffOutflow     // inkl. Bezeichnung
yearsBefore           += duration

4.10 Ergebnisstruktur

PlanComputed {
  phases: PhaseComputed[]   // alle Kennzahlen je Phase, inkl. elements[]
  yearly: YearPoint[]       // ein Punkt pro Jahr über alle Phasen (inkl. Vermögen je Jahr)
  nachlass: number          // = endWealthNominal der letzten Phase, sonst 0
  ruinAge: number | null    // Alter Person A beim ersten Gesamtvermögen < 0
}

4.11 Systemparameter

Zentral in src/lib/constants.ts geführt, weil sie sich periodisch durch Bundesanpassungen ändern:

Konstante Wert Bedeutung
AHV_MIN_MONTHLY_FULL 1'260 R0 Mindestbetrag der vollen Monatsrente (Skala 44). Alle Schwellen der Rentenformel sind Vielfache davon
AHV_PENSION_MONTHS 13 Rentenzahlungen pro Jahr 13. Altersrente ab 1.1.2026
AHV_CONTRIBUTION_START_AGE 21 Beitragspflicht ab 1. Januar nach dem 20. Geburtstag
AHV_MAX_ANNUAL_SINGLE 32'760 abgeleitet: 2 × R0 × 13
AHV_GROSS_FROM_NET_FACTOR 1.12 Netto → Brutto für die AHV; Herleitung siehe 4.4.5
AHV_COUPLE_CAP_FACTOR 1.5 Ehepaar-Plafonierung: 150 % der Einzel-Maximalrente
AHV_FULL_CONTRIBUTION_YEARS 44 Volle Beitragsdauer (Rentenskala 44)
PILLAR_3A_MAX_ANNUAL 7'258 Max. 3a-Beitrag/Jahr für PK-Versicherte (2026)
DEFAULT_PK_CONVERSION_RATE 6 % Umwandlungssatz
DEFAULT_CAPITAL_TAX_RATE 8 % Kapitalbezugssteuer
DEFAULT_PROPERTY_GAINS_TAX_RATE 20 % Grundstückgewinnsteuer

Die drei Default-Sätze werden sowohl als UI-Vorschlag als auch in der Berechnung als Fallback 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:

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.

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). 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.12.7 Zwei Welten, vier Fälle

Die Simulation läuft immer zweimal einmal um die historischen Renditen, einmal um die geplanten Werte des Plans. Aus jeder der beiden Verteilungen werden zwei Schwellen abgelesen: der Plan-Endbetrag (read-only, der geplante Nachlass des Szenarios) und der Zielbetrag (manuell, einer für alle Szenarien). Das ergibt vier Zahlen:

Schwelle Plan-Endbetrag Schwelle Zielbetrag
Welt historisch (Eingabe) Fall 1 Realismus der Planung Fall 3 Ziel, wenn der Markt wie früher läuft
Welt geplant (aus dem Plan) Fall 2 Nullpunkt für das Urteil Fall 4 Ziel, wenn der Markt wie geplant läuft

Das baut auf der Trennung der zwei Renditezahlen aus 4.12.3 auf. Die Streuung ist in beiden Welten dieselbe; nur der Mittelpunkt unterscheidet sie. In der geplanten Welt kommt der Mittelpunkt je Element aus plannedReturnOf (erste Phase, bei Immobilien die Wertsteigerung) und die Inflation aus plan.inflationRateDefault.

Warum zwei Läufe und nicht vier. Eine Wahrscheinlichkeit P(Endvermögen ≥ X) hängt nur von der Verteilung und von X ab. Wird das gesamte Endvermögen eines Laufs sortiert aufbewahrt (finalWealthSorted), lässt sich jede Schwelle nachträglich per Binärsuche ablesen (probabilityAtLeast). Zwei Läufe genügen also für vier Zahlen halbe Rechenzeit, und vor allem:

Fall 1 und Fall 3 stammen aus derselben Verteilung. Liegt der Zielbetrag unter dem Plan-Endbetrag, ist Fall 3 damit zwingend ≥ Fall 1. Der Widerspruch, der die alte Darstellung unlesbar machte («Planung 69 %, Ziel 3 Mio nur 41 %» zwei verschiedene Welten gegeneinander gestellt), ist strukturell nicht mehr möglich. Ein Test sichert das ab.

Der Nullpunkt ist Fall 2, nicht 50 %. Naiv erwartet man, dass der eigene Plan-Endbetrag in der eigenen geplanten Welt in ~50 % der Fälle erreicht wird. Das ist falsch: Der Volatilitäts-Drag drückt den Median unter den deterministischen Pfad, und zwar umso stärker, je höher die Streuung (gemessen: 37 % bei σ = 15 %, 27 % bei σ = 25 %; siehe 9.26). Fall 2 misst genau diesen Nullpunkt und wird deshalb als Referenz ausgewiesen, gegen die Fall 1 verglichen wird beides derselbe Schwellwert, nur andere Renditeannahme.

Urteil (Toleranzband ± 5 Prozentpunkte):

Fall 1 vs. Fall 2 Urteil
mehr als 5 pp höher Planung zurückhaltend gerechnet
innerhalb ± 5 pp Planung realistisch gerechnet
mehr als 5 pp tiefer Planung tendenziell zu optimistisch

Darstellung (bei einem einzelnen Szenario):

  • Prominent: Fall 1 als grosse Prozentzahl mit dem Urteil.
  • Untergeordnet: Fall 3 und 4 als Satzpaar, dessen Wortwahl automatisch dreht, je nachdem welche Welt besser dasteht («Wenn der Markt so schlecht läuft, wie du es in deiner Planung vorgibst, … bei X %. Läuft er so gut wie in der Vergangenheit, … bei Y %.»).
  • Kleingedruckt: Fall 2 als Referenz plus der Median je Welt.

Die Tabelle zeigt je Szenario Fall 1 (Fall 2) und Fall 3 (Fall 4). Ruin, Bandbreite und Fächer stammen immer aus der historischen Welt, weil das das ehrliche Risikobild ist.

Eingaben: Da beide Welten immer laufen, sind die historischen Mittelwerte und der Zielbetrag stets Pflichtfelder. Der frühere Drei-Wege-Umschalter entfällt.

Technik: kein Eingriff in den Rechenkern. Beide Durchgänge laufen mit demselben Seed (Common Random Numbers), damit der Unterschied zwischen den Welten nicht Rauschen ist.

Referenz: src/lib/montecarlo.ts (finalWealthSorted, probabilityAtLeast, plannedReturnOf), 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), 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).

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.

4.13.5 Wirkungslose Treiber werden erklärt

Ein Balken mit Spannweite 0 ohne Erklärung ist die schlechteste Antwort der Benutzer hält ihn für einen Fehler. computeTornado hängt deshalb an jeden Nullbalken eine Begründung (ineffectiveReason).

Der wichtigste Fall ist die Immobilien-Wertsteigerung bei einem Verkauf. Der Verkaufserlös ist Verkaufspreis Hypothek Grundstückgewinnsteuer und hängt damit am erfassten Verkaufspreis, nicht am modellierten Verkehrswert. Wird die Immobilie vor Planende verkauft, wird die aufgelaufene Wertsteigerung an dieser Stelle verworfen der Treiber kann das Endvermögen dann rechnerisch nicht mehr beeinflussen.

Erkannt wird das daran, dass in der letzten Phase alle REAL_ESTATE-Elemente den Status SOLD tragen. Andernfalls greift ein allgemeiner Hinweis. Durch Tests abgedeckt: gehalten → Spannweite > 0 ohne Hinweis; verkauft → Spannweite 0 mit Begründung.

Verwandt: Der Verkaufspreis-Abgleich im Übergangs-Dialog (3.5.8) setzt an derselben Stelle an, nur früher er verhindert, dass die Annahmen überhaupt auseinanderlaufen.

4.14 Verlaufswerte, Brücken und Rechenwege

Dieses Kapitel beschreibt, was computePlan über die reinen Ergebniswerte hinaus mitführt die Grundlage der Detailansichten (Roadmap Nr. 43) und der Transparenz-Offenlegung (Nr. 41).

4.14.1 Verlaufswerte je Element

ElementPhaseComputed.yearly hält einen Punkt pro Phasenjahr je Element (Jahr, Alter, Wert; bei Immobilien zusätzlich Verkehrswert und Restschuld).

Der Grund ist derselbe wie beim Vermögensverlauf in Version 0.9: Nur Start- und Endwert zu kennen reicht nicht. Eine Anlage mit 5 % über 20 Jahre wächst konvex eine Gerade zwischen den Phasengrenzen wäre sichtbar falsch und würde den Zinseszins optisch unterschlagen. Der Punkt wird am Jahresende erfasst, nach Verzinsung und Tilgung, konsistent zum YearPoint.

4.14.2 Die beiden Wasserfälle

Ein Wasserfall zerlegt eine Veränderung in ihre Summanden. Der entscheidende Punkt ist, welche Posten dazugehören und hier liegt eine Falle:

Sparraten, Amortisationen und Zusatzinvestitionen sind Umbuchungen, keine Verluste. Eine 3a-Einzahlung verlässt das Cash und erhöht im selben Zug das 3a-Guthaben; das Vermögen bleibt unverändert. Eine Amortisation senkt das Cash und die Hypothek das Eigenkapital steigt sogar. Als Abgang im Vermögens-Wasserfall gezeichnet würden diese Posten eine Vermögensminderung vortäuschen, die es nicht gibt.

Deshalb gibt es zwei Zerlegungen mit unterschiedlichen Fragestellungen:

WealthBridge „Warum hat sich mein Vermögen so entwickelt?"

Vermögen Ende Vorphase
  + einmaliger Zufluss    einmalige Kosten          ⎫
   Steuern am Übergang (Kapitalbezug, Grundstückgewinn) ⎬ Vermögensänderungen
   verrentetes PK-Kapital (verlässt die Bilanz)     ⎪ AN der Phasengrenze
  ± Verkaufspreis minus Verkehrswert                  ⎭
= Vermögen zu Phasenbeginn
  + Spar-/Verzehrquote (Summe über alle Phasenjahre)  ⎫
  + Kapitalerträge (PK, 3a, Sonstiges Vermögen)       ⎬ INNERHALB der Phase
  + Wertsteigerung der Liegenschaft                   ⎪
  + PK-Beiträge                                       ⎭
= Vermögen am Phasenende

Zwei Posten verdienen eine Erläuterung:

  • PK-Beiträge sind ein echter Zugang. Sie belasten das Cash nicht (im Nettolohn bereits abgezogen), erhöhen aber das Vorsorgekapital anders als 3a-Beiträge, die aus dem Cash fliessen und deshalb reine Umbuchung sind.
  • Verrentetes PK-Kapital verlässt die Bilanz. Bei der Verrentung wird Kapital in einen Rentenstrom umgewandelt; der Saldo fällt auf 0. Ohne diesen Posten ginge die Brücke am Pensions-Übergang nicht auf.

CashBridge „Wohin ist mein Cash geflossen?"

Cash Ende Vorphase (Phase 1: Cash-Anfangswert)
  + Kapitalzufluss + einmaliger Zufluss
   Sofort-Tilgung/Sonderamortisation  einmalige Kosten
   Investitionen am Phasenanfang
= Cash zu Phasenbeginn
  + Spar-/Verzehrquote  Sparraten  Amortisationen/Tilgungen + Bezugsraten
= Cash am Phasenende

Beide Strukturen führen einen Restposten (residual) mit: die Differenz zwischen dem gerechneten Endwert und der Summe der Summanden. Er ist die eingebaute Selbstkontrolle ist die Zerlegung vollständig und richtig, muss er exakt 0 sein.

src/lib/bridges.test.ts nagelt das über sieben Plankonstellationen fest (Ansparen mit 3a und Schuldentilgung, Pensionierung mit Verrentung und 3a-Bezug, PK-Kapitalbezug, Immobilie gehalten, Immobilie verkauft, einmalige Sonderein-/ausgaben, Sofort-Tilgung mit Sonderamortisation) je Phase für beide Brücken, zusätzlich der Abgleich der Kontrollpunkte gegen startWealthNominal / endWealthNominal / cashStart / cashEnd.

Im UI wird ein Restposten über 2 Franken als Fehlermeldung ausgewiesen, nicht als beiläufige Rundungsnotiz: Eine Brücke, die nicht aufgeht, ist ein Rechenfehler und kein Darstellungsproblem.

4.14.2.1 Darstellung der Wasserfälle

Die Wasserfälle werden nicht mit Recharts gezeichnet. Ein Wasserfall lebt von drei Dingen, die dort nicht ohne Weiteres zu bekommen sind:

  • Verbindungslinien zwischen den Balken ohne sie sieht man nicht, dass jeder Balken dort ansetzt, wo der vorherige aufhört, und die Grafik zerfällt in unverbundene Rechtecke.
  • Wertbeschriftung an jedem Schritt, statt Beträge aus der Achse zu schätzen.
  • Unterscheidung von Zwischenstand und Veränderung. Ein Zwischenstand („Vermögen Phasenbeginn") ist ein absoluter Wert ab Null, eine Veränderung („Kapitalerträge") setzt auf dem laufenden Saldo auf. Sehen beide gleich aus, ist die Grafik nicht lesbar.

Die Darstellung ist deshalb eine eigene HTML/CSS-Konstruktion und liegend statt stehend die Beschriftungen sind lang und müssten stehend gedreht werden; liegend ist es ausserdem konsistent zum Tornado. Abschnitts-Überschriften trennen „Am Übergang in diese Phase" von „Innerhalb der Phase". Posten mit Wert 0 werden gar nicht erst gezeichnet.

Darunter steht aufklappbar eine Tabelle mit laufendem Zwischenstand. Bei sieben bis zwölf Schritten mit stark unterschiedlichen Grössenordnungen ist sie der Grafik schlicht überlegen die Grafik zeigt das Verhältnis, die Tabelle die Zahl.

4.14.3 Rechenweg-Protokoll

computePlan(plan, sample?, { explain: true }) legt zu jedem Ergebnis die Herleitung ab:

interface TraceStep { label; formula?; substituted?; result; unit?; note? }
interface Trace     { title; specAnchor?; steps: TraceStep[] }

formula ist die abstrakte Regel, substituted dieselbe Regel mit den eingesetzten Zahlen, note benennt eine an dieser Stelle geltende Vereinfachung.

Der Architekturentscheid dahinter ist der wichtigste Teil dieses Kapitels. Die naheliegende Alternative wäre, die Formeln im UI nachzurechnen und anzuzeigen. Das ergäbe eine zweite Implementierung jeder Formel und damit die Möglichkeit, dass die angezeigte Herleitung still von der tatsächlichen Rechnung abdriftet. Bei einem Tool, dessen Kernversprechen die rechnerische Korrektheit ist, wäre das die gefährlichste Variante überhaupt: Ein Nutzer, der nachrechnet und eine Abweichung findet, verliert mehr Vertrauen, als eine Black Box je gekostet hätte.

Deshalb entstehen die Schritte innerhalb von computePlan, als Nebenprodukt der Rechnung, die ohnehin läuft. Die Erklärung ist die Rechnung.

Zwei Konsequenzen daraus:

  • Standardmässig aus. Die Monte-Carlo-Simulation ruft computePlan zehntausendfach auf und darf von der Protokollierung nichts merken. Ein Test prüft, dass explain die Ergebniswerte nicht verschiebt.
  • Die Arithmetik wird nicht umgestellt. Wo für das Protokoll eine Zwischengrösse gebraucht wird (etwa der Renditeanteil eines Jahres), wird sie als Differenz abgeleitet statt die Formel umzuformen a × (1 + r) und a + a × r sind in Gleitkomma-Arithmetik nicht bitgleich. Die 43 Golden Tests laufen unverändert.

Abdeckung. Vollständig über alle Ebenen: Element je Phase (Einkommen, Ausgaben, PK, 3a, Immobilie, Sonstiges Vermögen, Schulden, AHV-/PK-Renten), Element je Übergang (Bezugsarten, Verkauf inkl. Grundstückgewinnsteuer, Teilverkauf, Sonderamortisation, Sofort-Tilgung), Phasen-Kennzahlen (Quote, Spar-/Verzehrrate, Cash-Fortschreibung, Vermögen, Phasentyp, Cash-Übergang) und Plan-Ebene (Deflatoren, AHV-Karriere je Person, Ruinalter).

Verweis in die Spezifikation. Jeder Trace trägt optional einen specAnchor auf das zugehörige Kapitel dieses Dokuments; die App springt von dort in die eingebaute SPEZIFIKATION-Ansicht (die Anker erzeugt rehype-slug). Ein Test liest SPEZIFIKATION.md und prüft, dass jeder Verweis eine existierende Überschrift trifft sonst würden die Links bei einer Umbenennung still ins Leere zeigen.

Wo die Erklärung gerechnet wird. Nicht auf dem Server: computePlan ist rein und läuft im Browser (wie schon bei Monte Carlo und Sensitivitätsanalyse), und der Client hält den PlanInput ohnehin. Die Detailansicht rechnet die erklärte Fassung beim Öffnen lokal kein API-Umbau, keine grössere Antwort, keine Serverlast, und per Konstruktion identisch zum 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) noch «wie wahrscheinlich ist das» (das ist Monte-Carlo, 4.12), 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): 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

5.1 Technologie-Stack

Bereich Technologie Version
Framework Next.js (App Router) 16.2.10
UI React 19.2.4
Sprache TypeScript ^5
Styling Tailwind CSS ^4 (via @tailwindcss/postcss)
Icons lucide-react ^1.24.0
Charts recharts ^3.9.2
ORM Prisma ^7.8.0 (Client-Output nach src/generated/prisma)
Datenbank PostgreSQL 16-alpine
DB-Treiber pg + @prisma/adapter-pg ^8.22.0 / ^7.8.0
Validierung Zod ^4.4.3
Auth jose (JWT) + bcryptjs ^6.2.3 / ^3.0.3
Tests Vitest ^4.1.10
Build Docker (multi-stage), output: "standalone"

Hinweis für Entwickler: Gemäss AGENTS.md weicht diese Next.js-Version von verbreiteten Konventionen ab. Vor Änderungen ist der relevante Guide unter node_modules/next/dist/docs/ zu konsultieren.

5.2 Verzeichnisstruktur

FPT/
├── prisma/
│   ├── schema.prisma            Datenmodell
│   └── migrations/              17 Migrationen (chronologisch, siehe 5.4.6)
├── src/
│   ├── app/
│   │   ├── api/                 Route Handlers (siehe Kapitel 6)
│   │   ├── login/page.tsx       Login-/Registrierseite
│   │   ├── 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/              23 React-Komponenten (alle "use client")
│   ├── generated/prisma/        Generierter Prisma-Client (nicht editieren)
│   ├── lib/                     Domänenlogik (siehe 5.3)
│   └── middleware.ts            Zugriffsschutz (Edge-Runtime)
├── Info Dateien/                Fachdokumente, Roadmap (historisch)
├── docker-compose.yml, Dockerfile, docker-entrypoint.sh
└── .gitea/workflows/deploy.yaml CI/CD

5.3 Schichtenmodell und src/lib

Ein bewusster Entkopplungs-Entscheid (src/lib/types.ts Zeilen 14): Die Berechnungslogik arbeitet auf eigenen Domänentypen, nicht auf den generierten Prisma-Typen. Dadurch ist computePlan ohne Datenbank testbar.

Prisma-Modelle (DB)
      │  toPlanInput()   ← queries.ts: parst + validiert die JSON-Felder
      ▼
PlanInput (types.ts)     ← reine Domänentypen
      │  computePlan()   ← calculations.ts: pure function
      ▼
PlanComputed             ← an den Client geliefert
Datei Verantwortung
calculations.ts Berechnungskern + CSV-Export. Keine I/O.
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), 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; zusätzlich applyElementDriver / tunableElements für die einzeln regelbaren Element-Renditen. Rein, läuft im Browser.
actuals.ts Effektive Werte: Zuordnung auf die Szenario-Elemente über die Herkunfts-Kette, Einspielen in den Rechenkern, Bestand/Fluss. Rein.
report.ts Berichtsmodell: Kennzahlen, Annahmen, Vergleich, Plan/Ist -- rein, ohne PDF-Kenntnisse (Kap. 3.11).
report-pdf.ts Zeichnet das Berichtsmodell mit pdfkit. Nur serverseitig.
decisions.ts Offene Übergangs-Entscheide -- von Matrix UND Bericht benutzt, damit beide dieselbe Zahl nennen.
analyses.ts Gespeicherte Analysen: Typen, generische Ergebnis-Form, Speicher-Helfer (Kap. 3.10).
dataview.ts Bündelt Plan-Sicht und Ist-Sicht für Matrix, Grafiken und Analysewerkzeuge. Rein.
ratefields.ts Ratenfelder je Kategorie, Reichweite der Übernahme über Lebensphasen, Aufbau der Schreibvorgänge (Kap. 3.6.11). Rein.
versioning.ts Versionierung: Nummerierung A.B, Zusammenfassung je Sitzung, Bauplan des Wiederherstellens, Auswirkung auf Kind-Szenarien. Rein, ohne I/O.
versioning-db.ts Datenbank-Anbindung der Versionierung: Snapshot festhalten, Hauptversion, Wiederherstellen. Führt den Bauplan aus versioning.ts nur aus.
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
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)
session.ts getCurrentUserId() aus dem Cookie (Node-Runtime)
users.ts Registrierung, Credential-Prüfung, Passwortwechsel (bcrypt). Nie aus der Middleware importieren Edge-Runtime hat keinen DB-Zugriff.
format.ts formatChf / parseChfInput
theme.ts Theme-Verwaltung (localStorage + data-theme)
api-client.ts Typisierter fetch-Wrapper mit einheitlicher Fehlerextraktion

5.4 Datenmodell

5.4.1 Tabellen

User

Feld Typ Constraints
id String PK, cuid()
username String unique
passwordHash String bcrypt, Cost 12
createdAt DateTime now()

Person

Feld Typ Constraints
id String PK, cuid()
scenarioId String FK → Scenario, Cascade
role PersonRole PERSON_A | PERSON_B
name String? optional
age Int aktuelles Alter
retirementAge Int szenario-eigenes Pensionsalter
@@unique([scenarioId, role])

Plan (Behälter trägt keine Finanzdaten)

Feld Typ Constraints
id String PK, cuid()
userId String FK → User, Cascade
name String
createdAt / updatedAt DateTime

Scenario (die berechenbare Einheit)

Feld Typ Constraints
id String PK, cuid()
planId String FK → Plan, Cascade
name String
isBase Boolean Default false; genau eines je Plan ist true
parentScenarioId String? FK → Scenario (Self-Relation „ScenarioTree"), SetNull Baum und Vergleichsbasis
householdType HouseholdType SINGLE | COUPLE
inflationRateDefault Float szenario-weite Inflation in %
initialCash Float Default 0
startYear Int? Kalenderjahr des Planbeginns nur Darstellung (siehe 3.2.7)
createdAt / updatedAt DateTime

Phase

Feld Typ Constraints
id String PK, cuid()
scenarioId String FK → Scenario, Cascade
sequenceNumber Int 1-basiert, lückenlos
name String
durationYears Int 180
cashTransition Json? Cash-Entscheid beim Übergang nach dieser Phase (siehe 5.4.5)
sourcePhaseId String? Gegenstück in der Vorlage (lose Referenz, kein FK) Diff-Grundlage
createdAt / updatedAt DateTime
@@unique([scenarioId, sequenceNumber])

cashTransition liegt an der Phase und nicht in ElementTransitionValue, weil Cash kein FinancialElement ist und damit keine elementId besitzt. Die Verschlüsselung folgt derselben Logik wie dort: Der Übergang gehört der Von-Phase.

FinancialElement

Feld Typ Constraints
id String PK, cuid()
scenarioId String FK → Scenario, Cascade
category ElementCategory 8 Werte
name String
ownerRole OwnerRole? PERSON_A | PERSON_B | HOUSEHOLD
orderIndex Int Default 0
sourceElementId String? Gegenstück in der Vorlage (lose Referenz, kein FK) Diff-Grundlage
createdAt DateTime

Die Herkunfts-Verweise sind bewusst lose (kein Fremdschlüssel): Wird das Gegenstück in der Vorlage gelöscht, soll die Kopie bestehen bleiben und im Diff einfach als „neu" gelten ein Cascade wäre hier falsch.

ElementPhaseValue

Feld Typ Constraints
id String PK, cuid()
elementId String FK → FinancialElement, Cascade
phaseId String FK → Phase, Cascade
data Json Payload gemäss PhaseData
@@unique([elementId, phaseId])

ElementTransitionValue

Feld Typ Constraints
id String PK, cuid()
elementId String FK → FinancialElement, Cascade
fromPhaseId String FK → Phase, Cascade
data Json Payload gemäss TransitionData
@@unique([elementId, fromPhaseId])

5.4.2 Warum JSON?

Die kategoriespezifischen Felder liegen als JSON, weil sich sonst pro Kategorie eine eigene Tabelle (oder eine breite Sparse-Tabelle) ergäbe. Die Typisierung und Validierung findet in der Applikationsschicht statt (src/lib/elements.ts): PhaseData / TransitionData als TypeScript-Interfaces, phaseDataSchema / transitionDataSchema als Zod-Schemas an der API-Grenze.

Die Interfaces sind bewusst tolerant getippt (alle Felder optional): die Berechnung liest defensiv über num(value, fallback), das UI zeigt kontextabhängig nur die relevanten Felder.

Referenz: prisma/schema.prisma Zeilen 46, src/lib/elements.ts Zeilen 4851.

5.4.3 JSON-Payload PhaseData

Feld Kategorien Zod-Regel
amount INCOME, EXPENSE ≥ 0
teuerungsausgleich INCOME, EXPENSE 20 bis 50
gapYears AHV Integer ≥ 0
avgIncomeBefore AHV nur wenn bei Planbeginn bereits pensioniert ≥ 0, real
gapYearsBefore AHV dito Integer 050
currentValue PENSION_FUND, PILLAR_3A ≥ 0
startValue OTHER_ASSET, OTHER_DEBT ≥ 0
expectedReturn PK, 3a, OTHER_ASSET 50 bis 100
annualContribution PK, 3a, OTHER_ASSET ≥ 0
annualWithdrawal OTHER_ASSET ≥ 0
additionalInvestment PK, 3a, OTHER_ASSET (ab Phase 2) ≥ 0
purchasePrice REAL_ESTATE ≥ 0
mortgage REAL_ESTATE ≥ 0
amortization REAL_ESTATE ≥ 0
interestRate REAL_ESTATE Hypothekarzins %/Jahr 020
interestHandling REAL_ESTATE Doppelzählungs-Schalter INCLUDED (Default) | ADD
valueGrowth REAL_ESTATE Wertsteigerung %/Jahr auf die Liegenschaft 20 bis 20
annualRepayment OTHER_DEBT ≥ 0

5.4.4 JSON-Payload TransitionData

Feld Kategorien Zod-Regel
reviewed AHV (Pensions-Übergang) Beitragskarriere geprüft Boolean
avgIncomeBefore AHV (Pensions-Übergang) ≥ 0, real
gapYearsBefore AHV (Pensions-Übergang) Integer 050
withdrawalMode PK, 3a (normal) NONE | AMOUNT
withdrawal PK, 3a (normal) ≥ 0, brutto
payoutMode PK (Pensionierung) CAPITAL | PENSION | COMBI
capitalAmount PK (COMBI) ≥ 0
conversionRate PK 020
capitalTaxRate PK + 3a, sowohl Vorbezug als auch Pensionierung 0100
decision REAL_ESTATE, OTHER_ASSET HOLD | SELL | PARTIAL
salePrice REAL_ESTATE ≥ 0
partialSaleAmount OTHER_ASSET (Teilverkauf) ≥ 0
extraAmortization REAL_ESTATE (Sonderamortisation) ≥ 0
saleTaxRate REAL_ESTATE 0100
immediateRepayment OTHER_DEBT ≥ 0

5.4.5 JSON-Payload CashTransitionData

Liegt in Phase.cashTransition. Validierung über cashTransitionSchema.

Feld Bedeutung Zod-Regel
mode Entscheid NONE | INFLOW | OUTFLOW | BOTH
inflowLabel Bezeichnung des Zuflusses (z. B. „Erbschaft") ≤ 120 Zeichen
inflowAmount Betrag nominal ≥ 0
inflowTaxRate Steuer auf den Zufluss, Default 0 % 0100
outflowLabel Bezeichnung der Kosten (z. B. „Poolbau") ≤ 120 Zeichen
outflowAmount Betrag real (heutige Kaufkraft) ≥ 0

Pro Übergang ist genau ein Zufluss und eine Kostenposition möglich siehe 9.7.

Alle Schemas verwenden .strip() unbekannte Felder werden verworfen, nicht abgelehnt. Beim Lesen aus der DB gilt zusätzlich: schlägt safeParse fehl, wird {} zurückgegeben (parsePhaseData / parseTransitionData) korrupte Daten führen also nie zu einem Absturz, sondern zu leeren Werten.

5.4.6 Migrationshistorie

Migration Inhalt
20260708171600_init Initiales Schema
20260709010000_add_transition_automation Übergangs-Automatik
20260709120000_rework_realestate Immobilien-Überarbeitung
20260711090000_multi_user User-Entität, Ownership
20260713100000_element_model_rework Elemente auf Plan-Ebene, Phase-/Transition-Werte als JSON
20260713150000_profile_to_plan_v3 Grundprofil von Household auf Plan verschoben
20260714120000_person_name Person.name
20260715120000_plan_initial_cash Plan.initialCash
20260716210000_drop_phase_inflation_rate Phase.inflationRate entfernt (Inflation ist plan-weit)
20260716230000_phase_cash_transition Phase.cashTransition (JSONB) für einmalige Sonderein-/ausgaben
20260718090000_plan_scenario_hierarchy V6: PlanScenario (IDs erhalten), neuer Behälter Plan, planIdscenarioId, Herkunfts-Verweise
20260718140000_scenario_start_year Scenario.startYear (Kalenderjahr des Planbeginns), bestehende auf das laufende Jahr gesetzt
20260719210000_scenario_versioning Tabelle ScenarioVersion (Snapshot als JSONB, A.B eindeutig je Szenario) und Scenario.currentMajor
20260720090000_actuals Tabelle ActualsSet (effektive Werte je Plan, Werte als JSONB, Cash separat)
20260720140000_plan_level_profile V7: Haushaltsform, Personen (Name/Alter) und Startjahr vom Szenario auf den Plan; neue Tabelle PlanPerson; Person behält nur das Pensionsalter. Datenübernahme aus dem Basisszenario.
20260721090000_reports Tabelle Report: gewählte Parameter, eingefrorenes Modell und die PDF-Datei als BYTEA
20260720160000_saved_analyses Tabelle SavedAnalysis (Eingaben + Ergebnis als JSONB, denormalisierte Kerndaten)

Zur V6-Migration: Sie benennt die bisherige Plan-Tabelle in Scenario um dadurch bleiben alle IDs und damit sämtliche Kind-Fremdschlüssel gültig. Für jedes bisherige Wurzel-Element entsteht ein neuer Behälter ('plan_' || id, deterministisch ableitbar, daher ohne Hilfstabelle); der bisherige Plan-Name wandert dorthin, das Szenario heisst „Basisszenario". Bestehende Szenarien werden per rekursivem CTE demselben Behälter zugeordnet auch mehrfach verschachtelte. Ein Sicherheitsnetz fängt verwaiste Szenarien ab und macht sie eigenständig.

Die Migration wurde vor dem Deploy gegen echtes PostgreSQL verifiziert (PGlite, in-process): alle Vorgänger-Migrationen einspielen, realistische Daten inkl. verschachtelter Szenarien anlegen, migrieren, Ergebnis und Cascade prüfen.

5.5 Frontend-Architektur

5.5.1 Datenfluss

page.tsx (Client)
   └─ GET /api/auth/me → username
      └─ AppShell
         ├─ GET /api/plans            → Planliste (Sidebar, Kacheln)
         ├─ GET /api/plans/<id>       → { plan: PlanInput, computed: PlanComputed }
         │   └─ PlanView   (Matrix, Dialoge)  → onChanged() → silent reload
         │   └─ Dashboard  (Kennzahlen, 3 Grafiken, Vergleich, Export)
         └─ Dialoge: PlanDialog, ScenarioDialog

Es gibt keinen State-Management-Layer (kein Redux/Zustand/React Query). Der Zustand lebt in AppShell (Planliste, ausgewählter Plan, Detail) und lokal in den Dialogen. Nach jeder Mutation wird der Plan neu geladen; die Berechnung kommt immer vom Server.

5.5.2 Komponenten

Komponente Zeilen Rolle
AppShell 433 Layout, Sidebar, Planliste, Laden, Plan-/Szenario-Dialoge
PlanView 1304 Matrix, Spaltenaufbau, Kontextbildung, alle Zell-Dialoge
ElementDetail 548 Feldgruppen je Kategorie/Kontext, Speicherlogik, Übergangs-Defaults
Dashboard 187 Kennzahlkarten, 3 Grafiken, Planvergleich, CSV-Link
FormField 275 NumberField, MoneyInput/MoneyField, TextField, SelectField
ProfileMenu 166 Benutzer, Theme-Wahl, Passwortwechsel, Logout
Timeline 108 Zeitachse mit Pensions- und Ruin-Markern
WealthChart 106 Vermögensverlauf, mehrere Serien
PlanProfileFields 101 Wiederverwendete Grundprofil-Felder
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
VersionHistoryDialog ~290 Änderungshistorie: Liste, Hauptversion festlegen, Anzeigen, Wiederherstellen mit Warnung (3.8)
VersionMatrix ~110 Nur-Lese-Matrix eines festgehaltenen Standes
PlanViews ~350 Plan-Dashboard, Szenario-Liste, Analysen-Kacheln + gespeicherte Liste (Kap. 3.10)
SavedAnalysisView ~180 Nur-Lese-Ansicht einer gespeicherten Analyse
SaveAnalysisButton ~90 Speichern-Knopf mit Namensvorschlag
VersionPicker ~140 Wahl der Berechnungsgrundlage in den vier Analysewerkzeugen
LiveSimDialog ~330 Live-Simulation: Regler links, Grafikwahl rechts, Kennzahlenleiste mit Differenz zum Plan (4.15)
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
SpecView 65 Rendert SPEZIFIKATION.md (via /api/spec) als lesbares Dokument, inkl. Sprungmarken aus den Rechenwegen
InfoBubble 28 Hilfe-Tooltip
ui ~370 UI-Primitiven: Button, Modal, InspectorShell, Confirm, Toast, Skeleton, EmptyState (3.7.6/3.7.7)
PlanWizard ~560 Geführter Plan-Assistent in fünf Schritten, abschnittsbasierte Phasenplanung (3.2.8)
Tour ~140 Interaktive Kurz-Tour über die Planansicht (3.7.8)
CommandPalette ~130 Befehls-Palette Ctrl/Cmd+K (3.7.9)
DistributionDialogs ~460 Verteil-Werkzeuge für Kapital und Spar-/Verzehrquote (3.6.10)
Sparkline ~45 Mini-Verlaufskurve je Element-Zeile (3.7.9)

5.5.3 Wiederverwendungsmuster

ElementPhaseFields und ElementTransitionFields sind aus ElementDetail exportiert und werden an drei Stellen wiederverwendet: im Zell-Dialog, im Erstell-Dialog („Werte (erste Lebensphase)") und im geführten Übergangs-Review. Dadurch gibt es genau eine Definition der Felder je Kategorie.

Der CellContext ist die einheitliche Kontext-Schnittstelle dieser Feldgruppen; er wird in PlanView durch buildPhaseContext / buildTransitionContext aus der Berechnung befüllt.

5.5.4 React-Detail: der key auf Dialogen

Zell-Dialoge tragen einen key aus elementId + phaseId. Grund (Kommentar Zeilen 479480): Beim Wechsel von Zelle zu Zelle wird ein Neuaufbau erzwungen, damit der lokale Formularzustand nicht vom vorher geöffneten Element übrig bleibt.

5.6 Sicherheit

Aspekt Umsetzung
Passwortspeicherung bcrypt, Cost 12
Session JWT HS256, 30 Tage, HttpOnly-Cookie, sameSite=lax, secure in Produktion
Secret SESSION_SECRET aus der Umgebung; Fehlen wirft beim ersten Zugriff
Autorisierung Middleware (grob) + Ownership-Query je Route (fein)
Information Disclosure Fremde/nicht existierende Ressourcen → einheitlich 404; Login-Fehler unspezifisch
Eingabevalidierung Zod an jeder API-Grenze; .strip() gegen Mass-Assignment
SQL-Injection Prisma (parametrisiert)
XSS React-Escaping; einziges dangerouslySetInnerHTML ist das statische Theme-Init-Script
CSRF Kein Token. Schutz beruht allein auf sameSite=lax siehe 9.4

5.7 Konfiguration

Variable Zweck
DATABASE_URL Postgres-Connection-String
SESSION_SECRET JWT-Signaturschlüssel (z. B. openssl rand -hex 32)
POSTGRES_PASSWORD Nur für docker-compose: Passwort des db-Containers
NODE_ENV Steuert u. a. das secure-Flag des Cookies und den Prisma-Global-Cache

Es gibt kein konfiguriertes Login-Passwort: Konten werden über die Registrierung angelegt.


6. API-Referenz

Alle Routen liefern JSON. Fehlerformat einheitlich: { "error": "<Meldung>" } (bei Zod-Fehlern in POST /api/plans: { "error": <flatten()-Objekt> }). Alle Routen ausser login/register erfordern ein gültiges Session-Cookie.

6.1 Authentifizierung

Methode Pfad Body Antwort
POST /api/auth/register { username, password } 201 { ok, username } + Cookie · 400 Validierung · 409 Name vergeben
POST /api/auth/login { username, password } 200 { ok, username } + Cookie · 400 · 401
POST /api/auth/logout 200 { ok }, Cookie gelöscht
GET /api/auth/me 200 { user: { id, username, createdAt } } · 401 · 404
POST /api/auth/change-password { currentPassword, newPassword } 200 { ok } · 400 · 401

6.2 Pläne

GET /api/plans

Liste der eigenen Pläne, sortiert nach createdAt aufsteigend.

{ "plans": [ { "id", "name", "parentPlanId", "branchFromPhaseId", "createdAt",
               "phases": [ { "id", "name", "sequenceNumber" } ] } ] }

POST /api/plans

{ "name": "Basisplan", "householdType": "COUPLE", "inflationRateDefault": 1.5,
  "persons": [ { "role": "PERSON_A", "name": "Anna", "age": 40, "retirementAge": 65 },
               { "role": "PERSON_B", "name": null,   "age": 38, "retirementAge": 64 } ] }

Validierung: name 1120; inflationRateDefault 20…50; persons 12 Einträge; age 0120; retirementAge 30100; name je Person ≤ 60. Zusätzlich Konsistenzregel SINGLE=1 / COUPLE=2 Personen. Legt Plan und Basisszenario an. → 201 { plan: { id }, scenario: { id } }

GET /api/plans liefert die Pläne inkl. Szenario-Kopfdaten:

{ "plans": [ { "id", "name", "createdAt",
               "scenarios": [ { "id", "planId", "name", "isBase", "parentScenarioId" } ] } ] }

PATCH /api/plans/<planId>

{ name } der Plan trägt nur noch den Namen. → 200 { plan: { id, name } }

DELETE /api/plans/<planId>

→ 200 { ok: true }, Cascade über alle Szenarien (inkl. Versionen und Ist-Sätzen).

GET /api/plans/<planId>/actuals

Alle erfassten Ist-Sätze, neueste zuerst (3.9):

{ "sets": [ { "id", "recordedOn": "2026-08-18", "year": 2026, "comment",
              "cash", "values": { "<rootElementId>": { "value", "mortgage" } },
              "author", "createdAt" } ] }

POST /api/plans/<planId>/actuals

{ recordedOn: "JJJJ-MM-TT", comment?, cash?, values } year wird aus dem Datum abgeleitet. → 201 { set: { id, year } }

DELETE /api/plans/<planId>/actuals/<setId>

→ 200 { ok: true }. Ein Ist-Satz ist eine Beobachtung es gibt weder Versionierung noch Wiederherstellung.

GET /api/plans/<planId>/dashboard

Gebündelte Kennzahlen fürs Plan-Dashboard und die Szenario-Liste (3.10): Haushaltsdaten, Zähler (Szenarien/Ist-Sätze/Analysen), Basisszenario-Kennzahlen inkl. Ist-Abweichung, und je Szenario Version/Elementzahl/Endvermögen/Ruinalter.

GET/POST /api/plans/<planId>/analyses

Gespeicherte Analysen GET listet die Kopfdaten (ohne die grossen JSON-Felder), POST legt eine Momentaufnahme an ({ name, type, metric, source, scenarioName?, versionLabel?, inputs, result }).

GET/DELETE /api/plans/<planId>/analyses/<analysisId>

GET liefert die vollständige Analyse inkl. der eingefrorenen Zahlen (Eingaben + Ergebnis), DELETE entfernt sie. Read-only es wird nichts neu gerechnet.

6.3 Szenarien

GET /api/scenarios/<scenarioId>

Liefert Eingabe, Berechnung und die Vergleichsbasis in einem Zug:

{ "plan": <PlanInput>, "computed": <PlanComputed>,
  "base": <PlanInput|null>,
  "meta": { "id", "planId", "planName", "name", "isBase", "parentScenarioId" } }

base ist das Eltern-Szenario (null beim Basisszenario) daraus rechnet der Client den Diff. Dies ist der einzige Endpunkt, der die Berechnung ausführt (neben export). → 404 wenn fremd.

PATCH /api/scenarios/<scenarioId>

Akzeptiert eine Union von zwei Formen:

  1. Vollständiges Profil: { householdType, inflationRateDefault, persons[], name? } ersetzt die Personen in einer Transaktion.
  2. Teilaktualisierung: { name?, initialCash? } initialCash 0…1'000'000'000, gerundet.

→ 200 { scenario: { id, name } }

DELETE /api/scenarios/<scenarioId>

→ 200 { ok: true } · 400 wenn es das Basisszenario ist.

POST /api/scenarios/<scenarioId>/copy

{ name } vollständige Kopie; setzt parentScenarioId sowie die Herkunfts-Verweise. → 201 { scenarioId: "<neue Id>" }

GET /api/scenarios/<scenarioId>/export

text/csv; charset=utf-8, Content-Disposition: attachment. Liefert immer den aktuellen Stand, auch wenn im Client eine ältere Version betrachtet wird (3.8.4).

GET /api/scenarios/<scenarioId>/versions

Änderungshistorie, neueste zuerst ohne die Snapshots (je zig Kilobyte).

{ "currentMajor": 2,
  "versions": [ { "id", "major", "minor", "comment", "isMajor",
                  "createdAt", "updatedAt", "author" } ] }

POST /api/scenarios/<scenarioId>/versions

{ comment } (Pflicht, ≥ 3 Zeichen) legt den aktuellen Stand als Hauptversion fest und setzt Scenario.currentMajor. → 201 { version: { major, minor } } · 400 ohne Kommentar.

GET /api/scenarios/<scenarioId>/versions/<versionId>

Ein einzelner Stand samt Berechnung und der Vorwarnung für das Wiederherstellen:

{ "version": { "id", "major", "minor", "comment", "isMajor", "createdAt" },
  "plan": <PlanInput>, "computed": <PlanComputed>,
  "impact": { "lostElementIds": [], "lostPhaseIds": [], "affectedChildren": [] } }

impact benennt die Kind-Szenarien, die durch ein Wiederherstellen ihre Diff-Basis verlören.

POST /api/scenarios/<scenarioId>/versions/<versionId>

Setzt das Szenario auf diesen Stand zurück IDs bleiben erhalten, und der wiederhergestellte Stand wird selbst als neue Version festgehalten («Wiederhergestellt aus A.B»). → 200 { version: { major, minor } }

6.4 Phasen

POST /api/scenarios/<scenarioId>/phases

Body optional: { name?, durationYears? }. Hängt eine Phase am Ende an, kappt die Dauer, vergibt Default-Name, legt vorbelegte ElementPhaseValue für alle aktiven Elemente an (alles in einer Transaktion). → 201 { phase: { id } }

PUT /api/phases/<phaseId>

{ name?, durationYears? } durationYears 180, wird gekappt. → 200 { phase: { id } }

DELETE /api/phases/<phaseId>

Nur die letzte Phase. → 200 { ok } · 400 „Nur die letzte Phase kann gelöscht werden."

PUT /api/phases/<phaseId>/cash-transition

Body = CashTransitionData (siehe 5.4.5). Speichert den Cash-Entscheid für den Übergang nach dieser Phase (einmalige Sonderein-/ausgaben). Schreibt die Spalte Phase.cashTransition. → 200 { ok } · 404 wenn die Phase nicht dem Benutzer gehört.

6.5 Elemente

POST /api/scenarios/<scenarioId>/elements

{ category, name, ownerRole? } → 201 { element: { id } }

  • PERSON_ONLY_CATEGORIES ohne Person → 400
  • fehlendes ownerRole sonst → HOUSEHOLD
  • orderIndex = Max + 1

PATCH /api/elements/<elementId>

{ name } (1120). → 200 { ok }

DELETE /api/elements/<elementId>

→ 200 { ok }, Cascade auf alle Phasen-/Übergangswerte.

PUT /api/elements/<elementId>/phase/<phaseId>

Body = PhaseData. Upsert auf @@unique([elementId, phaseId]). Prüft zusätzlich, dass die Phase zum selben Plan gehört wie das Element. → 200 { ok }

PUT /api/elements/<elementId>/transition/<fromPhaseId>

Body = TransitionData. Upsert auf @@unique([elementId, fromPhaseId]). → 200 { ok }


7. Betrieb und Deployment

7.1 Container

Dockerfile vierstufiger Multi-Stage-Build auf node:20-alpine:

  1. base Arbeitsverzeichnis /app
  2. deps npm ci (mit prisma/ für den postinstall-Hook prisma generate)
  3. builder npx prisma generate + npm run build
  4. runner nur Laufzeit-Artefakte; ENTRYPOINT ./docker-entrypoint.sh, CMD npm start, Port 3000

docker-entrypoint.sh führt vor dem Start npx prisma migrate deploy aus Migrationen laufen also automatisch bei jedem Container-Start.

7.2 docker-compose

Zwei Services:

  • app hängt an zwei Netzwerken: agent-net (extern, Traefik) und internal (DB).
  • db postgres:16-alpine, nur im internal-Netz, persistiert auf Volume fpt_db_data. Die Datenbank ist von aussen nicht erreichbar.

Traefik-Labels:

traefik.enable=true
traefik.docker.network=agent-net          ← kritisch, siehe unten
traefik.http.routers.fpt.rule=Host(`fpt.aicds.ch`)
traefik.http.routers.fpt.entrypoints=websecure
traefik.http.routers.fpt.tls.certresolver=myresolver
traefik.http.services.fpt.loadbalancer.server.port=3000

Zwei projektspezifische Fallstricke sind im Code dokumentiert bzw. durch die Konventionen gesetzt:

  1. traefik.docker.network=agent-net ist zwingend. Der Container hängt an zwei Netzwerken; ohne diese Angabe wählt Traefik zufällig eines landet es im internen DB-Netzwerk, ist das Backend unerreichbar (Timeout). (Kommentar in docker-compose.yml.)
  2. Der Router-Name muss projektspezifisch sein (hier fpt, nicht app), sonst kollidiert er mit anderen Projekten auf derselben VM.

7.3 CI/CD

.gitea/workflows/deploy.yaml: Push auf main = automatisches Live-Deployment (bewusst kein Review-Gate). Ablauf:

  1. Checkout
  2. .env aus den Gitea-Secrets SESSION_SECRET und POSTGRES_PASSWORD schreiben
  3. Code nach /opt/aicds/apps/FPT/ kopieren
  4. docker compose downdocker compose up -d --builddocker image prune -f

Zielumgebung: Hetzner CX23, Traefik als Reverse Proxy, Domain fpt.aicds.ch, Gitea unter git.aicds.ch.


8. Qualitätssicherung

8.1 Teststrategie

Getestet wird ausschliesslich der Berechnungskern bewusst, da dort die Fachlogik und das 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 15 Treiber-Transformationen (Reinheit, Einheiten, Kappung), Verfügbarkeit, Tornado-Sortierung und -Richtung, Erklärung wirkungsloser Treiber
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
actuals.test.ts 22 Zuordnung über Kopie-Ketten, Sprung im Ist-Jahr, Weiterrechnen ab dem Ist-Wert, geschlossene Brücken, Fluss-Rückrechnung
dataview.test.ts 7 beide Sichten in einem Zug, Rückfall ohne Ist-Werte, Abweichungsmessung
ratefields.test.ts 17 Erkennung geänderter Raten, Reichweite (diese/folgende/alle), Erhalt der übrigen Werte in den Zielphasen
versioning.test.ts 22 Nummerierung und Zusammenfassung je Sitzung, unveränderte Stände, Benutzertrennung, Bauplan des Wiederherstellens, Auswirkung auf Kind-Szenarien
versioning-coverage.test.ts 3 statischer Wächter: jeder schreibende Endpunkt löst eine Version aus
report.test.ts 9 Zusammenfassung, Basis-Angabe zu JEDER Kennzahl, Szenario-Deckelung, Plan/Ist-Block, Haftungsausschluss, gültige PDF-Datei
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 3 spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein; prüft zusätzlich die V7-Datenübernahme (Basisszenario gewinnt, Pensionsalter bleiben szenario-eigen)
Total 221

8.2 Testfälle

Test Prüft
Amtliche Tabelle 318.117.1 ahvMonthlyFullPension reproduziert 11 Stützstellen der amtlichen Rentenskala exakt (Mindestrente, Formel 1, Wendepunkt 45'360 → 1'915, Formel 2, Maximalrente)
Kappung / Stetigkeit unter 15'120 → 1'260, über 90'720 → 2'520; kein Sprung am Wendepunkt
AHV volle Karriere mdJE 100'000 → Maximalrente 32'760
AHV abgestuft / Mindestrente mdJE 60'000 → Formel 2; mdJE 10'000 → 1'260 × 13
AHV Vorgeschichte dominiert 39 Jahre à 40'000 + 5 Jahre à 200'000 → mdJE 58'182
AHV Ausfalljahre 4 Ausfalljahre → 32'760 × 40/44; Ausfalljahre im Plan senken nur die Skala, nicht das mdJE
AHV ohne Prüfung ohne erfassten Wert gilt der geplante Durchschnitt (nicht 0)
AHV bereits pensioniert Karriere aus der Phasenzelle der ersten Phase
AHV Plafonierung zwei Maximalrenten im Paar-Plan → gekappt auf 32'760 × 1.5
AHV Netto → Brutto mdJE = 70'000 × 1.12, nicht 70'000
Immobilie: Hebel 1 Mio / 900k Hypothek / 1 % / 10 J. → Endwert 304'622; deutlich mehr als eine Wertsteigerung auf das Eigenkapital ergäbe
Immobilie: ohne Wertsteigerung Verhalten unverändert (1 Mio 800k = 200'000)
Immobilie: Zins-Schalter INCLUDED → kein Cash-Abzug; ADD → 10 × 1 % von 900'000 = 90'000
Immobilie: Zins sinkt Jahr 1: 10'000, Jahr 10: 9'100; schlägt auf die Quote durch
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 %
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
Tornado: wirkungslose Treiber jeder Nullbalken trägt eine Begründung; verkaufte Immobilie → Wertsteigerung wirkungslos mit konkretem Hinweis, gehaltene Immobilie → Spannweite > 0 ohne Hinweis
Brücken: Restgrösse residual === 0 je Phase für Vermögens- und Cash-Brücke über sieben Plankonstellationen (Ansparen, Verrentung, Kapitalbezug, Immobilie gehalten/verkauft, Einmalposten, Sofort-Tilgung mit Sonderamortisation)
Brücken: Kontrollpunkte startWealth/endWealth/cashStart/cashEnd der Brücken stimmen mit den offiziellen Phasen-Kennzahlen überein
Brücken: Umbuchungen Sparraten und Amortisationen erscheinen nur in der Cash-Brücke; die Vermögensänderung erklärt sich exakt aus Quote + Erträgen + Wertsteigerung + PK-Beiträgen
Brücken: Verrentung/Verkauf verrentetes PK-Kapital erscheint als Vermögensabgang am Übergang; Verkaufsdifferenz und Grundstückgewinnsteuer nur beim Verkauf, nicht beim Halten
Verlauf: ein Punkt je Jahr je aktivem Element genau durationYears Punkte pro Phase
Verlauf: Konvexität 200'000 @ 5 % + 10'000 Sparbeitrag: Jahr 1 = 220'000, und die Jahreszuwächse wachsen eine Gerade zwischen den Phasengrenzen hätte konstante Zuwächse
Verlauf: Immobilie Verkehrswert, Restschuld und Eigenkapital werden getrennt geführt; Eigenkapital = Verkehrswert Restschuld
Brücke: Vermögen geht auf Restposten ≤ 5 CHF über einen Plan mit allen Element-Arten und Übergangs-Entscheiden
Brücke: Cash geht auf dito; Start- und Endwert stimmen mit cashStart/cashEnd überein
Brücke: Umbuchung 5 × 12'000 Sparbeitrag: Cash 60'000, Vermögen unverändert die Sparrate erscheint nur im Cash-Wasserfall
Brücke: PK-Beiträge 4 × 10'000 PK-Beitrag: Vermögen +40'000, Cash unbelastet
Brücke: Übergangsposten Verrentung, Kapitalbezugssteuer und Einmalposten erscheinen in der Brücke der Folgephase
explain: Standardmässig aus ohne Option keine Traces; mit Option identische Ergebniswerte (Endvermögen, Cash, Ruinalter)
explain: Abdeckung Traces auf Element-, Übergangs-, Phasen- und Plan-Ebene vorhanden
explain: eingesetzte Zahlen jeder Schritt trägt die Formel und die substituierte Fassung; das Resultat stimmt mit dem Ergebniswert überein
explain: Spec-Verweise jeder specAnchor trifft eine existierende Überschrift in SPEZIFIKATION.md (gegen die echte Datei geprüft)
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
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
Test 4 Liquiditätslücke Sparrate 10'000: cashEnd === -5436, cashNegative === true
Ausgaben real → nominal Jahr 1 nominal = real; Jahr 5 = 100'000 × 1.02^4
Einkommen flach Lohnerhöhung 0 % → incomeStart === incomeEnd === 80'000
Bezugsrate 3 × 10'000 Entnahme → cashEnd === 30'000, Asset-Endwert 0, plannedWithdrawRate === 10'000
Kapitalzufluss/-investition Verkauf in P1, Reinvestition in P2: capitalInflow === 10'000, capitalInvest === 10'000, cashStart === 0, startWealthNominal === 10'000 (kein Doppelzählen)
Cash-Anfangswert initialCash 50'000 fliesst in Phase 1 ein
Tilgung stoppt Schuld 25'000, Tilgung 10'000/J., 5 Jahre: Gesamtabfluss 25'000 (nicht 50'000), cashEnd === 75'000, Restschuld 0
Amortisation stoppt Hypothek 15'000, Amortisation 10'000/J., 4 Jahre: cashEnd === 85'000 (nicht 60'000), Immobilie schuldenfrei
Vorbezugssteuer PK-Vorbezug 100'000 brutto @ 8 %: capitalInflow === 92'000, Restkapital 200'000 (brutto entnommen)
Einmaliger Zufluss 100'000 nominal @ 10 % Steuer → oneOffInflow === 90'000, Cash-Start Folgephase +90'000; Phase 1 hat keinen Zufluss
Einmalige Kosten 20'000 real, 2 % Inflation, Grenze nach 10 J. → 20'000 × 1.02^10, entsprechend vom Cash abgezogen
Zufluss + Kosten / NONE BOTH: +50'000 20'000 → Cash-Start 30'000. NONE mit erfassten Beträgen → keine Wirkung
Liquiditätslücke durch Kosten Kosten 25'000 bei Cash 10'000 → cashStart === 15'000, cashNegative, incomplete
Letzte Phase Cash-Entscheid der letzten Phase bleibt wirkungslos (kein Übergang mehr)
Fortschreibung Einkommens-Basiswert P1 → Startwert P2 = 100'000 × 1.02^5; cashStart(P2) === cashEnd(P1)

8.3 Ausführung

npm test          # vitest run
npm run lint      # eslint
npm run build     # next build

Verifikationseinschränkung: Lokal steht keine Datenbank/Docker zur Verfügung. Die Verifikation erfolgt über npm run build und die isolierten Berechnungstests; End-to-End-Prüfung erst gegen das Deployment (fpt.aicds.ch).


9. Bekannte Einschränkungen und Modellentscheide

Dieser Abschnitt hält fest, was im Code steht und beim Weiterentwickeln bekannt sein muss.

9.1 Cash wird nicht automatisch ausgeglichen

Wird das Cash negativ, meldet das Tool eine Liquiditätslücke (rotes Icon, incomplete = true), greift aber nicht ein es wird kein Vermögen automatisch verkauft und kein Kredit aufgenommen. Negatives Cash geht mit negativem Vorzeichen ins Gesamtvermögen ein.

9.2 Verwaiste PERSON_B-Elemente

Die Haushaltsform ist eine Szenario-Eigenschaft und lässt sich im Dialog „Plan-Einstellungen" auch bei einem bestehenden Szenario nachträglich ändern (nicht pro Phase innerhalb eines Szenarios gilt sie durchgehend). Wechselt ein Szenario dabei von COUPLE auf SINGLE, schneidet PlanProfileFields die Personen auf eine zusammen und PATCH /api/scenarios/<id> löscht Person B aus der Datenbank. Elemente mit ownerRole = "PERSON_B" bleiben bestehen. In der Berechnung liefert personByRole dann null:

  • AHV: wird per if (!owner || …) continue; übersprungen keine Rente.
  • PENSION_FUND / PILLAR_3A: owner ist null, deshalb fällt ownerWorking auf anyWorking zurück, d. h. das Element folgt dem Erwerbsstatus des Haushalts statt der (nicht mehr existierenden) Person.

Es gibt keine Bereinigung und keine Warnung im UI. Da die Haushaltsform normalerweise nicht nachträglich geändert wird, ist der Fall selten er ist aber erreichbar.

9.3 Immobilien: was noch fehlt

Seit Version 0.5 kennt das Modul Hypothekarzins und Wertsteigerung. Nicht modelliert sind:

  • Nebenkosten und Unterhalt (Faustregel ~1 % des Werts). Lassen sich heute als normales Ausgaben-Element erfassen.
  • Eigenmietwert ohne echte Steuerlogik (Roadmap Nr. 23) nur halb wirksam.
  • Mieteinnahmen (Renditeliegenschaften) anderer Anwendungsfall.
  • Wertvermehrende Investitionen und die Haltedauer-Abstufung der Grundstückgewinnsteuer (kantonal, teils stark degressiv).
  • Zinsänderungsrisiko: Der Zinssatz gilt für die ganze Phase. Ein Zinsschock lässt sich nur abbilden, indem man an dieser Stelle eine Phasengrenze zieht und den Satz neu setzt.

9.4 Kein CSRF-Token

Zustandsändernde Requests sind allein durch sameSite=lax geschützt. Das deckt klassische Cross-Site-Formular-POSTs ab, ist aber schwächer als ein expliziter Token.

9.5 Logout invalidiert das Token nicht serverseitig

Das JWT ist zustandslos und bis zu 30 Tage gültig. logout löscht nur das Cookie.

9.6 Spar- und Bezugsraten werden nicht indexiert

annualContribution, annualWithdrawal, amortization und annualRepayment sind flache Nominalbeträge, die über die Phasenjahre konstant bleiben. Eine Sparrate von 10'000 bleibt 20 Jahre lang 10'000 nominal und verliert dabei real an Gewicht. Wer eine mitwachsende Rate abbilden will, muss die Phase teilen und den Betrag in der Folgephase erhöhen.

9.7 Nur ein Zufluss und eine Kostenposition pro Übergang

CashTransitionData hält genau ein Zufluss- und ein Kostenpaar (Bezeichnung + Betrag). „Erbschaft + Autoverkauf + Poolbau + Küche" am selben Übergang lässt sich nur durch Zusammenfassen abbilden („Diverses, 45'000") die Aufschlüsselung geht dabei verloren. Bewusster Entscheid zugunsten eines einfachen UI; erweiterbar auf Listen.

9.8 Einmalige Ereignisse nur an Phasengrenzen

Ein Ereignis kann nur an einem Phasenübergang liegen. Ein Poolbau in Jahr 3 einer 10-jährigen Phase ist nur abbildbar, wenn dort eine Phasengrenze gezogen wird. Nach der letzten Phase gibt es keinen Übergang ein dort erfasster Betrag bleibt wirkungslos (durch Test abgedeckt).

9.9 Gemeinsames Einkommen zählt bei Paaren nicht für die AHV

Das mdJE ist eine personenbezogene Grösse. Einkommen wird deshalb nur einer Person zugerechnet, wenn das INCOME-Element ihr zugeordnet ist (PERSON_A/PERSON_B). Bei einem Einzelplan zählt HOUSEHOLD zur Person A es gibt ja nur eine. Bei einem Paar-Plan bleibt HOUSEHOLD-Einkommen für die AHV unberücksichtigt.

Wer in einem Paar-Plan den Lohn als „Gemeinsam" erfasst, sieht deshalb im Prüf-Dialog ein geplantes Durchschnittseinkommen von 0 und bekäme die Mindestrente. Der Dialog zeigt den Wert prominent an, sodass der Fehler auffällt aber es gibt keine aktive Warnung. Erwerbseinkommen sollte in Paar-Plänen immer personenscharf erfasst werden.

Ebenfalls nicht modelliert: das Einkommenssplitting verheirateter Paare (während der Ehe werden die Einkommen hälftig geteilt) sowie Erziehungs- und Betreuungsgutschriften. Beides würde das mdJE real beeinflussen und wäre der nächste Ausbauschritt.

9.10 Verhaltensänderung: AHV-Rente bestehender Pläne

Bis Version 0.3 erhielt jede AHV-Position pauschal die Maximalrente (32'760), gekürzt nur um Ausfalljahre unabhängig vom Einkommen. Seit 0.4 folgt sie der Rentenformel. Bestehende Pläne zeigen dadurch eine andere, in der Regel tiefere AHV-Rente, sobald das geplante Durchschnittseinkommen unter 90'720 liegt. Das ist keine Regression, sondern die Korrektur einer zu optimistischen Pauschale.

Zwei Fälle brauchen Aufmerksamkeit:

  • Bereits pensionierte Personen (bei Planbeginn): Ohne erfasstes Durchschnittseinkommen ergibt das mdJE 0 → Mindestrente. Vorher war es die Maximalrente. Die Felder stehen in der AHV-Phasenzelle der ersten Phase; solange sie leer sind, ist die Rente bewusst konservativ.
  • Paar-Pläne mit HOUSEHOLD-Einkommen: siehe 9.9.

9.11 AHV-Rente wird nach der Pensionierung nicht indexiert

Die Rente wird zum Pensionierungszeitpunkt in heutigem Geld berechnet und danach nominal eingefroren. Die echte AHV wird alle zwei Jahre an den Mischindex angepasst. Über 25 Rentenjahre verliert die modellierte Rente damit real spürbar an Wert das Modell ist an dieser Stelle deutlich konservativ. Bewusster Alt-Entscheid, unabhängig von der Rentenformel; der grösste verbliebene Hebel im AHV-Modell.

9.12 PILLAR_3A_MAX_ANNUAL wird nur im UI erzwungen

Das Feld ist per max-Prop hart geklammert. Das Zod-Schema kennt für annualContribution nur ≥ 0 ein direkter API-Aufruf kann die Obergrenze überschreiten.

9.13 Verhaltensänderung: Nettolohn-Klarstellung und AHV

Bis Version 0.4 war nirgends definiert, ob INCOME netto oder brutto ist der Hilfetext sagte nur „die Zahl auf dem Lohnausweis", und dort stehen beide. Für den Cash-Fluss war das folgenlos, weil sich beide Konventionen aufheben (brutto Ausgaben inkl. Abzügenetto Ausgaben ohne Abzüge). Mit der einkommensabhängigen AHV (0.4) hing daran aber plötzlich eine Rente.

Seit 0.5 gilt: amount ist der Nettolohn, und die AHV rechnet mit × 1.12 auf brutto hoch. Konsequenzen:

  • Wer bisher netto erfasst hat (die dokumentierte Absicht): Die AHV-Rente steigt sie war zuvor um bis zu ~1'900/Jahr zu tief, am stärksten bei mittleren Einkommen (bei 80'000 brutto: 28'974 statt 30'902). Über 90'720 brutto verschwindet der Effekt, weil beide Werte in die Maximalrente laufen.
  • Wer brutto erfasst hat: Cash-Fluss und AHV sind nun beide zu hoch. Der Einkommensbetrag gehört auf netto korrigiert und die Lohnabzüge aus dem Ausgabenbetrag entfernt.

9.14 Keine Steuerschätzung

Bewusst nicht umgesetzt: eine automatische Schätzung von Einkommens- und Vermögenssteuer.

Die Bemessungsgrundlage ist das steuerbare Einkommen, nicht der Nettolohn also brutto minus PK, 3a, Berufsauslagen, Versicherungs- und Kinderabzüge. Darauf kommen drei Ebenen (Bund, Kanton, Gemeinde); allein der Gemeindesteuerfuss variiert innerhalb eines Kantons um rund den Faktor zwei. Dazu Zivilstand, Kinder, Konfession und für die Vermögenssteuer 26 kantonale Tarife mit eigenen Freibeträgen. Das Tool kennt weder Wohnort noch Kinder.

Eine Schätzung daraus läge im Einzelfall schnell 3050 % daneben bei vielen Haushalten dem grössten Ausgabenposten. Eine selbst berechnete Zahl wirkt zudem autoritativ und wird nicht hinterfragt. Der Benutzer kennt seine Steuerrechnung dagegen exakt aus der letzten Veranlagung.

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 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 Diff ist live gegen die Vorlage

Die Abweichungs-Markierung vergleicht immer den aktuellen Stand des Eltern-Szenarios. Ändert man die Vorlage nachträglich, verschiebt sich die Markierung in allen Kindern rückwirkend: Setzt man in der Vorlage einen Wert auf das, was ein Szenario ohnehin hatte, verschwindet dort die gelbe Markierung, ohne dass das Szenario angefasst wurde.

Das ist logisch korrekt („weicht ab von der Vorlage"), kann aber überraschen. Die Alternative ein eingefrorener Snapshot beim Kopieren wäre schnell veraltet und würde Abweichungen anzeigen, die keine mehr sind. Bewusster Entscheid zugunsten des Live-Vergleichs.

Verwandt: Wird ein Element in der Vorlage gelöscht, gilt das Gegenstück im Kind ab dann als neu (der Herkunfts-Verweis zeigt ins Leere). Auch das folgt aus dem Live-Vergleich.

9.17 Kleinere Beobachtungen

  • planToCsv(plan, computed) erhält den plan-Parameter, verwendet ihn aber nicht.
  • Plan.branchFromPhaseId hat keine Fremdschlüssel-Constraint; die Phase kann gelöscht werden, ohne dass das Feld bereinigt wird.
  • Die Element-Umbenennung ist als API vorhanden, im UI aber nicht erreichbar.
  • Die README.md ist noch der unveränderte create-next-app-Text.

(Der frühere Selection-Rest in PlanView und der ProfileMenu-Lint-Fehler sind seit 0.13 behoben npm run lint läuft fehlerfrei.)

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). 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.


9.20 Transparenz legt auch die Vereinfachungen offen

Die vollständige Offenlegung der Rechenwege macht sichtbar, was das Modell nicht kann: die nach der Pensionierung nicht indexierte AHV-Rente (9.11), den pauschalen Netto-Brutto-Faktor 1.12 (4.4.5), die nicht indexierten Spar- und Bezugsraten (9.6) und die fehlende Steuerberechnung (9.14).

Das ist der eigentliche Wert der Offenlegung und kein Nebeneffekt. Entscheidend ist aber, dass die Erklärtexte die Grenze mitnennen, statt sie zu übergehen dafür ist das Feld note je Rechenschritt da. Ein fachkundiger Nutzer, der eine Vereinfachung selbst entdeckt, nachdem ihm volle Transparenz zugesagt wurde, zieht den härteren Schluss.

9.21 Wasserfall: die Zuordnung ist eine Interpretation

Welcher Posten in welchen Wasserfall gehört, ist eine fachliche Entscheidung und nicht aus den Zahlen ableitbar. Die hier getroffene Umbuchungen nur im Cash-Wasserfall, echte Zu- und Abgänge nur im Vermögens-Wasserfall (4.14.2) ist begründet, aber nicht die einzig denkbare. Wer die Sparrate als „gebundenes Geld" verstanden wissen will, würde sie anders einordnen.

Konkret uneindeutig sind zwei Fälle:

  • Amortisation senkt Cash und Hypothek. Im Vermögens-Wasserfall taucht sie nicht auf, obwohl sie das Eigenkapital erhöht die Erhöhung ist bereits im unveränderten Vermögenssaldo enthalten.
  • Verrentung erscheint als Vermögensabgang, obwohl der Gegenwert als Rentenstrom weiterlebt. Der Rentenstrom ist im Modell aber kein Bilanzposten, sondern Einkommen; er taucht in den Folgejahren über die Quote wieder auf.

Der Restposten (residual) ist die Kontrollgrösse dafür, dass die gewählte Zerlegung wenigstens vollständig ist nicht dafür, dass sie die einzig sinnvolle ist.

9.22 Verkaufspreis und Verkehrswert bleiben unabhängig

Seit Version 0.12 wird der Verkaufspreis mit dem modellierten Verkehrswert vorbelegt und die Abweichung ausgewiesen (3.5.8). Die beiden Grössen bleiben aber entkoppelt das Tool erzwingt keine Konsistenz.

Das ist bewusst so: Ein Verkauf unter dem Verkehrswert ist ein realer Fall (Notverkauf, Verkauf an Nachkommen, Liebhaberobjekt ohne Markt). Eine Zwangskopplung würde diese Fälle unmöglich machen. Der Preis dafür ist, dass eine unplausible Kombination weiterhin eingebbar bleibt neu aber nicht mehr unbemerkt.

Eine Folge bleibt bestehen und ist nicht offensichtlich: Wird die Immobilie vor Planende verkauft, hat die angenommene Wertsteigerung keinen Einfluss mehr auf das Endvermögen. Der Erlös folgt allein dem erfassten Verkaufspreis. Im Tornado führt das zu einem Nullbalken, der seit 0.12 erklärt wird (4.13.5); in der Vermögensbrücke erscheint stattdessen die Differenz Verkaufspreis Verkehrswert als eigener Posten.

9.23 Assistent: Teilzustand bei Abbruch

Der Plan-Assistent und der Beispielplan senden am Ende eine Sequenz bestehender API-Aufrufe (Plan → Phase 1 → Elemente → Folgephasen). Bricht die Sequenz mittendrin ab (Netzfehler), existiert ein Teil-Plan. Der ist normal weiterbearbeitbar und der Fehlerhinweis sagt das auch aber es gibt kein automatisches Rollback. Das wäre nur mit Backend-Unterstützung (Transaktion über mehrere Requests oder Batch-Endpunkt) sauber lösbar und ist bewusst nicht gebaut: Der seltene Fehlerfall rechtfertigt keinen neuen Endpunkt.

9.24 Tour ohne Spotlight-Engine

Die Tour hebt ihr Ziel per Rahmen-Puls und scrollIntoView hervor bewusst ohne Spotlight-Overlay und Positionierungs-Engine (die Karte sitzt fix unten). Bei stark verschachtelten Scroll-Situationen kann das Ziel teilweise verdeckt sein. Der Einfachheit halber in Kauf genommen; eine echte Coach-Mark-Bibliothek wäre der nächste Schritt, wenn die Tour sich bewährt.

9.25 Die Quote ist kein fester Betrag

Die Spar-/Verzehrquote verändert sich über die Phasenjahre: Das Einkommen wächst mit der Lohnerhöhung, die real erfassten Ausgaben mit der Inflation. Bei 0 % Lohnerhöhung und 1.5 % Inflation sinkt eine Quote von 20'000 über fünf Jahre auf rund 15'100 ohne dass der Nutzer etwas geändert hätte.

annualContribution und die übrigen Raten sind dagegen flache Jahresbeträge (9.6). Wer die Quote des ersten Jahres als Rate verteilt, erzeugt sich damit in den späteren Jahren eine Liquiditätslücke.

Der Verteil-Dialog begegnet dem auf drei Arten, statt es zu verstecken:

  • Er zeigt Quote erstes Jahr, letztes Jahr und absolut über die Phase.
  • Er warnt ausdrücklich, wenn die Quote über die Phase sinkt.
  • Die Live-Vorschau rechnet den ganzen Plan neu und meldet eine entstehende Liquiditätslücke sofort, statt sie erst nach dem Speichern sichtbar zu machen.

Bewusst nicht umgesetzt ist eine automatische Deckelung: Es gibt legitime Gründe, mehr zu sparen als die laufende Quote hergibt (etwa wenn ein Cash-Polster aus der Vorphase abgebaut werden soll). Das Werkzeug informiert, es bevormundet nicht.

9.26 Fall 2 ist der Nullpunkt nicht 50 %

Naiv erwartet man, dass eine Planung ihren eigenen Endbetrag in 50 % der Fälle erreicht, sobald man um genau die geplanten Werte würfelt. Das ist falsch, und der Fehler ist gross genug, um ein Urteil zu verderben.

Der Grund ist der Volatilitäts-Drag (4.12.5): Der deterministische Pfad kapitalisiert das arithmetische Mittel, der Median einer Simulation folgt aber näher dem geometrischen. Gemessen an einem Testplan (800k Startkapital, 30 Jahre, geplante Rendite 5.2 %, deterministisches Endvermögen 3'457'554):

Streuung σ Median-Endvermögen P(Plan-Endbetrag erreicht)
0 % 3'457'554 (= deterministisch)
3 % 3'432'399 ~48 %
6 % 3'322'529 ~45 %
15 % 2'612'222 37 %
25 % 1'451'659 27 %

Eine Planung mit hoher Aktienquote landet also bei 2737 %, obwohl an ihr nichts falsch ist. Ein fixer 50-%-Schnitt würde sie als «zu optimistisch» brandmarken.

Deshalb ist der Nullpunkt für das Urteil Fall 2 dieselbe Schwelle, dieselbe Streuung, nur der Mittelpunkt aus den eigenen Planannahmen statt aus der Historie (4.12.7). Der Vol-Drag steckt dann in beiden Zahlen und kürzt sich weg; übrig bleibt genau die Frage, die interessiert: Ist meine Renditeannahme optimistischer als die Vergangenheit? Das Toleranzband von ± 5 Prozentpunkten fängt Simulationsrauschen ab.

Fall 2 selbst wird nur klein als Referenz gezeigt: als Zahl ist er wenig aussagekräftig (er misst im Wesentlichen die eigene Streuung), als Vergleichsanker dagegen unverzichtbar. Der Hinweis, dass die Simulation Risiko um deine Annahmen misst und nicht deren Richtigkeit (9.15), bleibt bestehen.

9.27 Warum die Regler Standardbereiche haben und der Tornado nicht

Zwei Kapitel dieser Spezifikation scheinen sich zu widersprechen: 9.18 begründet, warum die Sensitivitätsanalyse bewusst keine Default-Bandbreiten anbietet, während die Live-Simulation (4.15) 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.

9.28 Was die Versionierung nicht leistet

Das Zeitfenster ist eine Konvention, keine Wahrheit. Zehn Minuten sind gesetzt, weil FPT keinen Speichern-Knopf hat und der Nutzer den Schnitt sonst nie selbst zieht (3.8.1). Wer nach acht Minuten Pause weiterarbeitet, landet in derselben Version; wer nach zwölf Minuten eine Kleinigkeit ändert, bekommt eine neue. Beides ist gelegentlich nicht das, was man gemeint hätte. Ein Speichern-Knopf wäre die exaktere Lösung, würde aber die Bedienung des ganzen Werkzeugs umkrempeln.

Eine Nebenversion hält den Stand am ENDE der Sitzung fest, nicht jeden Zwischenschritt darin. Wer innerhalb einer Sitzung etwas ändert und wieder zurücknimmt, findet den Zwischenstand nirgends. Die Historie ist eine Folge von Arbeitsständen, kein Undo.

Es gibt keinen Versionsvergleich. Zwei Stände lassen sich nur nacheinander ansehen, nicht nebeneinander stellen. Der bestehende Diff (3.2.6) vergleicht Szenarien gegen ihr Eltern-Szenario, nicht Versionen gegeneinander technisch wäre beides verwandt, aber es ist bewusst nicht Teil dieser Stufe.

Die Historie wird nie beschnitten. Jede Version hält den vollständigen Zustand als JSON (Grössenordnung 40 KB bei sechs Phasen und zwölf Elementen). Bei intensiver Nutzung wächst das linear; eine Aufräumregel (etwa: Nebenversionen älter als ein Jahr verdichten, Haupt- versionen behalten) gibt es noch nicht. Bei den heutigen Datenmengen ist das unkritisch.

«Wer» unterscheidet heute nichts. Ein Plan gehört genau einem Benutzer, es gibt keine Freigabe und keine Rollen die Spalte zeigt also immer denselben Namen. Sie ist Vorbereitung auf den Finanzberater, und die Logik trennt Benutzer bereits sauber (Änderungen verschiedener Benutzer werden nie in einer Version zusammengefasst).

Der Plan selbst ist nicht versioniert, nur seine Szenarien. Wird ein Szenario gelöscht, verschwindet seine Historie mit ihm (Cascade). Das ist gewollt: Eine Historie ohne das Objekt, das sie beschreibt, wäre nicht wiederherstellbar.

9.29 Acht Zahlen je Zelle und wie wir sie vermeiden

Mit den effektiven Werten (3.9) bekommt die Matrix eine zweite Achse. Naiv kombiniert ergibt das je Zelle: nominal und real, Plan und Ist, Phasenbeginn und Phasenende acht Zahlen. Das ist keine Tabelle mehr, das ist ein Zahlenfeld.

Zwei Entscheide halten es lesbar, und sie fallen an den zwei Orten verschieden aus.

In der Matrix: Wert und Abweichung statt zweier Rohwerte. Es gibt nur «Plan» oder «Effektiv», kein «beide». Im Ist-Modus steht der Ist-Wert und daneben klein die Differenz zum Plan, grün oder rot. Das beantwortet auch die bessere Frage: nicht «wie lauteten die zwei Zahlen», sondern «wie weit bin ich weg». Nominal/real bleibt dort bei drei Möglichkeiten es sind Zahlen in einer Zelle, keine Linien in einem Bild.

Nur die Abweichung trägt Farbe. Würde man die Beträge selbst einfärben, entstünde ein Ampelteppich, in dem die eigentliche Aussage untergeht.

In den Grafiken: nominal/real wird zur Einfachauswahl. Der Vermögensverlauf zeichnete bisher je Serie zwei Linien (nominal durchgezogen, real gestrichelt). Mit Plan/Ist wären es vier, bei zwei Szenarien acht. Das Stilbudget geht deshalb an die wichtigere Unterscheidung: Plan gestrichelt, Ist durchgezogen genau die «Plan-Linie vs. Ist-Linie», die die Roadmap verlangt. Wer real sehen will, schaltet um, statt eine zweite Linie dazuzubekommen.

Nicht jede Grafik verträgt beides. «Plan und Ist gleichzeitig» gibt es nur beim Vermögensverlauf. Die Vermögensaufteilung zeigt schon Beginn und Ende je Phase als gestapelte Balken Plan und Ist daneben vervierfachte sie. Und die Grafik «Einkommen vs. Ausgaben» lebt vom Band zwischen zwei Linien; ein zweites Paar darüber macht genau diese Aussage unkenntlich. Beide zeigen deshalb nur die gewählte Grundlage.

Serienobergrenze vier. Szenario mal Version mal Datenquelle wächst schnell; darüber hinaus hilft keine Farbpalette mehr.

9.30 Warum der Haushalt am Plan hängt

Bis V6 trug jedes Szenario sein eigenes Grundprofil: Haushaltsform, Personen (Name, Alter, Pensionsalter) und Startjahr. Das war zu grosszügig. Zwei Szenarien desselben Plans konnten so verschiedene Startjahre oder Haushaltsformen tragen und niemand hätte es bemerkt, bis eine Auswertung Unsinn ergab: Derselbe erfasste Ist-Satz für 2031 wäre je Szenario auf einem anderen Planjahr gelandet.

V7 zieht die Trennlinie neu. Was den Haushalt beschreibt, gehört an den Plan; was eine Planungsvariante ausmacht, ans Szenario:

Am Plan (für alle Szenarien) Am Szenario (variantenspezifisch)
Haushaltsform Pensionsalter je Person
Personen: Name, Alter Inflationsannahme
Planstartjahr Cash-Anfangswert
Phasen, Elemente, Werte

Das Pensionsalter bleibt bewusst unten es ist der Kern jedes Früh- oder Spätpensionierungs-Szenarios. Wollte man Name oder Alter einer Person unterschiedlich planen, wäre das kein Szenario mehr, sondern ein anderer Plan.

Der Rechenkern merkt nichts davon. toPlanInput() fügt die beiden Ebenen wieder zu einem unveränderten PlanInput zusammen; computePlan und die 43 Golden Tests bleiben unberührt. Die Verlagerung ist eine Frage der Datenhaltung, nicht der Berechnung.

Preis der Klarheit: Der Profil-Dialog muss jetzt je Feld anzeigen, ob es plan-weit gilt oder nur das Szenario betrifft sonst änderte man beim Bearbeiten eines Nebenszenarios unbemerkt den ganzen Plan. Diese Beschriftung ist der sichtbare Teil der Entscheidung.


10. Glossar

Begriff Bedeutung im FPT
Plan Selbsttragende Planungseinheit: Grundprofil + Phasenkette + Elemente
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)
Spannweite (Tornado) Differenz zwischen dem Ergebnis beim tiefen und beim hohen Wert eines Treibers; bestimmt Balkenlänge und Rangfolge
Umbuchung Bewegung, die Geld zwischen Cash und einem Vermögenswert verschiebt, ohne das Vermögen zu verändern (Sparrate, Amortisation, Zusatzinvestition). Erscheint nur im Cash-Wasserfall
Vermögens-/Cash-Brücke Zerlegung der Vermögens- bzw. Cash-Veränderung einer Phase in ihre Summanden (WealthBridge / CashBridge)
Restposten Differenz zwischen gerechnetem Endwert und der Summe der Brücken-Summanden; reine Rundung, Kontrollgrösse für die Vollständigkeit
Rechenweg (Trace) Protokoll der Rechenschritte mit Formel, eingesetzten Zahlen, Ergebnis und geltender Vereinfachung; entsteht innerhalb von computePlan
Grundprofil Haushaltsform, Personen (Alter, Pensionsalter, Name), Inflationsannahme
Lebensphase Zeitabschnitt mit fester Dauer; darf keine Pensionierung überspannen
Phasentyp ERWERB / PENSION / MIXED; abgeleitet, nie gespeichert
Finanzielles Element Plan-weite Entität einer der 8 Kategorien, über alle Phasen identisch
Übergang Grenze zwischen zwei Phasen; Ort der einmaligen Entscheide
Einmalige Sonderein-/ausgabe Ereignis am Übergang (Erbschaft, Poolbau), das direkt aufs Cash wirkt und nicht in die Quote eingeht
Pensions-Übergang Übergang, bei dem der Besitzer des Elements pensioniert wird
Carry / Fortschreibung Live-Übertragung des Endwerts einer Phase in die nächste
Cash Systemseitiges Ausgleichskonto; darf negativ werden (Liquiditätslücke)
Quote Einkommen nominale Ausgaben eines Jahres; negativ = Verzehr
Geplante Sparrate 3a-Beiträge + Sparbeiträge + Amortisationen + Tilgungen
Geplante Verzehrrate Summe der Bezugsraten aus Sonstigem Vermögen
Kapitalzufluss Verkaufserlöse + PK-/3a-Bezüge aus dem Übergang in die Phase
Kapitalinvestition Zusatzinvestitionen + Sofort-Tilgungen
Nominal Betrag in Franken des jeweiligen Jahres
Real Kaufkraftbereinigt auf den Planbeginn (nominal / Deflator)
Deflator Kumulierte Inflation seit Planbeginn
Ausfalljahr Jahr ohne AHV-Beiträge; kürzt die Rente um 1/44
mdJE Massgebendes durchschnittliches Jahreseinkommen Mittel der Beitragsjahre, bestimmt die Rentenhöhe
R0 Mindestbetrag der vollen AHV-Monatsrente (1'260); alle Schwellen sind Vielfache davon
Wendepunkt mdJE = 36 × R0 = 45'360; dort wechselt die Rentenformel von Teil 1 auf Teil 2
Plafonierung Deckelung der Ehepaar-AHV auf 150 % der Einzel-Maximalrente
Umwandlungssatz Prozentsatz zur Verrentung des PK-Kapitals
Ruin(alter) Alter von Person A, in dem das Gesamtvermögen erstmals unter 0 fällt
Nachlass Endvermögen der letzten Phase (nominal)

Ende der Spezifikation v0.16