|
import argparse |
|
import bisect |
|
from typing import Any |
|
|
|
from gekko import GEKKO |
|
|
|
|
|
cumulative_deposit_config = { |
|
"grossinterest": 2.6, |
|
"deduction": 3.5 + 1.5, |
|
} |
|
|
|
|
|
individual_monthly_config = { |
|
"grossinterestmap": { |
|
18: 2.5, |
|
24: 2.5, |
|
36: 2.5, |
|
48: 2.5, |
|
60: 2.5, |
|
72: 2.75, |
|
84: 2.75, |
|
96: 3.0, |
|
108: 3.0, |
|
120: 3.0, |
|
132: 3.0, |
|
144: 3.0, |
|
156: 3.0, |
|
168: 3.0, |
|
180: 3.0, |
|
192: 4.0, |
|
204: 6.0, |
|
216: 6.0 |
|
}, |
|
"deduction": 12.5, |
|
} |
|
|
|
|
|
def compounding( |
|
deposit: float, netinterest: float, maxyears: int, instalment: float = 0.0 |
|
) -> float: |
|
"""compounding interest effect""" |
|
for _ in range(maxyears): |
|
deposit += instalment |
|
deposit *= (1 + netinterest / 100) |
|
|
|
return deposit |
|
|
|
|
|
def smallest_number_greater_or_equal(sequence: list[int], number: int) -> int: |
|
"""finds the smallest number in a sequence that is greater than or equal to a number""" |
|
index = bisect.bisect_left(sequence, number) |
|
|
|
if index < len(sequence): |
|
return sequence[index] |
|
|
|
message = f"sequence has no number greater than or equal to {number}" |
|
raise ValueError(message) |
|
|
|
|
|
def cumulative_deposit( |
|
instalment: float, maxyears: int, grossinterest: float, deduction: float |
|
) -> tuple[float, float]: |
|
"""PostaFuturo da Grande deposit policy""" |
|
netinterest = grossinterest * (1 - deduction / 100) |
|
|
|
deposit = instalment * 12 * maxyears |
|
balance = compounding(deposit, netinterest, maxyears) |
|
|
|
return balance, deposit |
|
|
|
|
|
def individual_monthly_savings( |
|
instalment: float, |
|
maxyears: int, |
|
grossinterestmap: dict[int, float], |
|
deduction: float |
|
) -> tuple[float, float]: |
|
"""Piccoli e Buoni savings plan""" |
|
netinterestmap = { |
|
key: value * (1 - deduction / 100) for key, value in grossinterestmap.items() |
|
} |
|
|
|
netinterestseq = sorted(netinterestmap.keys()) |
|
|
|
endmonth = 18 * 12 |
|
maxmonth = min(maxyears * 12, endmonth - 18) |
|
|
|
deposit = 0.0 |
|
balance = 0.0 |
|
|
|
for month in range(1, maxmonth): |
|
month = endmonth - month + 1 |
|
month = smallest_number_greater_or_equal(netinterestseq, month) |
|
|
|
netinterest = netinterestmap[month] |
|
|
|
deposit += instalment |
|
balance += compounding(instalment, netinterest, month // 12) |
|
|
|
return balance, deposit |
|
|
|
|
|
def simulation(instalment: float) -> tuple[int, int, int, int]: |
|
model = GEKKO(remote=False) |
|
|
|
cumulative_deposit_maxyears = model.Var(value=5, lb=0, ub=10, integer=True) |
|
individual_monthly_maxyears = model.Var(value=5, lb=0, ub=8, integer=True) |
|
n = model.Var(value=1, lb=0, ub=2, integer=True) |
|
m = model.Var(value=1, lb=0, ub=1, integer=True) |
|
|
|
def objective() -> float: |
|
cumulative_deposit_X = cumulative_deposit( |
|
instalment, cumulative_deposit_maxyears.value.value, **cumulative_deposit_config |
|
) |
|
individual_monthly_X = individual_monthly_savings( |
|
instalment, individual_monthly_maxyears.value.value, **individual_monthly_config |
|
) |
|
|
|
balance = n * cumulative_deposit_X[0] + m * individual_monthly_X[0] |
|
deposit = n * cumulative_deposit_X[1] + m * individual_monthly_X[1] |
|
|
|
return balance - deposit |
|
|
|
model.Maximize(objective()) |
|
|
|
model.Equation(cumulative_deposit_maxyears + individual_monthly_maxyears <= 18) |
|
model.Equation(n + m <= 2) |
|
model.Equation(n * instalment * 12 <= 6000) |
|
|
|
model.options.SOLVER = 1 # https://stackoverflow.com/a/71580462 |
|
|
|
model.solve(disp=False) |
|
|
|
return ( |
|
cumulative_deposit_maxyears.value.value[0], |
|
individual_monthly_maxyears.value.value[0], |
|
n.value.value[0], |
|
m.value.value[0], |
|
) |
|
|
|
|
|
def commandline() -> dict[str, Any]: |
|
parser = argparse.ArgumentParser() |
|
|
|
parser.add_argument( |
|
'--instalment', |
|
type=float, |
|
default=50.0, |
|
help="fixed monthly deposit amount" |
|
) |
|
|
|
args, _ = parser.parse_known_args() |
|
args = vars(args) |
|
|
|
return args |
|
|
|
|
|
def main(instalment: float): |
|
( |
|
cumulative_deposit_maxyears, |
|
individual_monthly_maxyears, |
|
n, |
|
m |
|
) = map(int, simulation(instalment)) |
|
|
|
balance = cumulative_deposit( |
|
instalment, cumulative_deposit_maxyears, **cumulative_deposit_config |
|
) |
|
print("# of cumulative deposit", n) |
|
print("cumulative deposit", balance) |
|
|
|
balance = individual_monthly_savings( |
|
instalment, individual_monthly_maxyears, **individual_monthly_config |
|
) |
|
print("# of individual monthly", m) |
|
print("individual monthly", balance) |
|
|
|
|
|
if __name__ == "__main__": |
|
args = commandline() |
|
main(**args) |