Calculate compound interest
#!/usr/bin/env python3 | |
""" | |
Author: Ming Wen (bitmingw@gmail.com) | |
This python script calculates compound interest: | |
1. Given (initial assets value, investment per year, growth rate, number of years), | |
find out final assets value. | |
Example usage: | |
$ python3 compound_interest.py --initial 50000 --invest-per-year 30000 --growth-rate 7.5 --years 15 --find target | |
2. Given (initial assets value, growth rate, number of years, target assets value), | |
find out required investment per year. | |
Example usage: | |
$ python3 compound_interest.py --initial 50000 --growth-rate 7.5 --years 15 --target 1000000 --find invest-per-year | |
3. Given (initial assets value, investment per year, number of years, target assets value), | |
find out required growth rate. | |
Example usage: | |
$ python3 compound_interest.py --initial 50000 --invest-per-year 30000 --years 15 --target 1000000 --find growth-rate | |
4. Given (initial assets value, investment per year, growth rate, target assets value), | |
find out required number of years. | |
Example usage: | |
$ python3 compound_interest.py --initial 50000 --invest-per-year 30000 --growth-rate 7.5 --target 1000000 --find years | |
MIT License | |
Copyright (c) 2020 Ming Wen | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
""" | |
import argparse | |
import math | |
class CompoundInterest: | |
find_type_invest_per_year = ["invest", "invest per year", "invest-per-year", "invest_per_year"] | |
find_type_growth_rate = ["growth", "growth rate", "growth-rate", "growth_rate"] | |
find_type_years = ["years"] | |
find_type_target = ["target"] | |
diff_threshold = 1.0 | |
def __init__(self): | |
self.initial = None | |
self.invest_per_year = None | |
self.growth_rate = None | |
self.years = None | |
self.target = None | |
def find_invest_per_year(self) -> float: | |
assert self.initial != None \ | |
and self.growth_rate != None \ | |
and self.years != None \ | |
and self.target != None | |
# Find the value by binary search | |
lo, hi = 0, self.target - self.initial | |
while True: | |
mi = (lo + hi) / 2 | |
actual_target = CompoundInterest._find_target(self.initial, | |
mi, | |
self.growth_rate, | |
self.years) | |
diff = actual_target - self.target | |
if abs(diff) < CompoundInterest.diff_threshold: | |
return mi | |
elif diff < 0: | |
lo = mi | |
else: | |
hi = mi | |
def find_growth_rate(self) -> float: | |
assert self.initial != None \ | |
and self.invest_per_year != None \ | |
and self.years != None \ | |
and self.target != None | |
if self.initial + self.invest_per_year * self.years >= self.target: | |
return 0.0 | |
# Find the value by binary search | |
lo, hi = 0, (self.target - self.initial) / (self.invest_per_year * self.years) | |
while True: | |
mi = (lo + hi) / 2 | |
actual_target = CompoundInterest._find_target(self.initial, | |
self.invest_per_year, | |
mi, | |
self.years) | |
diff = actual_target - self.target | |
if abs(diff) < CompoundInterest.diff_threshold: | |
return mi | |
elif diff < 0: | |
lo = mi | |
else: | |
hi = mi | |
def find_years(self) -> int: | |
assert self.initial != None \ | |
and self.invest_per_year != None \ | |
and self.growth_rate != None \ | |
and self.target != None | |
if self.initial >= self.target: | |
return 0 | |
year = 1 | |
current = self.initial | |
while True: | |
current *= 1 + self.growth_rate | |
current += self.invest_per_year * pow(math.e, self.growth_rate) | |
if current >= self.target: | |
return year | |
year += 1 | |
def find_target(self) -> float: | |
assert self.initial != None \ | |
and self.invest_per_year != None \ | |
and self.growth_rate != None \ | |
and self.years != None | |
return CompoundInterest._find_target(self.initial, | |
self.invest_per_year, | |
self.growth_rate, | |
self.years) | |
@classmethod | |
def _find_target(cls, | |
initial: float, | |
invest_per_year: float, | |
growth_rate: float, | |
years: int): | |
current = initial | |
for _ in range(years): | |
current *= 1 + growth_rate | |
current += invest_per_year * pow(math.e, growth_rate) | |
return current | |
def main(): | |
parser = argparse.ArgumentParser() | |
parser.add_argument("--initial", | |
type=float, | |
help="initial assets in the portfolio") | |
parser.add_argument("--invest-per-year", | |
type=float, | |
help="contribution to portfolio per year") | |
parser.add_argument("--growth-rate", | |
type=float, | |
help="percentage of portfolio growth per year") | |
parser.add_argument("--years", | |
type=int, | |
help="years of investment") | |
parser.add_argument("--target", | |
type=float, | |
help="target assets value") | |
parser.add_argument("--find", | |
type=str, | |
help="which type of information to find", | |
required=True) | |
args = parser.parse_args() | |
param = CompoundInterest() | |
if args.initial != None: | |
if args.initial < 0: | |
print("Initial assets value must be at least 0") | |
exit(1) | |
param.initial = args.initial | |
else: | |
param.initial = 0 | |
if args.invest_per_year != None: | |
if args.invest_per_year < 0: | |
print("Investment per year must be at least 0") | |
exit(1) | |
param.invest_per_year = args.invest_per_year | |
if args.growth_rate != None: | |
if args.growth_rate < 0: | |
print("Growth rate must be at least 0") | |
exit(1) | |
param.growth_rate = args.growth_rate / 100 | |
if args.years != None: | |
if args.years < 1: | |
print("Number of years must be at least 1") | |
exit(1) | |
param.years = args.years | |
if args.target != None: | |
if args.target < param.initial: | |
print("Target value can't be smaller than initial value") | |
exit(1) | |
param.target = args.target | |
if args.find in param.find_type_invest_per_year: | |
print("Investment per year: {}".format(int(param.find_invest_per_year()))) | |
elif args.find in param.find_type_growth_rate: | |
print("Growth rate: {:3.1f}%".format(param.find_growth_rate() * 100)) | |
elif args.find in param.find_type_years: | |
print("Number of years: {}".format(param.find_years())) | |
elif args.find in param.find_type_target: | |
print("Target: {}".format(int(param.find_target()))) | |
else: | |
print("Unknown find type: {}".format(args.find)) | |
print(""" | |
Supported find types: | |
invest-per-year | |
growth-rate | |
years | |
target | |
""") | |
exit(1) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment