import math
import json
import csv
import os
from datetime import datetime, date
from pathlib import Path
from dateutil.relativedelta import relativedelta
# Try importing dateutil
try:
from dateutil.relativedelta import relativedelta
DATEUTIL_OK = True
except ImportError:
DATEUTIL_OK = False
# ============================================================
# CONFIGURATION
# ============================================================
HISTORY_FILE = "emi_history.json"
CURRENCY_SYM = "₹"
# ============================================================
# FORMAT HELPERS
# ============================================================
def fmt(amount):
return f"{CURRENCY_SYM}{amount:,.2f}"
def fmt_plain(amount):
return f"{amount:,.2f}"
def draw_bar(value, max_val, width=30, fill="█", empty="░"):
if max_val == 0:
return ""
filled = int((min(value, max_val) / max_val) * width)
return fill * filled + empty * (width - filled)
# ============================================================
# CORE EMI CALCULATION
# ============================================================
def calculate_emi(principal, annual_rate, tenure_months):
"""
EMI = P × r × (1+r)^n / ((1+r)^n - 1)
Where:
P = Principal
r = Monthly interest rate (annual_rate / 12 / 100)
n = Tenure in months
"""
if annual_rate == 0:
return round(principal / tenure_months, 2)
r = annual_rate / 12 / 100
n = tenure_months
emi = principal * r * math.pow(1 + r, n) / (math.pow(1 + r, n) - 1)
return round(emi, 2)
def calculate_summary(principal, annual_rate, tenure_months):
"""Return full summary of loan calculations."""
emi = calculate_emi(principal, annual_rate, tenure_months)
total_payment = round(emi * tenure_months, 2)
total_interest = round(total_payment - principal, 2)
interest_pct = round((total_interest / principal) * 100, 2)
return {
"principal": principal,
"annual_rate": annual_rate,
"tenure_months": tenure_months,
"tenure_years": round(tenure_months / 12, 2),
"emi": emi,
"total_payment": total_payment,
"total_interest": total_interest,
"interest_pct": interest_pct,
}
# ============================================================
# AMORTIZATION TABLE
# ============================================================
def generate_amortization(principal, annual_rate, tenure_months,
start_date=None):
"""
Generate month-by-month amortization schedule.
Returns list of dicts with payment breakdown per month.
"""
emi = calculate_emi(principal, annual_rate, tenure_months)
r = annual_rate / 12 / 100
balance = principal
schedule = []
if start_date is None:
start_date = date.today()
for month in range(1, tenure_months + 1):
interest_component = round(balance * r, 2)
principal_component = round(emi - interest_component, 2)
# Last payment adjustment for rounding
if month == tenure_months:
principal_component = round(balance, 2)
emi_actual = round(principal_component + interest_component, 2)
else:
emi_actual = emi
balance = round(balance - principal_component, 2)
if balance < 0:
balance = 0.0
# Calculate payment date
if DATEUTIL_OK:
pay_date = (start_date + relativedelta(months=month)).strftime("%b %Y")
else:
m = (start_date.month + month - 1) % 12 + 1
y = start_date.year + (start_date.month + month - 1) // 12
pay_date = f"{date(y, m, 1).strftime('%b %Y')}"
schedule.append({
"month": month,
"pay_date": pay_date,
"emi": emi_actual,
"principal": principal_component,
"interest": interest_component,
"balance": balance,
})
return schedule
# ============================================================
# DISPLAY SUMMARY
# ============================================================
def display_summary(summary, label="LOAN SUMMARY"):
p = summary["principal"]
ti = summary["total_interest"]
tp = summary["total_payment"]
# Pie-style ASCII breakdown
p_bar = draw_bar(p, tp, width=25)
ti_bar = draw_bar(ti, tp, width=25)
print("\n" + "="*58)
print(f" {label}")
print("="*58)
print(f" Principal Amount : {fmt(summary['principal'])}")
print(f" Annual Interest : {summary['annual_rate']}%")
print(f" Tenure : {summary['tenure_months']} months "
f"({summary['tenure_years']} years)")
print("-"*58)
print(f" Monthly EMI : {fmt(summary['emi'])}")
print(f" Total Payment : {fmt(summary['total_payment'])}")
print(f" Total Interest : {fmt(summary['total_interest'])} "
f"({summary['interest_pct']}% of principal)")
print("-"*58)
print(f" Principal {fmt_plain(p):>14} {draw_bar(p, tp, 20)}")
print(f" Interest {fmt_plain(ti):>14} {draw_bar(ti, tp, 20)}")
print(f" Total {fmt_plain(tp):>14}")
print("="*58)
# ============================================================
# DISPLAY AMORTIZATION TABLE
# ============================================================
def display_amortization(schedule, show_all=False):
total_months = len(schedule)
print("\n" + "="*72)
print(f" AMORTIZATION SCHEDULE ({total_months} months)")
print("="*72)
print(f" {'Mo':<4} {'Date':<10} {'EMI':>12} {'Principal':>12} "
f"{'Interest':>12} {'Balance':>12}")
print(" " + "-"*68)
# Decide which months to show
if show_all or total_months <= 24:
rows = schedule
else:
# Show first 6, middle hint, last 6
rows = schedule[:6]
mid = total_months // 2
rows_display = schedule[:6]
print_middle_hint = True
if show_all or total_months <= 24:
for row in schedule:
print(f" {row['month']:<4} {row['pay_date']:<10} "
f"{fmt_plain(row['emi']):>12} "
f"{fmt_plain(row['principal']):>12} "
f"{fmt_plain(row['interest']):>12} "
f"{fmt_plain(row['balance']):>12}")
else:
# First 6 months
for row in schedule[:6]:
print(f" {row['month']:<4} {row['pay_date']:<10} "
f"{fmt_plain(row['emi']):>12} "
f"{fmt_plain(row['principal']):>12} "
f"{fmt_plain(row['interest']):>12} "
f"{fmt_plain(row['balance']):>12}")
print(f"\n ... {total_months - 12} more months ...\n")
# Last 6 months
for row in schedule[-6:]:
print(f" {row['month']:<4} {row['pay_date']:<10} "
f"{fmt_plain(row['emi']):>12} "
f"{fmt_plain(row['principal']):>12} "
f"{fmt_plain(row['interest']):>12} "
f"{fmt_plain(row['balance']):>12}")
# Totals
total_emi = sum(r["emi"] for r in schedule)
total_prin = sum(r["principal"] for r in schedule)
total_int = sum(r["interest"] for r in schedule)
print(" " + "-"*68)
print(f" {'TOTAL':<15} {fmt_plain(total_emi):>12} "
f"{fmt_plain(total_prin):>12} {fmt_plain(total_int):>12}")
print("="*72)
# ============================================================
# YEARLY SUMMARY
# ============================================================
def display_yearly_summary(schedule):
yearly = {}
for row in schedule:
year = row["pay_date"].split()[-1]
if year not in yearly:
yearly[year] = {"principal": 0, "interest": 0, "emi": 0}
yearly[year]["principal"] += row["principal"]
yearly[year]["interest"] += row["interest"]
yearly[year]["emi"] += row["emi"]
print("\n" + "="*58)
print(" YEARLY SUMMARY")
print("="*58)
print(f" {'YEAR':<8} {'EMI PAID':>12} {'PRINCIPAL':>12} {'INTEREST':>12}")
print(" " + "-"*50)
for year, vals in sorted(yearly.items()):
print(f" {year:<8} {fmt_plain(vals['emi']):>12} "
f"{fmt_plain(vals['principal']):>12} "
f"{fmt_plain(vals['interest']):>12}")
print("="*58)
# ============================================================
# EMI COMPARISON (Multiple Rates / Tenures)
# ============================================================
def compare_options(principal):
print(f"\n EMI COMPARISON for {fmt(principal)}")
print(" " + "="*70)
rates = [6, 7, 8, 9, 10, 11, 12, 14]
tenures = [12, 24, 36, 60, 84, 120, 180, 240]
# Header
print(f" {'RATE':<6}", end="")
for t in tenures:
y = t // 12
print(f" {str(y)+'yr':>9}", end="")
print()
print(" " + "-"*70)
for rate in rates:
print(f" {rate}% ", end="")
for t in tenures:
emi = calculate_emi(principal, rate, t)
print(f" {fmt_plain(emi):>9}", end="")
print()
print(" " + "="*70)
print(" (Values show monthly EMI in same currency)")
# ============================================================
# PREPAYMENT IMPACT
# ============================================================
def prepayment_impact(principal, annual_rate, tenure_months, prepay_amount, prepay_month):
"""
Calculate how a lump-sum prepayment affects
total interest and remaining tenure.
"""
# Without prepayment
orig = calculate_summary(principal, annual_rate, tenure_months)
# With prepayment — recalculate from prepay month
schedule_before = generate_amortization(principal, annual_rate, tenure_months)
if prepay_month >= tenure_months:
print(" Prepayment month exceeds tenure.")
return
balance_at_prepay = schedule_before[prepay_month - 1]["balance"]
new_principal = max(0, balance_at_prepay - prepay_amount)
if new_principal <= 0:
print(f"\n Prepayment fully closes the loan at month {prepay_month}!")
interest_paid_so_far = sum(
r["interest"] for r in schedule_before[:prepay_month]
)
total_paid = (prepay_month * orig["emi"]) + prepay_amount
saved = orig["total_payment"] - total_paid
print(f" Total paid : {fmt(total_paid)}")
print(f" Interest saved : {fmt(orig['total_interest'] - interest_paid_so_far)}")
return
remaining_months = tenure_months - prepay_month
new_summary = calculate_summary(new_principal, annual_rate, remaining_months)
interest_before = sum(r["interest"] for r in schedule_before[:prepay_month])
total_interest_new = interest_before + new_summary["total_interest"]
interest_saved = round(orig["total_interest"] - total_interest_new, 2)
months_saved = tenure_months - (prepay_month + remaining_months)
print("\n" + "="*55)
print(" PREPAYMENT IMPACT ANALYSIS")
print("="*55)
print(f" Prepayment Amount : {fmt(prepay_amount)} at month {prepay_month}")
print(f" Balance Before : {fmt(balance_at_prepay)}")
print(f" Balance After : {fmt(new_principal)}")
print("-"*55)
print(f" Original Interest : {fmt(orig['total_interest'])}")
print(f" New Total Interest : {fmt(total_interest_new)}")
print(f" Interest Saved : {fmt(interest_saved)}")
print(f" New Monthly EMI : {fmt(new_summary['emi'])}")
print(f" Old EMI : {fmt(orig['emi'])}")
print(f" Months Saved : {months_saved}")
print("="*55)
# ============================================================
# EXPORT TO CSV
# ============================================================
def export_to_csv(schedule, summary, filename=None):
if not filename:
filename = f"emi_schedule_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
# Summary header
writer.writerow(["LOAN EMI SCHEDULE"])
writer.writerow(["Principal", fmt_plain(summary["principal"])])
writer.writerow(["Annual Rate (%)", summary["annual_rate"]])
writer.writerow(["Tenure (months)", summary["tenure_months"]])
writer.writerow(["Monthly EMI", fmt_plain(summary["emi"])])
writer.writerow(["Total Payment", fmt_plain(summary["total_payment"])])
writer.writerow(["Total Interest", fmt_plain(summary["total_interest"])])
writer.writerow([])
# Table header
writer.writerow(["Month", "Date", "EMI",
"Principal", "Interest", "Balance"])
for row in schedule:
writer.writerow([
row["month"], row["pay_date"],
fmt_plain(row["emi"]),
fmt_plain(row["principal"]),
fmt_plain(row["interest"]),
fmt_plain(row["balance"])
])
print(f"\n Exported: {filename}")
return filename
# ============================================================
# SAVE / LOAD HISTORY
# ============================================================
def save_to_history(summary):
history = []
if Path(HISTORY_FILE).exists():
try:
with open(HISTORY_FILE, "r") as f:
history = json.load(f)
except:
pass
summary["saved_at"] = datetime.now().strftime("%d-%m-%Y %H:%M")
history.append(summary)
history = history[-20:]
with open(HISTORY_FILE, "w") as f:
json.dump(history, f, indent=2)
print(" Saved to history.")
def show_history():
if not Path(HISTORY_FILE).exists():
print("\n No history yet.")
return
try:
with open(HISTORY_FILE, "r") as f:
history = json.load(f)
except:
print(" Could not load history.")
return
print("\n" + "="*65)
print(f" CALCULATION HISTORY ({len(history)} entries)")
print("="*65)
print(f" {'DATE':<18} {'PRINCIPAL':>14} {'RATE':>6} "
f"{'MONTHS':>7} {'EMI':>12}")
print(" " + "-"*60)
for h in reversed(history):
print(f" {h.get('saved_at',''):<18} "
f"{fmt_plain(h['principal']):>14} "
f"{h['annual_rate']:>5}% "
f"{h['tenure_months']:>6} "
f"{fmt_plain(h['emi']):>12}")
print("="*65)
# ============================================================
# GET LOAN INPUT
# ============================================================
def get_loan_input():
print("\n Enter Loan Details")
print(" " + "-"*40)
try:
principal = float(input(" Loan Amount : ").strip().replace(",", ""))
if principal <= 0:
print(" Amount must be positive.")
return None, None, None
rate = float(input(" Annual Interest % : ").strip())
if rate < 0 or rate > 100:
print(" Rate must be between 0 and 100.")
return None, None, None
tenure_input = input(" Tenure (months or Xy for years): ").strip()
if tenure_input.lower().endswith("y"):
tenure_months = int(float(tenure_input[:-1]) * 12)
else:
tenure_months = int(tenure_input)
if tenure_months <= 0:
print(" Tenure must be positive.")
return None, None, None
return principal, rate, tenure_months
except ValueError:
print(" Invalid input.")
return None, None, None
# ============================================================
# MAIN MENU
# ============================================================
def print_menu():
print("\n" + "-"*48)
print(" LOAN EMI CALCULATOR")
print("-"*48)
print(" 1. Calculate EMI & summary")
print(" 2. Full amortization table")
print(" 3. Yearly payment summary")
print(" 4. Compare rates & tenures")
print(" 5. Prepayment impact analysis")
print(" 6. Export schedule to CSV")
print(" 7. View calculation history")
print(" 0. Exit")
print("-"*48)
def main():
print("\n" + "="*55)
print(" LOAN EMI CALCULATOR")
print("="*55)
print("\n Calculate EMI for Home, Car, Personal loans.")
print(" Generate full amortization & prepayment analysis.\n")
last_summary = None
last_schedule = None
while True:
print_menu()
choice = input(" > ").strip()
if choice in ["1", "2", "3", "5", "6"]:
if choice in ["2", "3", "5", "6"] and last_summary:
reuse = input("\n Use last loan details? (y/n): ").strip().lower()
if reuse == "n":
last_summary = last_schedule = None
if not last_summary:
p, r, t = get_loan_input()
if p is None:
continue
last_summary = calculate_summary(p, r, t)
last_schedule = generate_amortization(p, r, t)
if choice == "1":
display_summary(last_summary)
save_to_history(last_summary.copy())
elif choice == "2":
display_summary(last_summary)
show_all = input("\n Show ALL months? (y/n): ").strip().lower()
display_amortization(last_schedule, show_all == "y")
elif choice == "3":
display_yearly_summary(last_schedule)
elif choice == "4":
try:
p = float(input("\n Loan amount for comparison: ")
.strip().replace(",", ""))
compare_options(p)
except ValueError:
print(" Invalid amount.")
elif choice == "5":
if not last_summary:
p, r, t = get_loan_input()
if p is None:
continue
last_summary = calculate_summary(p, r, t)
last_schedule = generate_amortization(p, r, t)
display_summary(last_summary)
try:
prepay_amt = float(input("\n Prepayment amount : ").strip().replace(",", ""))
prepay_month = int(input(" At which month : ").strip())
prepayment_impact(
last_summary["principal"],
last_summary["annual_rate"],
last_summary["tenure_months"],
prepay_amt, prepay_month
)
except ValueError:
print(" Invalid input.")
elif choice == "6":
if last_summary and last_schedule:
fname = input(" Filename (Enter for auto): ").strip() or None
export_to_csv(last_schedule, last_summary, fname)
else:
print(" No data to export. Calculate EMI first.")
elif choice == "7":
show_history()
elif choice == "0":
print("\n Goodbye!\n")
break
else:
print(" Invalid choice.")
# ============================================================
# RUN
# ============================================================
if __name__ == "__main__":
main()
No comments:
Post a Comment