import panel as pn import param import pandas as pd from bokeh.palettes import Spectral10 from bokeh.plotting import figure from bokeh.transform import cumsum class MortgageCalculator(param.Parameterized): total_available_for_closing = param.Number(default=20000) home_purchase_price = param.Number(default=200000) home_appraised_value = param.Number(default=180000) mortgage_duration_years = param.Number(default=30) interest_rate_percent = param.Number(default=6.5) hoa = param.Number(default=100) monthly_insurance = param.Number(default=65) property_tax_rate_percent = param.Number(default=1.4) fixed_closing_costs = param.Number(default=15000) @param.depends("interest_rate_percent") def monthly_interest_rate(self): return self.interest_rate_percent / 12 / 100.0 @param.depends("home_purchase_price", "monthly_property_tax") def prepaid_closing_costs(self): prepaid_interest_for_12_days = 12 * self.interest_rate_percent / 365 / 100.0 return ( 15 * self.monthly_insurance + 4 * self.monthly_property_tax() + prepaid_interest_for_12_days ) @param.depends("prepaid_closing_costs", "fixed_closing_costs") def estimated_closing_costs(self): return round(self.prepaid_closing_costs() + self.fixed_closing_costs) @param.depends( "home_purchase_price", "home_appraised_value", "total_available_for_closing", "estimated_closing_costs", ) def down_payment(self): diff_between_purchase_price_and_appraisal = ( self.home_purchase_price - self.home_appraised_value ) return ( self.total_available_for_closing - diff_between_purchase_price_and_appraisal - self.estimated_closing_costs() ) @param.depends( "home_appraised_value", "down_payment", ) def loan_amount(self): down_payment = self.down_payment() return round(self.home_appraised_value - down_payment) @param.depends("loan_amount", "mortgage_duration_years", "monthly_interest_rate") def mortgage(self): """ M = P [ i(1 + i)^n ] / [ (1 + i)^n – 1] P = principal loan amount i = monthly interest rate n = number of months required to repay the loan """ # Assume monthly payments number_of_payments = self.mortgage_duration_years * 12 monthly_interest_rate = self.monthly_interest_rate() monthly_mortgage_payment = self.loan_amount() * ( monthly_interest_rate * (1 + monthly_interest_rate) ** number_of_payments / (((1 + monthly_interest_rate) ** number_of_payments) - 1) ) return round(monthly_mortgage_payment) @param.depends("property_tax_rate_percent", "home_appraised_value") def monthly_property_tax(self): return round( self.property_tax_rate_percent * self.home_appraised_value / 12 / 100.0 ) @param.depends("mortgage", "hoa", "monthly_insurance", "monthly_property_tax") def monthly_estimated_payments(self): return round( self.mortgage() + self.hoa + self.monthly_insurance + self.monthly_property_tax() ) @param.depends("mortgage") def amortization_schedule(self): number_of_payments = self.mortgage_duration_years * 12 monthly_interest_rate = self.monthly_interest_rate() months = range(1, number_of_payments + 1) mortgage = self.mortgage() principal_left = self.loan_amount() amortization = [] for month in months: interest_paid_in_month = principal_left * monthly_interest_rate principal_paid_in_month = ( min(mortgage, principal_left + interest_paid_in_month) - interest_paid_in_month ) principal_left -= principal_paid_in_month amortization.append( { "Month": month, "Principal Left": round(principal_left), "Interest Paid": round(interest_paid_in_month), "Principal Paid": round(principal_paid_in_month), } ) return pn.pane.DataFrame( pd.DataFrame.from_records(amortization), width=600, index=False ) @param.depends("monthly_estimated_payments") def monthly_payments_pie_chart(self): payments = { "Mortgage": self.mortgage(), "Property Tax": self.monthly_property_tax(), "Home Insurance": self.monthly_insurance, "HOA": self.hoa, } return make_pie_chart( payments, f"Monthly Costs: ${self.monthly_estimated_payments()}" ) @param.depends("estimated_closing_costs", "down_payment") def cost_at_closing_pie_chart(self): payments = { "Fixed Closing Costs": self.fixed_closing_costs, "Prepaid Closing Costs": self.prepaid_closing_costs(), "Down Payment": self.down_payment(), "Purchase Cost not Covered by Loan": self.home_purchase_price - self.home_appraised_value, } return make_pie_chart( payments, f"Costs at Closing: ${self.total_available_for_closing}" ) def make_pie_chart(key_values, title): data = ( pd.Series(key_values) .reset_index(name="value") .rename(columns={"index": "Costs"}) ) data["angle"] = data["value"] / data["value"].sum() * 2 * 3.14 data["color"] = Spectral10[: len(key_values)] p = figure( height=300, title=title, toolbar_location=None, tools="hover", tooltips="@Costs: @value{1}", x_range=(-0.5, 1.0), ) p.wedge( x=0, y=1, radius=0.3, start_angle=cumsum("angle", include_zero=True), end_angle=cumsum("angle"), line_color="white", fill_color="color", legend_field="Costs", source=data, ) p.axis.axis_label = None p.axis.visible = False p.grid.grid_line_color = None return p explorer = MortgageCalculator(name="Mortgage Calculator") pn.Row( explorer.param, pn.Column( pn.Row( pn.Column( explorer.cost_at_closing_pie_chart, explorer.monthly_payments_pie_chart, ), ), pn.pane.HTML("<h2>Amortization Schedule</h2>"), explorer.amortization_schedule, ) ).servable()