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)