Phasenkopf neu + Bezugsrate (Verzehrrate) beim Sonstigen Vermoegen
Deploy App / deploy (push) Successful in 1m2s

- Sonstiges Vermoegen bekommt "Jaehrliche Bezugsrate (Entnahme)": mindert das Vermoegen
  (gekappt am Bestand) und fliesst jaehrlich ins Cash.
- Phasenkopf neu strukturiert: Einkommen / Ausgaben / Quote, dann Geplante Sparrate
  (3a + Vermoegen-Sparbeitrag + Amortisation + Tilgung) und Geplante Verzehrrate
  (Bezugsraten), dann Kapitalzufluss (Verkaeufe + PK-/3a-Bezuege aus dem Uebergang) und
  Kapitalinvestitionen (Zusatz-/Neuinvestitionen + sofortige Tilgungen), dann Vermoegen.
  Die Cash-Zeile im Kopf entfaellt (die Cash-Zeile in der Matrix bleibt).
- Engine liefert plannedSaveRate/plannedWithdrawRate/capitalInflow/capitalInvest.
- Golden Tests: Bezugsrate ins Cash + Kapitalzufluss/-investitionen (10 gruen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 23:11:40 +02:00
parent 435bef6026
commit 3acac56640
5 changed files with 99 additions and 23 deletions
+41 -19
View File
@@ -54,7 +54,10 @@ export interface PhaseComputed {
quotaStart: number;
quotaEnd: number;
isConsumption: boolean;
plannedRatesTotal: number; // Summe der geplanten flachen Jahresraten (3a, Vermoegen, Amort., Tilgung)
plannedSaveRate: number; // geplante Sparrate: 3a + Sonstiges-Vermoegen-Sparbeitrag + Amort. + Tilgung
plannedWithdrawRate: number; // geplante Verzehrrate: Bezugsraten aus Sonstigem Vermoegen
capitalInflow: number; // Kapitalzufluss: PK-/3a-Bezuege + Verkaeufe (aus dem Uebergang in diese Phase)
capitalInvest: number; // Kapitalinvestitionen: Zusatz-/Neuinvestitionen + sofortige Tilgungen
cashStart: number;
cashEnd: number;
cashNegative: boolean; // Cash faellt in dieser Phase (irgendwann) unter 0 -> Liquiditaetsluecke
@@ -138,6 +141,9 @@ export function computePlan(plan: PlanInput): PlanComputed {
let yearsBefore = 0;
let cumulativeInflation = 1;
let cashCarryIn = Math.round(plan.initialCash || 0);
// Aus dem Uebergang der Vorphase in DIESE Phase fliessende Groessen (Kopf-Kennzahlen).
let incomingInflow = 0; // Brutto-Zufluss: Verkaeufe + PK-/3a-Bezuege
let incomingImmediateRepay = 0; // sofortige Schuldentilgungen (Abfluss)
let ruinAge: number | null = null;
for (let i = 0; i < phases.length; i++) {
@@ -201,10 +207,11 @@ export function computePlan(plan: PlanInput): PlanComputed {
const incomes: { basis: number; idx: number; ec: ElementPhaseComputed }[] = [];
const expenses: { basis: number; idx: number; ec: ElementPhaseComputed }[] = [];
let renteTotal = 0; // AHV + PK-Renten (nominal fix)
const assets: { value: number; rate: number; r: number; ec: ElementPhaseComputed }[] = [];
const assets: { value: number; rate: number; r: number; withdrawal: number; ec: ElementPhaseComputed }[] = [];
const realEstates: { purchase: number; mortgageStart: number; amort: number; ec: ElementPhaseComputed }[] = [];
const debts: { owedStart: number; repay: number; ec: ElementPhaseComputed }[] = [];
let plannedRatesTotal = 0; // R: 3a + Sonstiges Vermoegen + Amortisation + Tilgung
let plannedRatesTotal = 0; // Sparraten (verlassen das Cash): 3a + Sonstiges Vermoegen + Amort. + Tilgung
let plannedWithdrawTotal = 0; // Bezugsraten (fliessen ins Cash): Sonstiges Vermoegen
let investmentsFromCash = 0; // Neuinvestitionen/Aufstockungen (ab Phase 2, aus Cash)
let wealthStart = 0;
let wealthEnd = 0; // wird nach der Jahresschleife gefuellt
@@ -292,7 +299,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
ec.baseValue = base;
ec.startValue = start;
wealthStart += start;
assets.push({ value: start, rate, r: num(pd.expectedReturn), ec });
assets.push({ value: start, rate, r: num(pd.expectedReturn), withdrawal: 0, ec });
}
break;
}
@@ -310,7 +317,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
ec.baseValue = base;
ec.startValue = start;
wealthStart += start;
assets.push({ value: start, rate, r: num(pd.expectedReturn), ec });
assets.push({ value: start, rate, r: num(pd.expectedReturn), withdrawal: 0, ec });
}
break;
}
@@ -319,12 +326,14 @@ export function computePlan(plan: PlanInput): PlanComputed {
const topUp = carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0;
const start = base + topUp;
const rate = Math.round(num(pd.annualContribution));
const withdrawal = Math.round(num(pd.annualWithdrawal));
plannedRatesTotal += rate;
plannedWithdrawTotal += withdrawal;
if (!isFirstPhase) investmentsFromCash += topUp;
ec.baseValue = base;
ec.startValue = start;
wealthStart += start;
assets.push({ value: start, rate, r: num(pd.expectedReturn), ec });
assets.push({ value: start, rate, r: num(pd.expectedReturn), withdrawal, ec });
break;
}
case "REAL_ESTATE": {
@@ -394,8 +403,15 @@ export function computePlan(plan: PlanInput): PlanComputed {
quotaEnd = quote;
}
cash += quote - plannedRatesTotal;
for (const a of assets) a.value = a.value * (1 + a.r / 100) + a.rate;
// Vermoegen verzinsen + Sparbeitrag; Bezugsrate entnehmen (gekappt am Bestand) und ins Cash.
let cashFromWithdraw = 0;
for (const a of assets) {
const grown = a.value * (1 + a.r / 100) + a.rate;
const w = Math.min(a.withdrawal, Math.max(0, grown));
a.value = grown - w;
cashFromWithdraw += w;
}
cash += quote - plannedRatesTotal + cashFromWithdraw;
if (cash < 0) cashNegative = true;
// Gesamtvermoegen zum Jahresende t (fuer Ruin-Erkennung).
@@ -460,7 +476,10 @@ export function computePlan(plan: PlanInput): PlanComputed {
quotaStart: Math.round(quotaStart),
quotaEnd: Math.round(quotaEnd),
isConsumption: quotaStart < 0,
plannedRatesTotal,
plannedSaveRate: plannedRatesTotal,
plannedWithdrawRate: plannedWithdrawTotal,
capitalInflow: Math.round(incomingInflow),
capitalInvest: Math.round(investmentsFromCash + incomingImmediateRepay),
cashStart: Math.round(cashStart),
cashEnd,
cashNegative,
@@ -475,7 +494,8 @@ export function computePlan(plan: PlanInput): PlanComputed {
});
// --- Uebergang: Carry aktualisieren, Cash der Folgephase bilden ---
let outgoing = 0;
let txInflow = 0;
let txImmediateRepay = 0;
for (const e of orderedElements) {
const carry = carries.get(e.id)!;
const ec = ecById.get(e.id)!;
@@ -503,7 +523,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
const value = ec.endValue;
const mode = td.payoutMode ?? "PENSION";
if (mode === "CAPITAL") {
outgoing += Math.round(value * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
txInflow += Math.round(value * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
carry.value = 0;
carry.pkPensionAnnual = 0;
} else if (mode === "PENSION") {
@@ -511,31 +531,31 @@ export function computePlan(plan: PlanInput): PlanComputed {
carry.value = 0;
} else {
const capital = Math.min(value, Math.round(num(td.capitalAmount)));
outgoing += Math.round(capital * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
txInflow += Math.round(capital * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
carry.pkPensionAnnual = Math.round(((value - capital) * num(td.conversionRate, DEFAULT_PK_CONVERSION_RATE)) / 100);
carry.value = 0;
}
} else {
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
carry.value = ec.endValue - withdrawal;
outgoing += withdrawal;
txInflow += withdrawal;
}
break;
}
case "PILLAR_3A": {
if (ownerRetiresNext) {
outgoing += Math.round(ec.endValue * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
txInflow += Math.round(ec.endValue * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
carry.value = 0;
} else {
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
carry.value = ec.endValue - withdrawal;
outgoing += withdrawal;
txInflow += withdrawal;
}
break;
}
case "OTHER_ASSET": {
if (td.decision === "SELL") {
outgoing += ec.endValue;
txInflow += ec.endValue;
carry.status = "SOLD";
} else {
carry.value = ec.endValue;
@@ -550,7 +570,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
const salePrice = Math.round(num(td.salePrice));
const gain = Math.max(0, salePrice - purchase);
const tax = gain * (num(td.saleTaxRate, DEFAULT_PROPERTY_GAINS_TAX_RATE) / 100);
outgoing += Math.round(salePrice - restMortgage - tax);
txInflow += Math.round(salePrice - restMortgage - tax);
carry.status = "SOLD";
} else {
carry.mortgage = restMortgage;
@@ -563,7 +583,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
const immediate = Math.min(carry.owed, Math.round(num(td.immediateRepayment)));
if (immediate > 0) {
carry.owed = Math.max(0, carry.owed - immediate);
outgoing -= immediate;
txImmediateRepay += immediate;
}
if (carry.owed === 0) carry.status = "SETTLED";
break;
@@ -574,7 +594,9 @@ export function computePlan(plan: PlanInput): PlanComputed {
carry.hasCarry = true;
}
cashCarryIn = cashEnd + outgoing;
cashCarryIn = cashEnd + txInflow - txImmediateRepay;
incomingInflow = txInflow;
incomingImmediateRepay = txImmediateRepay;
yearsBefore += duration;
}