1+ def level_payment (principal , annual_rate_pct , years , payments_per_year = 12 ):
2+ if principal <= 0 :
3+ raise ValueError ("principal must be > 0" )
4+ if years <= 0 or payments_per_year <= 0 :
5+ raise ValueError ("years and payments_per_year must be > 0" )
6+ r = (annual_rate_pct / 100.0 ) / payments_per_year
7+ n = years * payments_per_year
8+ if r == 0 :
9+ return principal / n
10+ factor = (1 + r ) ** n
11+ return principal * (r * factor ) / (factor - 1 )
12+
13+
14+ def amortization_schedule (principal , annual_rate_pct , years , payments_per_year = 12 , print_annual_summary = False , eps = 1e-9 ):
15+ pmt = level_payment (principal , annual_rate_pct , years , payments_per_year )
16+ r = (annual_rate_pct / 100.0 ) / payments_per_year
17+ n = years * payments_per_year
18+
19+ balance = float (principal )
20+ schedule = []
21+
22+ if print_annual_summary :
23+ print (f"{ 'Year' :<6} { 'Months Pd' :<12} { 'Tenure Left' :<13} { 'Payment/Period' :<16} { 'Outstanding' :<14} " )
24+
25+ for period in range (1 , n + 1 ):
26+ interest = balance * r
27+ principal_component = pmt - interest
28+
29+ # shortpay on the last period if the scheduled principal would overshoot
30+ if principal_component > balance - eps :
31+ principal_component = balance
32+ payment_made = interest + principal_component
33+ else :
34+ payment_made = pmt
35+
36+ if principal_component < 0 and principal_component > - eps : # clamp tiny negatives
37+ principal_component = 0.0
38+
39+ balance = max (0.0 , balance - principal_component )
40+ schedule .append ([period , payment_made , interest , principal_component , balance ])
41+
42+ # streamline for all time periods (monthly/quarterly/biweekly/weekly)
43+ months_elapsed = int (round ((period * 12 ) / payments_per_year ))
44+
45+ if print_annual_summary and (months_elapsed % 12 == 0 or balance <= eps ):
46+ tenure_left_periods = n - period
47+ print (f"{ months_elapsed // 12 :<6} { months_elapsed :<12} { tenure_left_periods :<13} { pmt :<16.2f} { balance :<14.2f} " )
48+
49+ if balance <= eps :
50+ break
51+
52+ # normalize any tiny residual
53+ if schedule and schedule [- 1 ][4 ] <= eps :
54+ schedule [- 1 ][4 ] = 0.0
55+
56+ return round (pmt , 4 ), schedule
57+
58+
59+ pmt , sched = amortization_schedule (10000 , 5.5 , 15 , payments_per_year = 12 , print_annual_summary = True )
60+ print (pmt )
0 commit comments