125 lines
3.9 KiB
Python
125 lines
3.9 KiB
Python
import json
|
|
import os
|
|
from datetime import date
|
|
from dateutil.relativedelta import relativedelta
|
|
import math
|
|
|
|
DATA_DIR = os.environ.get("DATA_DIR", os.path.join(os.path.dirname(__file__), "data"))
|
|
MORTGAGE_FILE = os.path.join(DATA_DIR, "mortgage.json")
|
|
|
|
|
|
def load_mortgage():
|
|
if os.path.exists(MORTGAGE_FILE):
|
|
with open(MORTGAGE_FILE) as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
|
|
def save_mortgage(data):
|
|
with open(MORTGAGE_FILE, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
|
|
def monthly_payment(principal, annual_rate, term_years):
|
|
if annual_rate == 0:
|
|
return round(principal / (term_years * 12), 2)
|
|
r = (annual_rate / 100) / 12
|
|
n = term_years * 12
|
|
payment = principal * (r * (1 + r) ** n) / ((1 + r) ** n - 1)
|
|
return round(payment, 2)
|
|
|
|
|
|
def amortization_schedule(principal, annual_rate, term_years, start_date_str, extra_monthly=0):
|
|
r = (annual_rate / 100) / 12 if annual_rate > 0 else 0
|
|
n = term_years * 12
|
|
base_payment = monthly_payment(principal, annual_rate, term_years)
|
|
total_payment = base_payment + extra_monthly
|
|
|
|
try:
|
|
current = date.fromisoformat(start_date_str)
|
|
except Exception:
|
|
current = date.today().replace(day=1)
|
|
|
|
schedule = []
|
|
balance = principal
|
|
total_interest = 0
|
|
total_principal = 0
|
|
|
|
for i in range(1, n + 1):
|
|
if balance <= 0:
|
|
break
|
|
interest = round(balance * r, 2)
|
|
principal_paid = min(round(total_payment - interest, 2), balance)
|
|
if principal_paid < 0:
|
|
principal_paid = 0
|
|
balance = round(balance - principal_paid, 2)
|
|
total_interest += interest
|
|
total_principal += principal_paid
|
|
|
|
schedule.append({
|
|
"month": i,
|
|
"date": current.strftime("%b %Y"),
|
|
"payment": round(interest + principal_paid, 2),
|
|
"principal": principal_paid,
|
|
"interest": interest,
|
|
"balance": max(balance, 0),
|
|
})
|
|
|
|
if balance <= 0:
|
|
break
|
|
current = current + relativedelta(months=1)
|
|
|
|
return schedule, round(total_interest, 2), len(schedule)
|
|
|
|
|
|
def biweekly_schedule(principal, annual_rate, term_years, start_date_str):
|
|
base = monthly_payment(principal, annual_rate, term_years)
|
|
biweekly_payment = round(base / 2, 2)
|
|
r_biweekly = (annual_rate / 100) / 26 if annual_rate > 0 else 0
|
|
|
|
try:
|
|
current = date.fromisoformat(start_date_str)
|
|
except Exception:
|
|
current = date.today().replace(day=1)
|
|
|
|
balance = principal
|
|
total_interest = 0
|
|
payments = 0
|
|
max_payments = term_years * 26 + 52
|
|
|
|
while balance > 0 and payments < max_payments:
|
|
interest = round(balance * r_biweekly, 2)
|
|
principal_paid = min(round(biweekly_payment - interest, 2), balance)
|
|
if principal_paid < 0:
|
|
principal_paid = 0
|
|
balance = round(balance - principal_paid, 2)
|
|
total_interest += interest
|
|
payments += 1
|
|
if balance <= 0:
|
|
break
|
|
|
|
years = payments / 26
|
|
months_total = round(payments / 26 * 12)
|
|
return round(total_interest, 2), payments, round(years, 1), months_total
|
|
|
|
|
|
def current_balance_estimate(principal, annual_rate, term_years, start_date_str, extra_monthly=0):
|
|
schedule, _, _ = amortization_schedule(
|
|
principal, annual_rate, term_years, start_date_str, extra_monthly)
|
|
today = date.today()
|
|
for row in schedule:
|
|
try:
|
|
row_date = date.strptime(row["date"], "%b %Y") if False else None
|
|
except Exception:
|
|
pass
|
|
# Count months elapsed
|
|
try:
|
|
start = date.fromisoformat(start_date_str)
|
|
months_elapsed = (today.year - start.year) * 12 + (today.month - start.month)
|
|
months_elapsed = max(0, months_elapsed)
|
|
if months_elapsed < len(schedule):
|
|
return schedule[months_elapsed]["balance"]
|
|
return 0
|
|
except Exception:
|
|
return principal
|