Phasenkopf neu + Bezugsrate (Verzehrrate) beim Sonstigen Vermoegen
Deploy App / deploy (push) Successful in 1m2s
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:
+41
-19
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user