Skip to content
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions financial/amortization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
Wiki: https://en.wikipedia.org/wiki/Amortization_calculator
"""


def level_payment(principal, annual_rate_pct, years, payments_per_year=12):
Comment thread
Nitroxium18 marked this conversation as resolved.
Outdated
Comment thread
Nitroxium18 marked this conversation as resolved.
Outdated
if principal <= 0:
raise ValueError("principal must be > 0")
if years <= 0 or payments_per_year <= 0:
raise ValueError("years and payments_per_year must be > 0")
r = (annual_rate_pct / 100.0) / payments_per_year
n = years * payments_per_year
if r == 0:
return principal / n
factor = (1 + r) ** n
return principal * (r * factor) / (factor - 1)


def amortization_schedule(principal, annual_rate_pct, years, payments_per_year=12, print_annual_summary=False, eps=1e-9):

Check failure on line 19 in financial/amortization.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

financial/amortization.py:19:89: E501 Line too long (121 > 88)
Comment thread
Nitroxium18 marked this conversation as resolved.
Outdated
pmt = level_payment(principal, annual_rate_pct, years, payments_per_year)
r = (annual_rate_pct / 100.0) / payments_per_year
n = years * payments_per_year

balance = float(principal)
schedule = []

if print_annual_summary:
print(
(
f"{'Year':<6}{'Months Pd':<12}{'Tenure Left':<13}"
f"{'Payment/Period':<16}{'Outstanding':<14}"
)

Check failure on line 32 in financial/amortization.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP034)

financial/amortization.py:29:13: UP034 Avoid extraneous parentheses
)


for period in range(1, n + 1):
interest = balance * r
principal_component = pmt - interest

# shortpay on the last period if the scheduled principal would overshoot
if principal_component > balance - eps:
principal_component = balance
payment_made = interest + principal_component
else:
payment_made = pmt

if principal_component < 0 and principal_component > -eps: # clamp tiny negatives

Check failure on line 47 in financial/amortization.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

financial/amortization.py:47:89: E501 Line too long (90 > 88)
principal_component = 0.0

balance = max(0.0, balance - principal_component)
schedule.append([period, payment_made, interest, principal_component, balance])

# streamline for all time periods (monthly/quarterly/biweekly/weekly)
months_elapsed = (round((period * 12) / payments_per_year))

if print_annual_summary and (months_elapsed % 12 == 0 or balance <= eps):
tenure_left_periods = n - period
print(
(
f"{months_elapsed // 12:<6}{months_elapsed:<12}{tenure_left_periods:<13}"

Check failure on line 60 in financial/amortization.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

financial/amortization.py:60:89: E501 Line too long (89 > 88)
f"{pmt:<16.2f}{balance:<14.2f}"
)

Check failure on line 62 in financial/amortization.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP034)

financial/amortization.py:59:13: UP034 Avoid extraneous parentheses
)

if balance <= eps:
break

# normalize any tiny residual
if schedule and schedule[-1][4] <= eps:
schedule[-1][4] = 0.0

return round(pmt, 4), schedule


pmt, sched = amortization_schedule(10000, 5.5, 15, payments_per_year=12, print_annual_summary=True)

Check failure on line 75 in financial/amortization.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

financial/amortization.py:75:89: E501 Line too long (99 > 88)
print(pmt)
Loading