Loan EMI Calculator

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: