From 7eacbe697d078019228aa91a5738d427b52b2aae Mon Sep 17 00:00:00 2001 From: croketillo Date: Fri, 28 Jun 2024 20:12:54 +0200 Subject: [PATCH] First commit --- .gitignore | 172 +++++++++++++++++++++++++++++++++++ README.md | 80 ++++++++++++++++ README.rst | 96 +++++++++++++++++++ compoundercalc/__init__.py | 0 compoundercalc/compounder.py | 143 +++++++++++++++++++++++++++++ compoundercalc/cosa.py | 22 +++++ setup.py | 27 ++++++ test/test.py | 28 ++++++ 8 files changed, 568 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 README.rst create mode 100644 compoundercalc/__init__.py create mode 100644 compoundercalc/compounder.py create mode 100644 compoundercalc/cosa.py create mode 100644 setup.py create mode 100644 test/test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65332b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,172 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..edf9b71 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ + +# Compound Interest Calculator + +This package provides a `CompoundInterestCalculator` class for calculating compound interest for various financial scenarios. + +## Features + +- Calculate the total amount with compound interest over a period of time. +- Determine the time needed to reach a target amount with compound interest. +- Calculate the required recurring deposit to reach a target amount. +- Calculate the required interest rate to reach a target amount. + +## Installation + +You can install the package using pip: + +```bash +pip install compoundercalc +``` + +## Usage + +### Importing the Calculator + +```python +from compoundercalc.compounder import CompoundInterestCalculator +``` + +### Creating an Instance + +```python +calc = CompoundInterestCalculator(initial_deposit=1000, recurring_deposit=150, num_recurring_per_year=12, interest_rate=0.08) +``` + +### Calculating the Final Amount + +```python +result = calc.final_amount(time_years=10) +print(f"The final amount is: {result:.2f}") +``` + +### Determining the Time to Reach a Target Amount + +```python +years, months, days = calc.time_goal(target_amount=100000) +print(f"Time needed to reach the goal: {years} years, {months} months, and {days} days") +``` + +### Calculating the Required Recurring Deposit + +```python +required_deposit = calc.calc_recurring_deposit(target_amount=100000, total_years=10) +print(f"The required recurring deposit is: {required_deposit:.2f}") +``` + +### Calculating the Required Interest Rate + +```python +try: + required_rate = calc.calc_interest_rate(target_amount=100000, total_years=10) + print(f"The required annual interest rate is: {required_rate:.4f}") +except ValueError as e: + print(e) +``` + +## Running Tests + +To run the tests, use the following command: + +```bash +python -m unittest discover -s test +``` + +## License + +This project is licensed under GNU License. See the [LICENSE](LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..099eeea --- /dev/null +++ b/README.rst @@ -0,0 +1,96 @@ +Compound Interest Calculator +============================ + +This package provides a ``CompoundInterestCalculator`` class for +calculating compound interest for various financial scenarios. + +Features +-------- + +- Calculate the total amount with compound interest over a period of + time. +- Determine the time needed to reach a target amount with compound + interest. +- Calculate the required recurring deposit to reach a target amount. +- Calculate the required interest rate to reach a target amount. + +Installation +------------ + +You can install the package using pip: + +.. code:: bash + + pip install compoundercalc + +Usage +----- + +Importing the Calculator +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + from compoundercalc.compounder import CompoundInterestCalculator + +Creating an Instance +~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + calc = CompoundInterestCalculator(initial_deposit=1000, recurring_deposit=150, num_recurring_per_year=12, interest_rate=0.08) + +Calculating the Final Amount +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + result = calc.final_amount(time_years=10) + print(f"The final amount is: {result:.2f}") + +Determining the Time to Reach a Target Amount +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + years, months, days = calc.time_goal(target_amount=100000) + print(f"Time needed to reach the goal: {years} years, {months} months, and {days} days") + +Calculating the Required Recurring Deposit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + required_deposit = calc.calc_recurring_deposit(target_amount=100000, total_years=10) + print(f"The required recurring deposit is: {required_deposit:.2f}") + +Calculating the Required Interest Rate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + try: + required_rate = calc.calc_interest_rate(target_amount=100000, total_years=10) + print(f"The required annual interest rate is: {required_rate:.4f}") + except ValueError as e: + print(e) + +Running Tests +------------- + +To run the tests, use the following command: + +.. code:: bash + + python -m unittest discover -s test + +License +------- + +This project is licensed under GNU License. See the +`LICENSE `__ file for details. + +Contributing +------------ + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/compoundercalc/__init__.py b/compoundercalc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compoundercalc/compounder.py b/compoundercalc/compounder.py new file mode 100644 index 0000000..4916a65 --- /dev/null +++ b/compoundercalc/compounder.py @@ -0,0 +1,143 @@ +#compounder-calc.py + +""" +This module contains the CompoundInterestCalculator class. +""" + +class CompoundInterestCalculator: + """ + A class used to calculate compound interest for various financial scenarios. + """ + def __init__(self, initial_deposit, recurring_deposit, + num_recurring_per_year, interest_rate, + total_years=None): + """ + Initializes the calculator with the given parameters. + + Parameters: + initial_deposit (float): The initial amount invested. + recurring_deposit (float): The amount of each recurring deposit. + num_recurring_per_year (int): The number of recurring deposits per year. + interest_rate (float): The annual interest rate as a decimal (e.g., 0.05 for 5%). + total_years (int, optional): The total number of years. Default is None. + """ + self.initial_deposit = initial_deposit + self.recurring_deposit = recurring_deposit + self.num_recurring_per_year = num_recurring_per_year + self.interest_rate = interest_rate + self.total_years = total_years + + def final_amount(self, time_years): + """ + Calculates the total amount with compound interest. + + Parameters: + time_years (int): The time in years. + + Returns: + float: The total amount at the end of the period. + """ + periods = self.num_recurring_per_year * time_years + periodic_rate = self.interest_rate / self.num_recurring_per_year + + future_value_initial = self.initial_deposit * (1 + periodic_rate) ** periods + future_value_recurring = self.recurring_deposit * ( + ((1 + periodic_rate) ** periods - 1) / periodic_rate + ) + + total_amount = future_value_initial + future_value_recurring + return round(total_amount,2) + + def time_goal(self, target_amount): + """ + Calculates the time needed to reach a target amount with compound interest. + + Parameters: + target_amount (float): The desired final amount. + + Returns: + (int, int, int): The time needed in years, months, and days. + """ + current_amount = self.initial_deposit + time_years = 0 + + while current_amount < target_amount: + time_years += 1 / self.num_recurring_per_year + current_amount = current_amount * (1 + self.interest_rate / self.num_recurring_per_year) + current_amount += self.recurring_deposit + + # Convert fractional years to years, months, and days + total_days = time_years * 365 + years, remainder = divmod(total_days, 365) + months, days = divmod(remainder, 30) + + return int(years), int(months), int(days) + + def calc_recurring_deposit(self, target_amount, total_years): + """ + Calculates the required recurring deposit to reach a target amount with compound interest. + + Parameters: + target_amount (float): The desired final amount. + total_years (int): The total number of years. + + Returns: + float: The required recurring deposit amount. + """ + periods = self.num_recurring_per_year * total_years + periodic_rate = self.interest_rate / self.num_recurring_per_year + + # Calculate future value of the initial deposit + future_value_initial = self.initial_deposit * (1 + periodic_rate) ** periods + + # Calculate the future value factor for recurring deposits + future_value_factor = ((1 + periodic_rate) ** periods - 1) / periodic_rate + + # Calculate the required recurring deposit + required_recurring_deposit = (target_amount - future_value_initial) / future_value_factor + + return required_recurring_deposit + + def calc_interest_rate(self, target_amount, total_years, tolerance=1e-6, max_iterations=1000): + """ + Calculates the required interest rate to reach a target amount with compound interest. + + Parameters: + target_amount (float): The desired final amount. + total_years (int): The total number of years. + tolerance (float): The tolerance level for convergence of the interest rate. + max_iterations (int): The maximum number of iterations for the rate calculation. + + Returns: + float: The required annual interest rate as a decimal rounded to four decimal places + (e.g., 0.0500 for 5%). + + Raises: + ValueError: If the interest rate calculation does not converge within the specified + number of iterations. + """ + def calculate_future_value(rate): + periods = self.num_recurring_per_year * total_years + periodic_rate = rate / self.num_recurring_per_year + future_value_initial = self.initial_deposit * (1 + periodic_rate) ** periods + future_value_recurring = self.recurring_deposit * (((1 + periodic_rate) ** periods - 1) / periodic_rate) + return future_value_initial + future_value_recurring + + low_rate = 0.0 + high_rate = 1.0 + + for _ in range(max_iterations): + mid_rate = (low_rate + high_rate) / 2 + future_value = calculate_future_value(mid_rate) + if abs(future_value - target_amount) < tolerance: + return round(mid_rate, 4) + elif future_value < target_amount: + low_rate = mid_rate + else: + high_rate = mid_rate + + raise ValueError( + f"Interest rate calculation did not converge after {max_iterations} iterations. " + f"Try increasing the number of iterations or adjusting the rate range. " + f"Current range: [{low_rate:.6f}, {high_rate:.6f}]" + ) diff --git a/compoundercalc/cosa.py b/compoundercalc/cosa.py new file mode 100644 index 0000000..39a36ae --- /dev/null +++ b/compoundercalc/cosa.py @@ -0,0 +1,22 @@ +import os + +def remove_trailing_whitespace(file_path): + with open(file_path, 'r') as file: + lines = file.readlines() + + with open(file_path, 'w') as file: + for line in lines: + file.write(line.rstrip() + '\n') + +def process_directory(directory): + for root, _, files in os.walk(directory): + for file in files: + if file.endswith('.py'): + file_path = os.path.join(root, file) + remove_trailing_whitespace(file_path) + print(f"Processed: {file_path}") + +if __name__ == "__main__": + directory = input("Enter the directory to process: ") + process_directory(directory) + print("Trailing whitespace removed from all Python files.") diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..878063f --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +# setup.py + +from setuptools import setup, find_packages + +setup( + name='compoundercalc', + version='0.1.0', + description='A package for compound interest calculations', + long_description=open('README.md').read(), + long_description_content_type='text/markdown', + url='https://github.com/croketillo/compoundercalc', + author='Croketillo', + author_email='croketillo@gmail.com', + license='GNU v3', + packages=find_packages(), + classifiers=[ + 'Development Status :: 3 - Alpha', + 'INTENDED AUDIENCE :: FINANCIAL AND INSURANCE INDUSTRY', + 'License :: OSI APPROVED :: GNU GENERAL PUBLIC LICENSE V3 (GPLV3)', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + ], + python_requires='>=3.7', +) diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..ea8a533 --- /dev/null +++ b/test/test.py @@ -0,0 +1,28 @@ +import unittest +from compoundercalc.compounder import CompoundInterestCalculator + +class TestCompoundInterestCalculator(unittest.TestCase): + + def test_final_amount(self): + calc = CompoundInterestCalculator(1000, 150, 12, 0.08) + result = calc.final_amount(10) + expected = 29661.55 # Este valor debe ser recalculado para asegurarse de que sea correcto. + self.assertAlmostEqual(result, expected, places=2) + + def test_time_goal(self): + calc = CompoundInterestCalculator(1000, 150, 12, 0.08) + years, months, days = calc.time_goal(100000) + self.assertTrue(years > 0 and months >= 0 and days >= 0) + + def test_calc_recurring_deposit(self): + calc = CompoundInterestCalculator(1000, 150, 12, 0.08) + required_deposit = calc.calc_recurring_deposit(100000, 10) + self.assertTrue(required_deposit > 0) + + def test_calc_interest_rate(self): + calc = CompoundInterestCalculator(1000, 150, 12, 0.08) + required_rate = calc.calc_interest_rate(100000, 10) + self.assertTrue(required_rate > 0) + +if __name__ == '__main__': + unittest.main()