Last active
October 6, 2022 17:17
-
-
Save jnhmcknight/44abdc84956b07ab68759cfde9b9cd0a to your computer and use it in GitHub Desktop.
Debt clearing month calculator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
This takes a JSON file of your current debts, and does a rollover calculation to see how long it will | |
take to pay off all of it, based on clearing the smallest debt first and merging that payment into | |
the next smallest debt's payment, and then repeating that process until all debts have been paid. | |
This calculator assumes that you are not increasing any of the debts in any way (ie: not still using | |
your credit card or credit line to pay for any things.) | |
Save this file locally, and run: | |
python3 ./get-out-of-debt.py | |
You can optionally pass it an APR threshold, which defaults to 7, so that higher interest debts are | |
prioritized for payoff first. However, there is minimal long-term difference when following this debt | |
clearing method whether prioritizing higher interest debt or not. It does however feel better to stop | |
paying anything that acrues interest first. | |
Format of `debt.json` is: | |
[ | |
{ | |
"name": "Some Credit Card", | |
"amount": 2501.23, | |
"apr": 19.99, | |
"payment": 250 | |
}, | |
{ | |
"name": "Some Loan", | |
"amount": 7665.97, | |
"apr": 6.5, | |
"payment": 345.89 | |
}, | |
... | |
] | |
`name` is just a description for you. | |
`amount` is the balance owing today. | |
`apr` is the annual percentage rate, as a percent (ie: in the range of 0-100) | |
`payment` is the monthly payment. | |
""" | |
import json | |
import math | |
import sys | |
with open('debt.json', 'r') as debtfile: | |
debts = json.load(debtfile) | |
class Rollover: | |
def __init__(self, debts): | |
self.debts = debts | |
def by_amount(self, reverse=False): | |
return sorted(self.debts, key=lambda item: item.amount, reverse=reverse) | |
def by_rate(self, reverse=False): | |
return sorted(self.debts, key=lambda item: item.apr, reverse=not reverse) | |
def by_end_date(self, reverse=False): | |
return sorted(self.debts, key=lambda item: item.months, reverse=reverse) | |
def calculate_rollover(self, *, debts=None, apr_threshold=7): | |
if debts is None: | |
debts = self.by_amount() | |
months = 0 | |
extra = 0 | |
print(f'Debt higher than {apr_threshold}%') | |
(months, extra) = self._rollover([debt for debt in debts if debt.apr >= apr_threshold]) | |
if apr_threshold > 0: | |
print(f'Debt lower than {apr_threshold}%') | |
(months, extra) = self._rollover([debt for debt in debts if 0 < debt.apr < apr_threshold], months, extra) | |
print('Debt with 0%') | |
(months, extra) = self._rollover([debt for debt in debts if debt.apr == 0], months, extra) | |
def _rollover(self, debts, *, months=0, extra=0): | |
for index, debt in enumerate(debts): | |
print(f' {debt.name}:') | |
print(f' -> ${debt.amount} @ {debt.apr}% -> ${debt.payment} per month') | |
if months == 0 and extra == 0: | |
new_months = math.ceil(debt.months) | |
print(f' -> ${debt.payment} for {new_months} months') | |
elif months > math.ceil(debt.months): | |
print(f' -> ${debt.payment} for {math.ceil(debt.months)} months') | |
extra += debt.payment | |
continue | |
else: | |
new_loan = debt.calc_months_w_extra(extra, months) | |
new_months = math.ceil(new_loan.months) | |
if months != 0: | |
print(f' -> ${debt.payment} for {months} months') | |
print(f' -> ${new_loan.payment} for {new_months} months') | |
months += new_months | |
extra += debt.payment | |
if (extra - debt.payment) > 0: | |
print(f' -> paid in {months} months instead of {math.ceil(debt.months)} months') | |
return (months, extra) | |
class Debt: | |
def __init__(self, *, apr=None, amount=None, payment=None, months=None, name=None): | |
self.apr = apr | |
self.amount = amount | |
self.payment = payment | |
self.months = months | |
self.name = name | |
def __str__(self): | |
if self.payment is None: | |
return f"{self.name} with ${self.amount} at {self.apr}% for {self.months} months" | |
return f"{self.name} for ${self.amount} at {self.apr}% paying ${self.payment}" | |
@property | |
def months(self): | |
return self._months if self._months is not None else self.calc_months() | |
@months.setter | |
def months(self, value): | |
self._months = value | |
@property | |
def payment(self): | |
return self._payment if self._payment is not None else self.calc_payment() | |
@payment.setter | |
def payment(self, value): | |
self._payment = value | |
@property | |
def mpr(self): | |
return self.apr / 12 / 100 | |
def calc_payment(self): | |
if self.apr == 0: | |
return self.amount/self.months | |
return self.mpr * (1/(1-(1+self.mpr)**(-self.months))) * self.amount | |
def calc_months(self): | |
if self.apr == 0: | |
return self.amount/self.payment | |
return abs(math.log(abs(1 - ((self.amount * self.mpr) / self.payment))) / math.log(1 + self.mpr)) | |
def calc_months_w_extra(self, extra, after_month): | |
return Debt( | |
apr=self.apr, | |
amount=((self.months - after_month) * self.payment), | |
payment=(self.payment + extra), | |
name=self.name, | |
) | |
try: | |
apr_threshold = int(sys.argv[1]) | |
except (ValueError, IndexError): | |
apr_threshold = 7 | |
rollover = Rollover([Debt(**debt) for debt in debts]) | |
rollover.calculate_rollover(apr_threshold=apr_threshold) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment