Neuer Knopf auf Plan-Ebene: Liste plus Wizard in zwei Schritten. Ein
Ist-Satz haengt am PLAN, nicht am Szenario -- die Zuordnung laeuft ueber
die Herkunfts-Kette sourceElementId.
computePlan nimmt neu { actuals }: Die Werte schnappen in jedem erfassten
Jahr auf die Realitaet und laufen von dort planmaessig weiter. Luecken
fallen auf die Plandaten zurueck. Ohne die Option unveraendert -- die 43
Golden Tests laufen durch.
Der Sprung ist keine Rendite: eigene Brueckenposition actualsCorrection
in Vermoegens- und Cash-Bruecke, sonst ginge die Zerlegung nicht auf.
Matrix: Umschalter Plan/Effektiv, im Ist-Modus mit farbiger Abweichung
statt acht Zahlen je Zelle. Zeitachse: Marker je Jahr, juengster farbig.
Vier Analysewerkzeuge mit einheitlicher Leiste (nominal/real als
Einfachauswahl, Plan/Effektiv). MC: Zielbetrag dreht mit, Startjahr
abgeleitet statt eingebbar.
Neue Tabelle ActualsSet (gegen echtes Postgres verifiziert), Module
actuals.ts und dataview.ts. Spezifikation 0.21, 27 Tests (181 -> 208).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+73
-2
@@ -11,6 +11,7 @@ import {
|
||||
DEFAULT_PROPERTY_GAINS_TAX_RATE,
|
||||
} from "@/lib/constants";
|
||||
import { num } from "@/lib/elements";
|
||||
import { actualsForYear, rebaseFlow, type ResolvedActuals } from "@/lib/actuals";
|
||||
import type { ElementCategory } from "@/lib/elements";
|
||||
import type { PersonRole, PlanInput } from "@/lib/types";
|
||||
|
||||
@@ -50,6 +51,9 @@ export interface ComputeOptions {
|
||||
// Standardmässig aus: die Monte-Carlo-Simulation ruft computePlan zehntausendfach auf
|
||||
// und darf von der Protokollierung nichts merken.
|
||||
explain?: boolean;
|
||||
// Effektive (Ist-)Werte, auf DIESES Szenario aufgelöst (siehe `actuals.ts`). Ohne sie
|
||||
// rechnet die Funktion exakt wie bisher -- die Plan-Sicht bleibt unangetastet.
|
||||
actuals?: ResolvedActuals[];
|
||||
}
|
||||
|
||||
// Ein Datenpunkt pro Jahr JE ELEMENT -- Grundlage der Detailansicht (Roadmap Nr. 43).
|
||||
@@ -111,6 +115,10 @@ export interface WealthBridge {
|
||||
investmentReturn: number; // Rendite auf PK/3a/Sonstigem Vermögen
|
||||
propertyAppreciation: number; // Wertsteigerung der Liegenschaft
|
||||
pensionFundContribution: number; // PK-Beiträge: erhöhen das Vermögen, ohne Cash zu kosten
|
||||
// Sprung auf die erfassten Ist-Werte (nur in der Ist-Sicht, sonst 0). Bewusst als eigene
|
||||
// Position: Die Differenz zwischen Plan und Wirklichkeit ist KEINE Rendite und darf nicht
|
||||
// als solche erscheinen -- ohne diese Zeile ginge die Brücke im Ist-Jahr nicht auf.
|
||||
actualsCorrection: number;
|
||||
endWealth: number; // = endWealthNominal
|
||||
residual: number; // Rundungsdifferenz (Kontrollgrösse, sollte nahe 0 sein)
|
||||
}
|
||||
@@ -127,6 +135,7 @@ export interface CashBridge {
|
||||
savingRates: number; // 3a + Sparbeiträge (Abgang)
|
||||
debtRates: number; // Amortisationen + Tilgungen (Abgang)
|
||||
withdrawals: number; // Bezugsraten aus Sonstigem Vermögen (Zugang)
|
||||
actualsCorrection: number; // Sprung auf den erfassten Ist-Cashbestand (sonst 0)
|
||||
cashEnd: number;
|
||||
residual: number;
|
||||
}
|
||||
@@ -332,6 +341,8 @@ function pct(v: number): string {
|
||||
|
||||
export function computePlan(plan: PlanInput, sample?: PlanSample, options?: ComputeOptions): PlanComputed {
|
||||
const explain = options?.explain === true;
|
||||
// Ohne Ist-Werte verhält sich die Funktion exakt wie bisher (die Golden Tests belegen es).
|
||||
const actuals = options?.actuals;
|
||||
const phases = [...plan.phases].sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
||||
const persons = plan.persons;
|
||||
const personA = persons.find((p) => p.role === "PERSON_A") ?? persons[0];
|
||||
@@ -703,6 +714,10 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
let savingRatesTotal = 0;
|
||||
let debtRatesTotal = 0;
|
||||
let withdrawalsTotal = 0;
|
||||
// Sprung auf die Ist-Werte. Ohne Ist-Daten bleiben beide 0 und die Brücken rechnen
|
||||
// exakt wie bisher.
|
||||
let actualsCorrectionTotal = 0;
|
||||
let actualsCashCorrectionTotal = 0;
|
||||
|
||||
for (let t = 1; t <= duration; t++) {
|
||||
// Einkommen: nominal (Basis x (1+Lohnerhöhung)^(t-1)) + Renten (nominal fix).
|
||||
@@ -787,6 +802,58 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
if (t === 1) plannedSaveRate = fixedRatesTotal + debtRates;
|
||||
|
||||
cash += quote - fixedRatesTotal - debtRates + cashFromWithdraw;
|
||||
|
||||
// --- Effektive Werte einspielen (Roadmap Nr. 5) --------------------------------------
|
||||
// Bewusst NACH Verzinsung, Tilgung und Cash-Fortschreibung: Der erfasste Wert ist der
|
||||
// Stand AM ENDE des Ist-Jahres. Rechnet man 2022 mit 120 und wieder 2024 mit 140, so
|
||||
// liegen dazwischen genau zwei Wachstumsjahre -- das entspricht der Erwartung.
|
||||
//
|
||||
// Die Differenz wird als eigene Grösse geführt und NICHT den Renditen zugeschlagen:
|
||||
// Ein Rückstand gegenüber dem Plan ist keine negative Rendite, sondern eine Korrektur.
|
||||
const act = actuals ? actualsForYear(actuals, yearsBefore + t) : null;
|
||||
if (act) {
|
||||
for (const a of assets) {
|
||||
const v = act.byElementId[a.ec.elementId]?.value;
|
||||
if (typeof v !== "number") continue; // Lücke -> Planlinie läuft weiter
|
||||
actualsCorrectionTotal += v - a.value;
|
||||
a.value = v;
|
||||
}
|
||||
for (const re of realEstates) {
|
||||
const av = act.byElementId[re.ec.elementId];
|
||||
if (typeof av?.value === "number") {
|
||||
actualsCorrectionTotal += av.value - re.value;
|
||||
re.value = av.value;
|
||||
}
|
||||
if (typeof av?.mortgage === "number") {
|
||||
// Eine höhere Restschuld mindert das Vermögen -- Vorzeichen umgekehrt.
|
||||
actualsCorrectionTotal -= av.mortgage - re.mortgage;
|
||||
re.mortgage = av.mortgage;
|
||||
}
|
||||
}
|
||||
for (const d of debts) {
|
||||
const v = act.byElementId[d.ec.elementId]?.value;
|
||||
if (typeof v !== "number") continue;
|
||||
actualsCorrectionTotal -= v - d.owed;
|
||||
d.owed = v;
|
||||
}
|
||||
// Flüsse: Der erfasste Betrag gilt für DIESES Jahr; die Basis wird so zurückgerechnet,
|
||||
// dass die Reihe hier den Ist-Wert trifft und danach planmässig weiterwächst.
|
||||
for (const inc of incomes) {
|
||||
const v = act.byElementId[inc.ec.elementId]?.value;
|
||||
if (typeof v === "number") inc.basis = rebaseFlow(v, inc.idx, t);
|
||||
}
|
||||
for (const exp of expenses) {
|
||||
const v = act.byElementId[exp.ec.elementId]?.value;
|
||||
// Ausgaben werden real geführt, erfasst wird der nominale Ist-Betrag.
|
||||
if (typeof v === "number") exp.basis = rebaseFlow(v, exp.idx, t, inflFactor);
|
||||
}
|
||||
if (typeof act.cash === "number") {
|
||||
actualsCashCorrectionTotal += act.cash - cash;
|
||||
actualsCorrectionTotal += act.cash - cash;
|
||||
cash = act.cash;
|
||||
}
|
||||
}
|
||||
|
||||
if (cash < 0) cashNegative = true;
|
||||
|
||||
quotaTotal += quote;
|
||||
@@ -1203,6 +1270,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
investmentReturn: Math.round(investmentReturnTotal),
|
||||
propertyAppreciation: Math.round(propertyAppreciationTotal),
|
||||
pensionFundContribution: Math.round(pensionFundContributionTotal),
|
||||
actualsCorrection: Math.round(actualsCorrectionTotal),
|
||||
endWealth: endWealthNominal,
|
||||
residual:
|
||||
endWealthNominal -
|
||||
@@ -1215,7 +1283,8 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
Math.round(quotaTotal) +
|
||||
Math.round(investmentReturnTotal) +
|
||||
Math.round(propertyAppreciationTotal) +
|
||||
Math.round(pensionFundContributionTotal)),
|
||||
Math.round(pensionFundContributionTotal) +
|
||||
Math.round(actualsCorrectionTotal)),
|
||||
},
|
||||
cashBridge: {
|
||||
openingCash: isFirstPhase ? Math.round(plan.initialCash || 0) : previousCashEnd,
|
||||
@@ -1229,6 +1298,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
savingRates: Math.round(savingRatesTotal),
|
||||
debtRates: Math.round(debtRatesTotal),
|
||||
withdrawals: Math.round(withdrawalsTotal),
|
||||
actualsCorrection: Math.round(actualsCashCorrectionTotal),
|
||||
cashEnd,
|
||||
residual:
|
||||
cashEnd -
|
||||
@@ -1236,7 +1306,8 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
Math.round(quotaTotal) -
|
||||
Math.round(savingRatesTotal) -
|
||||
Math.round(debtRatesTotal) +
|
||||
Math.round(withdrawalsTotal)),
|
||||
Math.round(withdrawalsTotal) +
|
||||
Math.round(actualsCashCorrectionTotal)),
|
||||
},
|
||||
traces: explain ? phaseTraces : undefined,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user