From 5b2b3f40dc3143a431de4299934b90381421bb03 Mon Sep 17 00:00:00 2001 From: John Koestner Date: Mon, 16 Dec 2024 00:33:09 -0500 Subject: [PATCH] add loan to budget --- folioflex/dashboard/pages/budget.py | 197 +++++++++++++++++++++++++++- folioflex/portfolio/loans.py | 63 +++++++-- tests/test_asset_loans.py | 14 +- 3 files changed, 260 insertions(+), 14 deletions(-) diff --git a/folioflex/dashboard/pages/budget.py b/folioflex/dashboard/pages/budget.py index abcf502..12d13e5 100644 --- a/folioflex/dashboard/pages/budget.py +++ b/folioflex/dashboard/pages/budget.py @@ -72,11 +72,12 @@ def layout(): dbc.Button( "Update Budget Database", id="budget-update-db-button", - color="primary", + color="secondary", className="mt-4", ), ], width=3, + className="d-flex justify-content-end", ), ], className="g-3", @@ -347,6 +348,124 @@ def layout(): ], title="Loans", ), + # Loan Calculator + dbc.AccordionItem( + [ + dbc.Button( + [ + html.I( + className="fas fa-calculator me-2" + ), # Font Awesome icon + "Calculate Loan", + ], + id="loan-calc-button", + color="primary", + className="mb-3", + ), + html.P( + "Input 3 values to calculate the 4th. " + "Payment amount must be input.", + ), + dbc.Card( + [ + dbc.CardHeader(html.H4("Loan Options")), + dbc.CardBody( + [ + dbc.Row( + [ + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.InputGroupText( + "$" + ), + dbc.Input( + id="loan-calc-loan-amount", + type="number", + placeholder="Loan Amount", + className="form-control-lg", + ), + ], + className="mb-3", + ), + ], + width=6, + ), + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.InputGroupText( + "%" + ), + dbc.Input( + id="loan-calc-interest", + type="number", + placeholder="Interest Rate", + className="form-control-lg", + ), + ], + className="mb-3", + ), + ], + width=6, + ), + ] + ), + dbc.Row( + [ + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.InputGroupText( + "#" + ), + dbc.Input( + id="loan-calc-payments-left", + type="number", + placeholder="Payments Left", + className="form-control-lg", + ), + ], + className="mb-3", + ), + ], + width=6, + ), + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.InputGroupText( + "$" + ), + dbc.Input( + id="loan-calc-payment-amount", + type="number", + placeholder="Payment Amount", + className="form-control-lg", + ), + ], + className="mb-3", + ), + ], + width=6, + ), + ] + ), + html.Div( + id="loan-calc-output", + ), + ] + ), + ], + className="shadow", + ), + ], + title="Loan Calculator", + ), ], start_collapsed=True, always_open=True, @@ -653,3 +772,79 @@ def retrieve_asset_values(clickData): return dash.no_update assets.update_asset_info(config_path="config.yml", db_write=True) return dash.no_update + + +@callback( + Output("loan-calc-output", "children"), + [Input("loan-calc-button", "n_clicks")], + [ + State("loan-calc-loan-amount", "value"), + State("loan-calc-interest", "value"), + State("loan-calc-payments-left", "value"), + State("loan-calc-payment-amount", "value"), + ], + prevent_initial_call=True, +) +def update_loan_calc( + clickData, loan_amount, interest_rate, payments_left, payment_amount +): + """Calculate the loan values.""" + if clickData is None: + return dash.no_update + + missing_values = [] + variables = [ + ("loan_amount", loan_amount), + ("interest", interest_rate), + ("payments_left", payments_left), + ("payment_amount", payment_amount), + ] + + for name, value in variables: + if value is None: + missing_values.append(name) + + if len(missing_values) != 1 or missing_values[0] == "loan_amount": + return dash.no_update + + if missing_values[0] == "interest": + calc_value = loans.get_interest( + current_loan=loan_amount, + payments_left=payments_left, + payment_amount=payment_amount, + ) + elif missing_values[0] == "payments_left": + calc_value = loans.get_payments_left( + current_loan=loan_amount, + interest_rate=interest_rate, + payment_amount=payment_amount, + ) + elif missing_values[0] == "payment_amount": + calc_value = loans.get_payment_amount( + current_loan=loan_amount, + interest_rate=interest_rate, + payments_left=payments_left, + ) + + total_paid = loans.get_total_paid( + current_loan=loan_amount, + interest_rate=interest_rate, + payments_left=payments_left, + ) + + loan_calc_output_div = html.Div( + [ + html.P( + [ + html.B(f"{missing_values[0]}"), + " was calculated to be ", + html.B(f"${calc_value:,.2f}"), + ". The total amount paid is ", + html.B(f"${total_paid:,.2f}"), + ".", + ] + ) + ] + ) + + return loan_calc_output_div diff --git a/folioflex/portfolio/loans.py b/folioflex/portfolio/loans.py index 54e497e..2037960 100644 --- a/folioflex/portfolio/loans.py +++ b/folioflex/portfolio/loans.py @@ -114,7 +114,7 @@ def get_payments_left( loan: Optional[str] = None, current_loan: Optional[float] = None, payment_amount: Optional[float] = None, - interest: Optional[float] = None, + interest_rate: Optional[float] = None, ) -> Union[float, None]: """ Get the info of a loan. @@ -134,7 +134,7 @@ def get_payments_left( The current loan amount. payment_amount : float, optional The payment amount. - interest : float, optional + interest_rate : float, optional The interest rate. Returns @@ -159,9 +159,9 @@ def get_payments_left( return None current_loan = params["current_loan"] payment_amount = params["monthly_payment"] - interest = params["nominal_annual_interest"] / 12 / 100 + interest_rate = params["nominal_annual_interest"] / 12 / 100 - if current_loan is None or payment_amount is None or interest is None: + if current_loan is None or payment_amount is None or interest_rate is None: logger.error( "The config_path or the current_loan, payment_amount, and interest " "should be provided." @@ -169,7 +169,7 @@ def get_payments_left( return None # checks - if interest >= 1: + if interest_rate >= 1: logger.error( "The interest rate should be less than 1 and be written as " "as percentage." @@ -178,8 +178,8 @@ def get_payments_left( # get the payments left payments_left = math.log10( - 1 / (1 - (current_loan * interest) / payment_amount) - ) / math.log10(1 + interest) + 1 / (1 - (current_loan * interest_rate) / payment_amount) + ) / math.log10(1 + interest_rate) return payments_left @@ -187,7 +187,7 @@ def get_payments_left( def get_payment_amount( current_loan: float, payments_left: float, - interest: float, + interest_rate: float, ) -> Union[float, None]: """ Get the amount of the payment. @@ -203,7 +203,7 @@ def get_payment_amount( The current loan amount. payments_left : float The monthly payment. - interest : float + interest_rate : float The interest rate. Returns @@ -217,7 +217,7 @@ def get_payment_amount( """ # checks - if interest >= 1: + if interest_rate >= 1: logger.error( "The interest rate should be less than 1 and be written as " "as percentage." @@ -225,7 +225,9 @@ def get_payment_amount( return None # get the payments left - payment_amount = (current_loan * interest) / (1 - (1 + interest) ** -payments_left) + payment_amount = (current_loan * interest_rate) / ( + 1 - (1 + interest_rate) ** -payments_left + ) return payment_amount @@ -252,12 +254,51 @@ def get_interest( interest : float The amount of interest that will be paid. + References + ---------- + - https://brownmath.com/bsci/loan.htm + """ interest = (payments_left * payment_amount) - current_loan return interest +def get_total_paid( + current_loan: float, interest_rate: float, payments_left: float +) -> float: + """ + Get the total amount paid at the end of the loan. + + Parameters + ---------- + current_loan : float + The current loan amount. + interest_rate : float + The interest rate. + payments_left : float + The amount of payments left. + + Returns + ------- + total_paid : float + The total amount paid at the end of the loan. + + References + ---------- + - https://brownmath.com/bsci/loan.htm + + """ + payment_amount = get_payment_amount( + current_loan=current_loan, + payments_left=payments_left, + interest_rate=interest_rate, + ) + total_paid = payment_amount * payments_left + + return total_paid + + def get_credit_card_value(engine: "Engine", user: Optional[str] = None) -> float: """ Get the value of the loan account. diff --git a/tests/test_asset_loans.py b/tests/test_asset_loans.py index 7a344df..08a4e89 100644 --- a/tests/test_asset_loans.py +++ b/tests/test_asset_loans.py @@ -37,7 +37,7 @@ def test_get_payments_left(): payments_left = loans.get_payments_left( current_loan=A, payment_amount=P, - interest=i, + interest_rate=i, ) calc_payments_left = -math.log10(1 - i * A / P) / math.log10(1 + i) assert payments_left == calc_payments_left @@ -52,7 +52,7 @@ def test_get_payment_amount(): payment_amount = loans.get_payment_amount( current_loan=A, payments_left=N, - interest=i, + interest_rate=i, ) calc_payment_amount = (i * A) / (1 - (1 + i) ** -N) assert payment_amount == calc_payment_amount @@ -70,3 +70,13 @@ def test_get_interest(): ) calc_interest_left = P * N - A assert interest_left == calc_interest_left + + +def test_get_total_paid(): + """Tests the get_total_paid function.""" + A = 400000 + i = 5 / 100 / 12 + N = 430.92 + total_paid = loans.get_total_paid(A, i, N) + calc_total_paid = loans.get_payment_amount(A, N, i) * N + assert total_paid == calc_total_paid