Skip to content

Instantly share code, notes, and snippets.

@jnhmcknight
Last active October 6, 2022 17:17
Show Gist options
  • Save jnhmcknight/44abdc84956b07ab68759cfde9b9cd0a to your computer and use it in GitHub Desktop.
Save jnhmcknight/44abdc84956b07ab68759cfde9b9cd0a to your computer and use it in GitHub Desktop.
Debt clearing month calculator
#!/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