Initial commit — Moon Household Budget
This commit is contained in:
124
mortgage.py
Normal file
124
mortgage.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import math
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user