Source code for qfinbox.tvm.loans

"""Loan calculations and amortization."""

import pandas as pd

from ..core.validators import validate_positive


[docs] def loan_payment( principal: float, annual_rate: float, years: float, payments_per_year: int = 12, ) -> float: """ Calculate periodic loan payment. Parameters ---------- principal : float Loan principal amount. annual_rate : float Annual interest rate (as decimal). years : float Loan term in years. payments_per_year : int, default 12 Number of payments per year. Returns ------- float Periodic payment amount. Examples -------- >>> loan_payment(300000, 0.05, 30) 1610.46 """ validate_positive(principal, "principal") validate_positive(annual_rate, "annual_rate") validate_positive(years, "years") validate_positive(payments_per_year, "payments_per_year") rate_per_period = annual_rate / payments_per_year num_payments = years * payments_per_year if rate_per_period == 0: return principal / num_payments return float( principal * (rate_per_period * (1 + rate_per_period) ** num_payments) / ((1 + rate_per_period) ** num_payments - 1) )
[docs] def loan_balance( principal: float, annual_rate: float, years: float, payments_made: int, payments_per_year: int = 12, ) -> float: """ Calculate remaining loan balance after specified payments. Parameters ---------- principal : float Original loan principal amount. annual_rate : float Annual interest rate (as decimal). years : float Loan term in years. payments_made : int Number of payments already made. payments_per_year : int, default 12 Number of payments per year. Returns ------- float Remaining loan balance. Examples -------- >>> loan_balance(300000, 0.05, 30, 120) 260108.58 """ validate_positive(principal, "principal") validate_positive(annual_rate, "annual_rate") validate_positive(years, "years") validate_positive(payments_made, "payments_made") validate_positive(payments_per_year, "payments_per_year") rate_per_period = annual_rate / payments_per_year total_payments = years * payments_per_year if payments_made >= total_payments: return 0.0 payment = loan_payment(principal, annual_rate, years, payments_per_year) if rate_per_period == 0: return principal - (payment * payments_made) remaining_payments = total_payments - payments_made return float( payment * (1 - (1 + rate_per_period) ** -remaining_payments) / rate_per_period )
[docs] def total_interest_paid( principal: float, annual_rate: float, years: float, payments_per_year: int = 12, ) -> float: """ Calculate total interest paid over the life of the loan. Parameters ---------- principal : float Loan principal amount. annual_rate : float Annual interest rate (as decimal). years : float Loan term in years. payments_per_year : int, default 12 Number of payments per year. Returns ------- float Total interest paid. Examples -------- >>> total_interest_paid(300000, 0.05, 30) 279767.35 """ payment = loan_payment(principal, annual_rate, years, payments_per_year) total_payments = years * payments_per_year return (payment * total_payments) - principal
[docs] def amortization_schedule( principal: float, annual_rate: float, years: float, payments_per_year: int = 12, ) -> pd.DataFrame: """ Generate loan amortization schedule. Parameters ---------- principal : float Loan principal amount. annual_rate : float Annual interest rate (as decimal). years : float Loan term in years. payments_per_year : int, default 12 Number of payments per year. Returns ------- pd.DataFrame Amortization schedule with columns: Payment, Interest, Principal, Balance. Examples -------- >>> schedule = amortization_schedule(300000, 0.05, 30) >>> schedule.head() Payment Interest Principal Balance 0 1610.46 1250.00 360.46 299639.54 1 1610.46 1248.50 361.96 299277.58 """ validate_positive(principal, "principal") validate_positive(annual_rate, "annual_rate") validate_positive(years, "years") validate_positive(payments_per_year, "payments_per_year") rate_per_period = annual_rate / payments_per_year total_payments = int(years * payments_per_year) payment = loan_payment(principal, annual_rate, years, payments_per_year) schedule = [] balance = principal for _ in range(1, total_payments + 1): interest_payment = balance * rate_per_period principal_payment = payment - interest_payment balance -= principal_payment # Ensure balance doesn't go negative due to rounding if balance < 0: principal_payment += balance balance = 0 schedule.append( { "Payment": payment, "Interest": interest_payment, "Principal": principal_payment, "Balance": balance, } ) if balance == 0: break return pd.DataFrame(schedule)