Skip to content

Commit

Permalink
add loan to budget
Browse files Browse the repository at this point in the history
  • Loading branch information
jkoestner committed Dec 16, 2024
1 parent bc07e65 commit 5b2b3f4
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 14 deletions.
197 changes: 196 additions & 1 deletion folioflex/dashboard/pages/budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
63 changes: 52 additions & 11 deletions folioflex/portfolio/loans.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -159,17 +159,17 @@ 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."
)
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."
Expand All @@ -178,16 +178,16 @@ 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


def get_payment_amount(
current_loan: float,
payments_left: float,
interest: float,
interest_rate: float,
) -> Union[float, None]:
"""
Get the amount of the payment.
Expand All @@ -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
Expand All @@ -217,15 +217,17 @@ 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."
)
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

Expand All @@ -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.
Expand Down
14 changes: 12 additions & 2 deletions tests/test_asset_loans.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

0 comments on commit 5b2b3f4

Please sign in to comment.