Skip to content

Instantly share code, notes, and snippets.

@Kukuster
Last active December 23, 2024 06:51
Show Gist options
  • Save Kukuster/90b594cc1be0b9c246bf8e92cb6d009f to your computer and use it in GitHub Desktop.
Save Kukuster/90b594cc1be0b9c246bf8e92cb6d009f to your computer and use it in GitHub Desktop.
Inflation adjusted total payment for a loan with fixed monthly payments, given a total loan amount, number of monthly payments, optionally interest rate (positive -> plays in bank's favor) and optionally a predicted inflation (positive -> plays in your favor). Agnostic to currency. Optionally, prints a lot of details
from typing import Any, Callable, Optional
import numpy as np
def ffmt(f: float|np.floating, precision: int = 4) -> str:
"""
Formats floating point value to string without trailing zeros and dot.
Disallows scientific notation.
"""
return (f'%0.{precision}f' % f).rstrip('0').rstrip('.')
def installment_inflation_adjusted_pay(
TOTAL_AMOUNT: float|np.float128,
n_monthly_pays: int,
yearly_inflation: Optional[float|np.float128] = None,
monthly_inflation: Optional[float|np.float128] = None,
yearly_interest_rate: Optional[float|np.float128] = None,
monthly_interest_rate: Optional[float|np.float128] = None,
starting_month_n: int = 1,
verbose: bool = False,
verbose_func: Callable[..., None] = print,
):
"""
Calculates inflation adjusted total payment for a loan with fixed monthly payments.
Parameters:
-----------
`TOTAL_AMOUNT`: float
Total amount of the loan.
`n_monthly_pays`: int
Number of monthly payments.
`yearly_inflation`|`monthly_inflation`: float, optional
Predicted inflation rate. Only one should be provided. If not provided, 0 is assumed. Use a negative value for deflation. Inflation is assumed to apply uniformly over time.
`yearly_interest_rate`|`monthly_interest_rate`: float, optional
Interest rate. Only one should be provided. If not provided, 0 is assumed.
`starting_month_n`: int, optional
Index of month where a payment starts. Default is `1` (starting next month).
`0` (starting immediately this month) is also common.
Inflation is applied starting from the current month (not the first payment month),
such that the returned total inflation adjusted payment is in terms
of the current month currency value (otherwise this parameter would not make any difference).
`verbose`: bool, optional
If True, prints the details of intermediate calculations.
`verbose_func`: Callable[..., None], optional
Function to use for printing. Default is the builtin print.
Returns:
--------
float
Inflation adjusted total payment.
"""
if TOTAL_AMOUNT <= 0:
raise ValueError("Total amount should be positive")
if n_monthly_pays <= 0:
raise ValueError("Number of monthly payments should be positive")
TOTAL_AMOUNT = np.float128(TOTAL_AMOUNT)
# if both yearly and monthly provided, raise an error
if yearly_inflation is not None and monthly_inflation is not None:
raise ValueError("Only one inflation rate should be provided")
if yearly_interest_rate is not None and monthly_interest_rate is not None:
raise ValueError("Only one interest rate should be provided")
# if none provided, set to 0
if yearly_inflation is None and monthly_inflation is None:
yearly_inflation = monthly_inflation = np.float128(0.0)
if yearly_interest_rate is None and monthly_interest_rate is None:
yearly_interest_rate = monthly_interest_rate = np.float128(0.0)
# if only one provided, calculate the other
if yearly_inflation is not None:
monthly_inflation = (1+np.float128(yearly_inflation))**(1/12) - 1
if verbose and monthly_inflation != 0:
verbose_func(f"monthly inflation rate: (1+{ffmt(yearly_inflation)})^(1/12) - 1 = {ffmt(monthly_inflation)}")
elif monthly_inflation is not None:
yearly_inflation = (1+np.float128(monthly_inflation))**12 - 1
if verbose and yearly_inflation != 0:
verbose_func(f"yearly inflation rate: (1+{ffmt(monthly_inflation)})^12 - 1 = {ffmt(yearly_inflation)}")
if yearly_interest_rate is not None:
monthly_interest_rate = (1+np.float128(yearly_interest_rate))**(1/12) - 1
if verbose and monthly_interest_rate != 0:
verbose_func(f"monthly interest rate: (1+{ffmt(yearly_interest_rate)})^(1/12) - 1 = {ffmt(monthly_interest_rate)}")
elif monthly_interest_rate is not None:
yearly_interest_rate = (1+np.float128(monthly_interest_rate))**12 - 1
if verbose and yearly_interest_rate != 0:
verbose_func(f"yearly interest rate: (1+{ffmt(monthly_interest_rate)})^12 - 1 = {ffmt(yearly_interest_rate)}")
monthly_inflation = np.float128(monthly_inflation)
yearly_inflation = np.float128(yearly_inflation)
monthly_interest_rate = np.float128(monthly_interest_rate)
yearly_interest_rate = np.float128(yearly_interest_rate)
monthly_inflation_coef: np.float128 = 1 + monthly_inflation
if verbose and yearly_inflation != 0:
verbose_func(f"monthly inflation coefficient: 1 + {ffmt(monthly_inflation)} = {ffmt(monthly_inflation_coef)}")
monthly_fee_coef_A: np.float128 = 1 + (monthly_interest_rate * n_monthly_pays) # from monthly interest rate
monthly_fee_coef_B: np.float128 = 1 + (yearly_interest_rate * (n_monthly_pays/12)) # from yearly interest rate
monthly_fee_coef: np.float128 = np.sqrt(monthly_fee_coef_A*monthly_fee_coef_B)
if verbose and monthly_interest_rate != 0:
verbose_func(f"total interest coefficient: 1 + {ffmt(monthly_interest_rate)}*{n_monthly_pays} = {ffmt(monthly_fee_coef)}")
monthly_payment_without_interest: np.float128 = (TOTAL_AMOUNT / n_monthly_pays)
monthly_payment: np.float128 = monthly_payment_without_interest * monthly_fee_coef
if verbose:
if yearly_interest_rate != 0:
verbose_func(f"monthly payment: ({ffmt(TOTAL_AMOUNT,2)} / {n_monthly_pays}) * {ffmt(monthly_fee_coef)} = {ffmt(monthly_payment,2)}")
verbose_func(f"would-be monthly payment without interest: ({ffmt(TOTAL_AMOUNT,2)} / {n_monthly_pays}) = {ffmt(monthly_payment_without_interest,2)}")
else:
verbose_func(f"monthly payment: {ffmt(TOTAL_AMOUNT,2)} / {n_monthly_pays} = {ffmt(monthly_payment_without_interest,2)}")
all_monthly_payments: list[np.float128] = [
monthly_payment / (monthly_inflation_coef**mo_num)
for mo_num in range(starting_month_n, n_monthly_pays + starting_month_n)
]
if verbose and yearly_inflation != 0:
verbose_func("")
verbose_func("monthly payments (inflation adjusted): ")
for i, p in enumerate(all_monthly_payments):
mo_num = i + starting_month_n
verbose_func(f"#{mo_num}: {ffmt(monthly_payment,2)}/{ffmt(monthly_inflation_coef**mo_num)} = {ffmt(p,2)}")
verbose_func("")
total_nominal_payment: np.float128 = n_monthly_pays*monthly_payment
if verbose:
if yearly_interest_rate != 0:
verbose_func(f"total nominal payment: {n_monthly_pays}*{ffmt(monthly_payment,2)} = {ffmt(total_nominal_payment,2)}")
verbose_func(f"would-be total payment without interest: {n_monthly_pays}*{ffmt(monthly_payment_without_interest,2)} = {ffmt(TOTAL_AMOUNT,2)}")
else:
verbose_func(f"TOTAL PAYMENT (WITH INTEREST): {n_monthly_pays}*{ffmt(monthly_payment,2)} = {ffmt(TOTAL_AMOUNT,2)}")
total_inflation_adjusted_payment = np.sum(all_monthly_payments)
if verbose:
nominal_overpayment = total_nominal_payment - TOTAL_AMOUNT
nominal_underpayment = TOTAL_AMOUNT - total_nominal_payment
if yearly_inflation != 0:
verbose_func(f"TOTAL INFLATION ADJUSTED PAYMENT: {ffmt(total_inflation_adjusted_payment,2)}")
if yearly_interest_rate > 0:
verbose_func(f"nominal overpayment due to interest (in bank's favor): {ffmt(nominal_overpayment,2)}")
elif yearly_interest_rate < 0:
verbose_func(f"nominal underpayment due to negative interest (in your favor): {ffmt(nominal_underpayment,2)}")
total_overpayment = total_inflation_adjusted_payment - TOTAL_AMOUNT
total_underpayment = TOTAL_AMOUNT - total_inflation_adjusted_payment
if total_overpayment >= 0:
verbose_func(f"real inflation-corrected overpayment (in bank's favor): {ffmt(total_overpayment,2)}")
else:
verbose_func(f"real inflation-corrected underpayment (in your favor, \"you are saving ...\"): {ffmt(total_underpayment,2)}")
elif yearly_interest_rate != 0:
if yearly_interest_rate > 0:
verbose_func(f"nominal overpayment due to interest (in bank's favor): {ffmt(nominal_overpayment,2)}")
elif yearly_interest_rate < 0:
verbose_func(f"nominal underpayment due to negative interest (in your favor): {ffmt(nominal_underpayment,2)}")
return total_inflation_adjusted_payment
@Kukuster
Copy link
Author

Kukuster commented Dec 14, 2024

#installment #mortgage #payinparts #credit #bank #loan #inflation #interest #rate #python #personal #finance #finances #economics

Most of the code is an optional detailed output of intermediate calculations, set with the function parameter flag verbose=True

Examples:

1. Pay in parts (without nominal overpayment).

This is useful to calculate, because given positive inflation we effectively save money (not counting that we don't pay everything immediately).

Let's say we bought a product at a price of 1000 moneys, paid in 10 parts monthly (100/mo), starting immediately this month. Let's also say, our currency inflation rate is predicted to be 18% annually. Even though nominally our total payment is 10 * 100 [moneys] = 1000 [moneys], inflation plays in our favor more and more each next month when we make a payment.

We calculate the inflation-adjusted total payment as follows:

installment_inflation_adjusted_pay(
    TOTAL_AMOUNT=1000,
    n_monthly_pays=10,
    yearly_inflation=0.18,
    verbose=True,
    starting_month_n=0
)
Result
# prints:
"""
monthly inflation rate: (1+0.18)^(1/12) - 1 = 0.0139
monthly inflation coefficient: 1 + 0.0139 = 1.0139
monthly payment: 1000 / 10 = 100

monthly payments (inflation adjusted): 
#0: 100/1 = 100
#1: 100/1.0139 = 98.63
#2: 100/1.028 = 97.28
#3: 100/1.0422 = 95.95
#4: 100/1.0567 = 94.63
#5: 100/1.0714 = 93.34
#6: 100/1.0863 = 92.06
#7: 100/1.1014 = 90.8
#8: 100/1.1167 = 89.55
#9: 100/1.1322 = 88.33

TOTAL PAYMENT (WITH INTEREST): 10*100 = 1000
TOTAL INFLATION ADJUSTED PAYMENT: 940.56
real inflation-corrected underpayment (in your favor, "you are saving ..."): 59.44
"""
# outputs:
# 940.5567549047053845

2. Mortgage

Let's say we take a mortgage for 1'000'000 moneys paid in fixed parts monthly for 20 years (forget about a large initial payment, so no need to calculate anything there). The annual interest rate is 7%, while predicted annual inflation is 18%. We pay some x moneys of initial payment, and further payments we care about for such calculations are starting next month.

We calculate the inflation-adjusted total payment as follows:

installment_inflation_adjusted_pay(
    TOTAL_AMOUNT=1_000_000,
    n_monthly_pays=(12*20),
    yearly_inflation=0.18,
    yearly_interest_rate=0.07,
    starting_month_n=1,
    verbose=True
)
Result
# prints:
"""
monthly inflation rate: (1+0.18)^(1/12) - 1 = 0.0139
monthly interest rate: (1+0.07)^(1/12) - 1 = 0.0057
monthly inflation coefficient: 1 + 0.0139 = 1.0139
total interest coefficient: 1 + 0.0057*240 = 2.3784
monthly payment: (1000000 / 240) * 2.3784 = 9910
would-be monthly payment without interest: (1000000 / 240) = 4166.67

monthly payments (inflation adjusted): 
#1: 9910/1.0139 = 9774.25
#2: 9910/1.028 = 9640.36
#3: 9910/1.0422 = 9508.31
...
#12: 9910/1.18 = 8398.31
...
#238: 9910/26.6477 = 371.89
#239: 9910/27.0178 = 366.8
#240: 9910/27.393 = 361.77

total nominal payment: 240*9910 = 2378400.25
would-be total payment without interest: 240*4166.67 = 1000000
TOTAL INFLATION ADJUSTED PAYMENT: 687495.27
nominal overpayment due to interest (in bank's favor): 1378400.25
real inflation-corrected underpayment (in your favor, "you are saving ..."): 312504.73
"""

# outputs:
# 687495.27200530013005

So nominally the bank will be awaiting 1'000'000 moneys, but inflation-adjusted it will be equivalent to 687'495 moneys (in present moneys terms). Let's say the initial payment was 500'000 moneys, and the full price is 900'000 moneys paid immediately. So instead of 900'000 moneys you end up paying 500'000 + 687'495 = 1'187'495 [moneys], which is not too bad!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment