126 lines
4.0 KiB
JavaScript
126 lines
4.0 KiB
JavaScript
/**
|
|
* calc.js — pure calculation engine, no DOM
|
|
*/
|
|
|
|
/**
|
|
* Run the full rent vs buy simulation month by month.
|
|
* Returns arrays (one value per year) plus summary stats.
|
|
*
|
|
* @param {Object} p - input parameters
|
|
* @returns {Object} result
|
|
*/
|
|
function run_simulation(p) {
|
|
const months = p.years * 12;
|
|
|
|
// --- mortgage setup ---
|
|
const loan = p.home_price * (1 - p.down_pct / 100);
|
|
const r_m = p.interest_rate / 100 / 12;
|
|
const n = p.loan_years * 12;
|
|
let monthly_payment;
|
|
if (r_m === 0) {
|
|
monthly_payment = loan / n;
|
|
} else {
|
|
monthly_payment = loan * (r_m * Math.pow(1 + r_m, n)) / (Math.pow(1 + r_m, n) - 1);
|
|
}
|
|
|
|
// upfront costs for buyer
|
|
const down_payment = p.home_price * (p.down_pct / 100);
|
|
const upfront_buy = down_payment + p.home_price * (p.closing_buy_pct / 100);
|
|
const start_capital = p.start_capital || 0;
|
|
|
|
// --- state ---
|
|
let balance = loan;
|
|
let home_value = p.home_price;
|
|
// renter invests all startkapital; buyer spends upfront_buy and invests the remainder
|
|
let portfolio = start_capital;
|
|
let buyer_portfolio = Math.max(0, start_capital - upfront_buy);
|
|
let monthly_rent = p.monthly_rent;
|
|
|
|
let cum_buy_cost = upfront_buy; // running cash spent on buying
|
|
let cum_rent_cost = 0;
|
|
|
|
// per-year arrays (index 0 = end of year 1)
|
|
const buyer_nw_arr = [];
|
|
const renter_nw_arr = [];
|
|
const cum_buy_arr = [];
|
|
const cum_rent_arr = [];
|
|
const labels = [];
|
|
|
|
let breakeven_month = null;
|
|
|
|
const invest_r_m = p.invest_return / 100 / 12;
|
|
const app_r_m = p.appreciation / 100 / 12;
|
|
const rent_inc_m = p.rent_increase / 100 / 12;
|
|
|
|
for (let m = 1; m <= months; m++) {
|
|
// --- buy side ---
|
|
const interest_m = balance * r_m;
|
|
const principal_m = monthly_payment - interest_m;
|
|
balance = Math.max(0, balance - principal_m);
|
|
home_value *= (1 + app_r_m);
|
|
|
|
const prop_tax_m = home_value * (p.prop_tax_rate / 100) / 12;
|
|
const maint_m = home_value * (p.maint_rate / 100) / 12;
|
|
const insurance_m = p.insurance / 12;
|
|
// German context: no mortgage interest tax deduction for private buyers
|
|
const total_buy_m = monthly_payment + prop_tax_m + maint_m + insurance_m;
|
|
|
|
cum_buy_cost += total_buy_m;
|
|
|
|
buyer_portfolio *= (1 + invest_r_m);
|
|
const selling_costs = home_value * (p.closing_sell_pct / 100);
|
|
const buyer_nw = home_value - selling_costs - balance + buyer_portfolio;
|
|
|
|
// --- rent side ---
|
|
const renters_ins_m = p.renters_ins / 12;
|
|
const total_rent_m = monthly_rent + renters_ins_m;
|
|
cum_rent_cost += total_rent_m;
|
|
|
|
// renter invests the difference if buying is more expensive
|
|
const monthly_delta = Math.max(0, total_buy_m - total_rent_m);
|
|
portfolio = portfolio * (1 + invest_r_m) + monthly_delta;
|
|
|
|
// apply capital gains tax on portfolio gains when we "cash out" at end
|
|
// (we track gross portfolio, deduct tax in summary)
|
|
const renter_nw = portfolio;
|
|
|
|
// breakeven: when buyer net worth overtakes renter
|
|
if (breakeven_month === null && buyer_nw >= renter_nw) {
|
|
breakeven_month = m;
|
|
}
|
|
|
|
// rent increases monthly
|
|
monthly_rent *= (1 + rent_inc_m);
|
|
|
|
// record yearly snapshots
|
|
if (m % 12 === 0) {
|
|
const yr = m / 12;
|
|
labels.push(`Jahr ${yr}`);
|
|
buyer_nw_arr.push(Math.round(buyer_nw));
|
|
// apply Abgeltungssteuer on portfolio gains at cashout
|
|
const portfolio_gains = portfolio - start_capital;
|
|
const tax_on_gains = Math.max(0, portfolio_gains) * (p.tax_rate / 100);
|
|
renter_nw_arr.push(Math.round(portfolio - tax_on_gains));
|
|
cum_buy_arr.push(Math.round(cum_buy_cost));
|
|
cum_rent_arr.push(Math.round(cum_rent_cost));
|
|
}
|
|
}
|
|
|
|
// final net worth at end of horizon
|
|
const final_buyer_nw = buyer_nw_arr[buyer_nw_arr.length - 1];
|
|
const final_renter_nw = renter_nw_arr[renter_nw_arr.length - 1];
|
|
|
|
return {
|
|
labels,
|
|
buyer_nw_arr,
|
|
renter_nw_arr,
|
|
cum_buy_arr,
|
|
cum_rent_arr,
|
|
monthly_payment: Math.round(monthly_payment),
|
|
final_buyer_nw,
|
|
final_renter_nw,
|
|
breakeven_month,
|
|
winner: final_buyer_nw >= final_renter_nw ? 'buy' : 'rent',
|
|
};
|
|
}
|