Last active
December 23, 2024 06:51
-
-
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
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
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
#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:
Result
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:
Result
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!