Repository: MartinPyka/financial_life Branch: master Commit: 24c4ce0a026f Files: 48 Total size: 185.7 KB Directory structure: gitextract_j_yn7vd1/ ├── .github/ │ └── workflows/ │ └── sonarcloud.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST ├── README.md ├── docs/ │ ├── 01_first_simulation.md │ ├── 02_using_callables_for_dynamic_changes.md │ ├── 03_dependencies_between_accounts.md │ └── README.md ├── financial_life/ │ ├── README.md │ ├── __init__.py │ ├── calendar_help/ │ │ └── __init__.py │ ├── constants/ │ │ ├── __init__.py │ │ └── intervals.py │ ├── examples/ │ │ ├── README.md │ │ ├── __init__.py │ │ ├── dependencies.py │ │ ├── meta_data.md │ │ ├── meta_data.py │ │ └── simple_examples.py │ ├── financing/ │ │ ├── __init__.py │ │ ├── accounts.py │ │ ├── colors.py │ │ ├── identity.py │ │ ├── plotting.py │ │ ├── test_financing.py │ │ ├── test_meta.py │ │ ├── test_status.py │ │ └── validate.py │ ├── products/ │ │ ├── __init__.py │ │ └── germany/ │ │ ├── __init__.py │ │ └── lbs/ │ │ └── __init__.py │ ├── reports/ │ │ ├── __init__.py │ │ ├── excel.py │ │ └── html.py │ ├── tax/ │ │ ├── __init__.py │ │ └── germany/ │ │ └── __init__.py │ ├── templates/ │ │ ├── __init__.py │ │ └── html/ │ │ ├── __init__.py │ │ └── standard/ │ │ ├── __init__.py │ │ ├── account_details.html │ │ ├── index.html │ │ └── render.py │ └── test_general.py ├── requirements.txt ├── setup.py └── unittests.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/sonarcloud.yml ================================================ # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. # This workflow helps you trigger a SonarCloud analysis of your code and populates # GitHub Code Scanning alerts with the vulnerabilities found. # Free for open source project. # 1. Login to SonarCloud.io using your GitHub account # 2. Import your project on SonarCloud # * Add your GitHub organization first, then add your repository as a new project. # * Please note that many languages are eligible for automatic analysis, # which means that the analysis will start automatically without the need to set up GitHub Actions. # * This behavior can be changed in Administration > Analysis Method. # # 3. Follow the SonarCloud in-product tutorial # * a. Copy/paste the Project Key and the Organization Key into the args parameter below # (You'll find this information in SonarCloud. Click on "Information" at the bottom left) # # * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN # (On SonarCloud, click on your avatar on top-right > My account > Security # or go directly to https://sonarcloud.io/account/security/) name: SonarCloud analysis on: push: branches: [ "master" ] pull_request: branches: [ "master" ] workflow_dispatch: permissions: pull-requests: read # allows SonarCloud to decorate PRs with analysis results jobs: Analysis: runs-on: ubuntu-latest steps: - name: Analyze with SonarCloud # You can pin the exact commit or the version. # uses: SonarSource/sonarcloud-github-action@de2e56b42aa84d0b1c5b622644ac17e505c9a049 uses: SonarSource/sonarcloud-github-action@de2e56b42aa84d0b1c5b622644ac17e505c9a049 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret) with: # Additional arguments for the sonarcloud scanner args: # Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu) # mandatory -Dsonar.projectKey= -Dsonar.organization= -X # Comma-separated paths to directories containing main source files. #-Dsonar.sources= # optional, default is project base directory # When you need the analysis to take place in a directory other than the one from which it was launched #-Dsonar.projectBaseDir= # optional, default is . # Comma-separated paths to directories containing test source files. #-Dsonar.tests= # optional. For more info about Code Coverage, please refer to https://docs.sonarcloud.io/enriching/test-coverage/overview/ # Adds more detail to both client and server-side analysis logs, activating DEBUG mode for the scanner, and adding client-side environment variables and system properties to the server-side log of analysis report processing. #-Dsonar.verbose= # optional, default is false ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # 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/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # Release jobs release.sh ================================================ FILE: CHANGELOG.md ================================================ # 0.9.4 (31.05.2017 * added support for export to Excel # 0.9.3 (13.01.2017) * changed some bugs for rendering HTML content # 0.9.2 (05.01.2017) * financial_life now supports meta-data for account objects and payments # 0.9.1 * minor issues with DataFrame support * plots in examples are displayed, when examples are called from command line * improved setup-script # 0.9 * financial-life supports DataFrames # 0.8 * first release on Github ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MANIFEST ================================================ # file GENERATED by distutils, do NOT edit setup.py financial_life/__init__.py financial_life/test_general.py financial_life/calendar_help/__init__.py financial_life/examples/__init__.py financial_life/examples/dependencies.py financial_life/examples/meta_data.py financial_life/examples/simple_examples.py financial_life/financing/__init__.py financial_life/financing/accounts.py financial_life/financing/colors.py financial_life/financing/identity.py financial_life/financing/plotting.py financial_life/financing/test_financing.py financial_life/financing/test_meta.py financial_life/financing/test_status.py financial_life/financing/validate.py financial_life/products/germany/lbs/__init__.py financial_life/reports/__init__.py financial_life/reports/html.py financial_life/tax/germany/__init__.py financial_life/templates/html/standard/__init__.py financial_life/templates/html/standard/account_details.html financial_life/templates/html/standard/index.html financial_life/templates/html/standard/render.py ================================================ FILE: README.md ================================================ # financial-life A framework for analysing financial products in personalized contexts
Latest Release latest release
[CHANGELOG.md](CHANGELOG.md) # Description financial_life is an opinionated framework written in Python that allows to simulate monetary flows between different types of accounts. These simulations allow a deeper understanding of financial plans and a better comparison of financial products (in particular loan conditions) for personal circumstances. With financial_life you can * analyse loan conditions and payment strategies * describe the properties of your financial plans with a few lines of code * create dynamic monetary flows between accounts for modeling more realistic scenarios * extend the code by controller functions (e.g. for modeling tax payments) View [documentation](docs/README.md) for a more detailed introduction. # Example Say you want to model an account with regular income and payments to a loan ```python from financial_life.financing import accounts as a from datetime import timedelta, datetime # create a private bank account and a loan account account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account') loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit') # add these accounts to the simulation simulation = a.Simulation(account, loan) # describe monetary flows between accounts simulation.add_regular('Income', account, 2000, interval = 'monthly') simulation.add_regular(account, loan, lambda: min(1500, -loan.account), interval = 'monthly') # simulate for ten years simulation.simulate(delta = timedelta(days=365*10)) # plot the data simulation.plt_summary() # print reports summarized by years print(account.report.yearly()) print(loan.report.yearly()) # analyze data print("Interests on bank account: %.2f" % sum(account.report.yearly().interest)) print("Interests on loan account: %.2f" % sum(loan.report.yearly().interest)) ``` The output will look like this: Simple simulation in financial_life Main account Date account output input interest ---------- --------- --------- -------- ---------- 31.12.2016 2000.32 -3000.00 4000.00 0.32 31.12.2017 8005.58 -18000.00 24000.00 5.26 31.12.2018 14016.85 -18000.00 24000.00 11.27 31.12.2019 20034.13 -18000.00 24000.00 17.28 31.12.2020 26057.42 -18000.00 24000.00 23.29 31.12.2021 32086.74 -18000.00 24000.00 29.32 31.12.2022 46271.00 -9853.30 24000.00 37.56 31.12.2023 70330.32 0.00 24000.00 59.32 31.12.2024 94413.68 0.00 24000.00 83.36 31.12.2025 118521.15 0.00 24000.00 107.47 01.10.2026 138521.15 0.00 20000.00 0.00 House Credit Date account interest payment ---------- --------- ---------- --------- 31.12.2016 -97190.22 -190.22 3000.00 31.12.2017 -80064.23 -874.01 18000.00 31.12.2018 -62766.98 -702.75 18000.00 31.12.2019 -45296.76 -529.78 18000.00 31.12.2020 -27652.02 -355.26 18000.00 31.12.2021 -9830.65 -178.63 18000.00 31.12.2022 0.00 -22.65 9853.30 31.12.2023 0.00 0.00 0.00 31.12.2024 0.00 0.00 0.00 31.12.2025 0.00 0.00 0.00 Interests on bank account: 374.45 Interests on loan account: -2853.30 Now let's say, we put some money on a special savings account with better interests, because we want to purchase in two years a car. With financial_life, you just add the necessary changes to your model. ```python # create new account savings = a.Bank_Account(amount = 5000, interest = 0.007, name = 'Savings') # add it to the simulation (or create a new simulation with all three accounts) simulation.add_account(savings) # add regular payment to the savings-account simulation.add_regular(account, savings, 500, interval = 'monthly') # somewhere in the distant future we will make a payment to # the vendor of a car simulation.add_unique(savings, 'Vendor of a car', 10000, '17.03.2019') ``` The plot will now include the savings-account as well. Simple simulation in financial_life You can also export the simulation to HTML to explore your model in the browser: ```python from financial_life.reports import html html.report(simulation, style="standard", output_dir = result_folder) ``` Simple simulation in financial_life You can analyse the reports as [pandas](https://github.com/pandas-dev/pandas) DataFrame as well and export it to excel: ```python import pandas as pd from financial_life.reports import excel account.report.as_df() # Hello pandas excel.report(simulation, filename='reports.xls') # explore the results in excel ``` [Here](financial_life/examples/README.md) are more examples. financial_life supports: * [dependencies between accounts](financial_life/examples/dependencies.py), e.g. to model how the ownership of a property rises when the loan decreases * [meta-data](financial_life/examples/meta_data.md), e.g. for writing tax-calculations, which require additional knowledge about your payments * [controller-functions](financial_life/examples/meta_data.md) for dynamic changes of the simulation properties during simulation # Installation financial_life is available in version 0.9.2. It is written in Python 3.4 and has not been tested for Python 2.x. To get a working environment, simply do git clone https://github.com/MartinPyka/financial_life.git cd financial_life virtualenv venv source venv/bin/activate pip install -r requirements.txt # test an example python financial_life/examples/simple_examples.py For installing the package: git clone https://github.com/MartinPyka/financial_life.git cd financial_life python setup.py install Or use pip pip install financial_life You can checkout the example with python financial_life/examples/simple_examples.py # Why financial_life financial_life was designed with the idea in mind that any line of code should contribute to the description of the problem you want to model. In spreadsheets, you would deal with a lot of auxiliary tables to accurately calculate the course of a loan influenced by incoming payments and generated interests. In financial_life, you just create your loan account with the given interests rate and you define the regular payments going into this loan account. That's it. Changes in the model and the exploration of different parameters within this model are therefore way easier to accomplish than in a spreadsheet-based simulation. ================================================ FILE: docs/01_first_simulation.md ================================================ # The first simulation In this chapter, we are going to introduce the basic concepts of creating a simulation in order to analyse monetary flows and growth on bank accounts. In the following, financial_life will be abbreviated with fl. Each simulation setup can be devided into three different steps: 1. Define the accounts involved in the simulation 2. Define monetary flows between these accounts 3. Simulate After that you have several options to explore the outcome of your simulation. ## 1. Define the accounts involved in the simulation The module `financial_life.financing.accounts` features some basic accounts like a normal bank account with yearly interests and a loan account that does not allow to be in the positive value range and only permits money transferd to the account but not from the account. The package also contains a Property account that establishes a dependency to a loan account. We will come to that later. Lets start with creating an ordinary bank account, which you would use to receive your salary and pay your bills. This account is called `Bank_Account` and it is defined in the following way: ```python class Bank_Account(Account): """ This is a normal bank account that can be used to manage income and outgoings within a normal household """ def __init__(self, amount, interest, date = None, name = None): ```
Keyword Argument Description
amount The amount of money to start with
interest The yearly interest rate for this account. 1% correspond to 0.01 as argument value
date The date, when this account should exist. Before this date, the account will not be simulated. For quick simulation, where this nuance is not relevant, this field can be left empty. As date, datetime and strings in the format '%d.%m.%y', '%m/%d/%Y' are accepted.
name Simply a string representation of the account id. If empty, fl will create a random string sequence.
So an ordinary bank account with a volume of 1000 and an interest rate of 0.1% is defined like this. ```python from financial_life.financing import accounts as a account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account') ``` A `loan` account, although its behavior is slightly different, is defined in the same manner. For a loan of 100,000 you would create a `loan` account like this: ```python loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit') ``` Last but not least, you need to assign these accounts to a simulation. This is a one-liner as well. ```python simulation = a.Simulation(account, loan) ``` ## 2. Define monetary flows between these accounts Next. we define monetary flows between these accounts. Here, we will introduce the methods for regular and unique payments but we will cover only a part of it. In later chapters, we will dig deeper into the kind of stuff that you can create with it. The simulation class is also defined in `financial_life.financing.accounts` and looks like this ```python def add_regular(self, from_acc, to_acc, payment, interval, date_start=datetime.min, day=1, name = '', date_stop = None, fixed = False): def add_unique(self, from_acc, to_acc, payment, date, name = '', fixed = False): ```
Keyword Argument Description
from_acc, to_acc The account objects that define from where money flows and to which account money flows. For money transfers that involve accounts outside of the simulation, a string can be used.
payment The amount of money that is transfered.
interval 'monthly' or 'yearly' are possible currently.
date_start / date The start date of this regular payments. It can be either a datetime object or a string. It has also a default value, so it does not need to be used for quick simulations.
day The day in a month on which this transfer should be initiated.
name Name of the money transfer. Corresponds to the subject in a normal money transfer.
date_stop Stop date of regular payments. Can be either datetime, string or even a callable. We will cover this later.
fixed Whether sender and receiver of the money should insist of the full money transfer. If, for example, a loan has only 100 to be payed, but the transfer defines 150, this would cause an error, when `fixed` is true. If it is false, the rest money is transfered back.
Here are two examples of how money flows can be defined. The first one describes the salary, coming from an account outside of this simulation. The second one is a money transfer within our simulation. ```python simulation.add_regular('Income', account, 2000, interval = 'monthly') simulation.add_regular(account, loan, 1500, interval = 'monthly') ``` ## 3. Simulate The simulation is started for a given amount of time. This period is either defined through a stop date or by a time interval delta. If both keywords are used the earlier stop date ends the simulation. ```python def simulate(self, date_stop = None, delta = None) ```
Keyword Argument Description
date_stop Stop date of the simulation
delta Number of days to simulate.
In order to simulate our model for 10 years from now on, we use the delta-keyword. ```python simulation.simulate(delta = timedelta(days=365*10)) ``` ## Explore the simulation Here are some examples of exploring the outcome of your data. ```python # use matplotlib for a graphical summary of the simulation simulation.plt_summary() # print reports summarized in years print(account.report.yearly()) print(loan.report.yearly()) # analyze data print("Interests on bank account: %.2f" % sum(account.report.interest)) print("Interests on loan account: %.2f" % sum(loan.report.interest)) # create html report cwd = os.path.dirname(os.path.realpath(__file__)) result_folder = cwd + '/example2' html.report(simulation, style="standard", output_dir = result_folder) ``` ## Code snippet And here is the code for the simulation again as a whole ```python account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account') loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit') simulation = a.Simulation(account, loan) simulation.add_regular('Income', account, 2000, interval = 'monthly') simulation.add_regular(account, loan, 1500, interval = 'monthly') simulation.simulate(delta = timedelta(days=365*10)) simulation.plt_summary() ``` ================================================ FILE: docs/02_using_callables_for_dynamic_changes.md ================================================ # Using callables for dynamic changes Sometimes, parameters like the amount of money to be transfered or the stop date cannot be statically defined at the beginning of your simulation. Instead, they depend on the current state of your simulation. Therefore, fl supports callables as arguments for some keywords, in order to cover more complex modeling scenarios. ## Dynamic payments Callables can be used to determine the amount of money to be transfered from one account to the other. This means, instead of writing ```python simulation.add_regular(from_acc=account, to_acc=loan, payment=1500, interval='monthly') ``` we can make sure, that we transfer only the amount of money to the loan account that is necessary: either 1500 or what is left. We can achieve this with a simple lambda-function. The loan account provide the property `loan.account`, which returns the debts we still need to pay. This is a negative number, therefore, we must put a minus-sign in front of it: ```python simulation.add_regular(from_acc=account, to_acc=loan, payment=lambda: min(1500, -loan.account), interval='monthly') ``` Let's go a step further and say we want to transfer at maximum 1500 but also make sure that we always have 2000 on our account and that we transfer only the money that is really need on the loan account. ```python simulation.add_regular(from_acc=account, to_acc=loan, payment=lambda: min( max( min(1500, account.account-2000), 0), -loan.account), interval='monthly') ``` The max-statement in this lambda-function is included to make sure that when `account.account - 2000` is a negative number, we won't initiate a negative transfer to the loan-account (which would be captured by the loan-account anyway). These callables are executed in each simulation cycle (which is every day within the simulation) and therefore decide depending on the simulation state how much money is transfered to the loan account. ================================================ FILE: docs/03_dependencies_between_accounts.md ================================================ # Dependencies between accounts Modeling loan accounts can be helpful to calculate payment durations and interests costs. But modeling them alone, does not reflect the accumulation of asset that one gains by decreasing the amount of debts. For example, if you start a loan in order to buy a house, you would like to take into account that the value of the house is more and more part of your asset the more you decrease the loan. This means, there is a dependency between the loan you pay and the value of the house that belongs to your asset. In fl, there is a class `Property` in order to model this relationship. And it can be used in the following way: ```python loan = a.Loan(200000, 0.0185, name = 'Credit' ) house = a.Property(200000, 0, loan, name='House') ``` You create a new object of type `Property`, in which you state the value of the property, the amount of own capital you put into it, and the loan account on which it depends. When during the simulation, the loan goes down, the property will go up. Dependency between loan and house Here is the [example code](../financial_life/examples/dependencies.py) The class `Property` is defined in the following way: ```python class Property(Account): """ This class can be used to reflect the amount of property that is gained from filling up a loan. This account does nothing else than adjusting the amount of property depending on the payments transfered to the loan class """ def __init__(self, property_value, amount, loan, date = None, name = None): ```
Keyword Argument Description
property_value The value of the property.
amount The amount of money that is already part of the own property.
loan A loan object to which a dependency is created.
date The date, when this account should exist. Before this date, the account will not be simulated. For quick simulation, where this nuance is not relevant, this field can be left empty. As date, datetime and strings in the format '%d.%m.%y', '%m/%d/%Y' are accepted.
name Simply a string representation of the property id. If empty, fl will create a random string sequence.
================================================ FILE: docs/README.md ================================================ # Documentation This documentations aims to introduce the user into the concepts of financial_life to start developing your own applications. ## User guide 1. [The first simulation](01_first_simulation.md) 2. [Using callables for dynamic changes](02_using_callables_for_dynamic_changes.md) 3. [Dependencies between accounts](03_dependencies_between_accounts.md) ## Developers guide 1. Short intro into the main routines 2. Adding your own account class ================================================ FILE: financial_life/README.md ================================================ # Enter the code Here, you are about to enter the code part of this repository. You might want to check out the [examples folder](examples/), where you will find another [README.md](examples/README.md). The other folders do not contain an MD file anymore. ================================================ FILE: financial_life/__init__.py ================================================ ''' Created on 03.12.2016 @author: martin ''' __version__ = '0.9.4' ================================================ FILE: financial_life/calendar_help/__init__.py ================================================ from calendar import monthrange from datetime import date from datetime import timedelta from datetime import datetime class Bank_Date(datetime): """ This is a helper class that adds some additional functionality to the datetime class, like adding months to the date or calculating the difference between two dates in months """ def is_end_of_month(self): """ returns true, if the current day is the end of month """ return monthrange(self.year, self.month)[1] == self.day def add_month(self, months): """ introduces calculation with months """ new_year = self.year + int((self.month + months - 1)/12) new_month = ((self.month + months - 1) % 12) + 1 new_day = min(self.day, monthrange(new_year, new_month)[1]) return Bank_Date(year = new_year, month = new_month, day = new_day) def diff_months(self, sub2): """ calculates the differences in months between two dates """ if not isinstance(sub2, datetime): raise NotImplementedError years = 0 months = 0 if (sub2.year > self.year): years = max(0, sub2.year - (self.year + 1)) months = (12 - self._month + 1) + sub2.month elif (sub2.year == self.year): months = sub2.month - self.month elif sub2.year < self.year: years = min(0, sub2.year + 1 - self.year) months = -(12 - sub2.month + 1) - self.month return years * 12 + months def get_days_per_year(year): # returns the number of days per year return 365 if monthrange(year, 2)[1] == 28 else 366 # deprecated, old methods for maniuplating datetime def add_month(start_date, months): """ introduces calculation with months """ new_year = start_date.year + int((start_date.month + months - 1)/12) new_month = ((start_date.month + months - 1) % 12) + 1 new_day = min(start_date.day, monthrange(new_year, new_month)[1]) new_date = date(new_year, new_month, new_day) return new_date def diff_months(sub1, sub2): """ calculates the differences in months between two dates """ years = 0 months = 0 if (sub2.year > sub1.year): years = max(0, sub2.year - (sub1.year + 1)) months = (12 - sub1.month + 1) + sub2.month elif (sub2.year == sub1.year): months = sub2.month - sub1.month elif sub2.year < sub1.year: years = min(0, sub2.year + 1 - sub1.year) months = -(12 - sub2.month + 1) - sub1.month return years * 12 + months ================================================ FILE: financial_life/constants/__init__.py ================================================ ================================================ FILE: financial_life/constants/intervals.py ================================================ ''' Created on 20.01.2017 This module lists all intervals, that can be used through out fl @author: martin ''' daily = 'daily' monthly = 'monthly' yearly = 'yearly' ================================================ FILE: financial_life/examples/README.md ================================================ # Examples In this folder, you find a list of examples that will help you getting started. ### simple_examples.py [simple_examples.py](simple_examples.py) shows three beginner examples that are also highlighted on the [front-page](../../README.md) of this repository. ### dependencies.py [dependencies.py](dependencies.py) showcases the account-class `Property`, which uses access to a loan class to determine, how much value of a given property has been transfered to the owner, when the loan is decreased. The key lines are: ```python loan = a.Loan(200000, 0.0185, name = 'Credit' ) # the class property defines a dependency on loan. When loan # decreases, the house-property increases house = a.Property(200000, 0, loan, name='House') ``` The value of `house` will becomes bigger when the value of `loan` decreases. See also more information about this construct in the [documentation](../../docs/03_dependencies_between_accounts.md). ### meta_data.py [meta_data.py](meta_data.py) demonstrates the usage of meta-data and controllers, in order to model tax returns. A full explanation of this example can be found [here](meta_data.md). ================================================ FILE: financial_life/examples/__init__.py ================================================ ================================================ FILE: financial_life/examples/dependencies.py ================================================ ''' Created on 14.08.2016 @author: martin ''' # standard libraries from datetime import timedelta, datetime import os # third-party libraries # own libraries from financial_life.financing import accounts as a from financial_life.reports import html from matplotlib.pyplot import show def dependencies(): loan = a.Loan(200000, 0.0185, name = 'Credit' ) # the class property defines a dependency on loan. When loan # decreases, the house-property increases house = a.Property(200000, 0, loan, name='House') simulation = a.Simulation(loan, house) simulation.add_regular('Income', loan, 1000, interval = 'monthly') simulation.simulate(delta = timedelta(days=365*20)) simulation.plt_summary() show(block=True) if __name__ == '__main__': dependencies() ================================================ FILE: financial_life/examples/meta_data.md ================================================ # Meta-Fields and Controller functions You can attach meta-data to any account class and any payment. These meta-data can be used to facilitate more complex calculations within your simulations, like the correct calculation of tax payments. With the help of controller-functions you can access these information to incorporate them into your simulation. Meta-data can be attached like this in your simulation definition: ```python # meta data for account classes, like loans loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit', date="01.09.2016", meta = {'tax': {'outcome': 'yearly_interests'}} ) # meta data for payments simulation.add_regular('Income', account, 2000, interval = 'monthly', date_start="01.09.2016", meta={'type': 'income', 'tax': { 'brutto': 2500, 'paid': 310, 'insurance': 190 } } ) ``` financial_life let's you add controller-functions to your simultions, which are executed every day before money is transfered between accounts. The controller-function is called with the simulation-object as argument: ```python def controller_tax(s): """ This is a controller function that calculates annual tax rates 's' is the simulation object """ # Do something, e.g. check, how much brutto income have been # transfered to the account and home much interests from the # loan account must be subtracted from it. See meta_data.py # for a full example # add controller function to simulation simulation.add_controller(controller_tax) ``` With `account.report.with_meta()` or `simulation.report.with_meta()` you can make these information visible, when you print the report. The result will look like this: ``` Main account Date description kind interest input foreign_account account output Meta ---------- ------------- --------------- ---------- ------- ----------------- --------- -------- --------------------------------------------------------------------------------------------------------------------------------------------------------------- 01.09.2016 0.00 0.00 1000.00 0.00 {} 01.09.2016 regular 0.00 2000.00 Income 3000.00 0.00 {'type': 'income', 'tax': {'insurance': 190, 'paid': 310, 'brutto': 2500}} 01.09.2016 regular 0.00 0.00 House Credit 1500.00 -1500.00 {} 01.10.2016 regular 0.00 2000.00 Income 3500.00 0.00 {'type': 'income', 'tax': {'insurance': 190, 'paid': 310, 'brutto': 2500}} 01.10.2016 regular 0.00 0.00 House Credit 2000.00 -1500.00 {} 01.11.2016 regular 0.00 2000.00 Income 4000.00 0.00 {'type': 'income', 'tax': {'insurance': 190, 'paid': 310, 'brutto': 2500}} 01.11.2016 regular 0.00 0.00 House Credit 2500.00 -1500.00 {} 01.12.2016 regular 0.00 2000.00 Income 4500.00 0.00 {'type': 'income', 'tax': {'insurance': 190, 'paid': 310, 'brutto': 2500}} 01.12.2016 regular 0.00 0.00 House Credit 3000.00 -1500.00 {} 31.12.2016 yearly interest 0.75 0.00 3000.75 0.00 {} 01.01.2017 regular 0.00 2000.00 Income 5000.75 0.00 {'type': 'income', 'tax': {'insurance': 190, 'paid': 310, 'brutto': 2500}} ``` See the full example [here](meta_data.py). ================================================ FILE: financial_life/examples/meta_data.py ================================================ ''' Created on 21.12.2016 @author: martin ''' # standard libraries from datetime import timedelta # third-party libraries # own libraries from financial_life.financing import accounts as a from financial_life.tax import germany as tax_ger def controller_tax(s): """ This is a controller function that calculates annual tax rates 's' is the simulation object """ # perform tax calculation always on 15th of February, just to # reflect the fact that tax payments for the previous year are # never made on the 1st of January, which has an impact on # interests as well if ((s.current_date.month == 2) and (s.current_date.day == 15)): # account class for payments account = s.accounts[0] # filter for all transactions that occured in the previous year # and of type 'income' income_report = s.report.subset(lambda st: (st.date.year == (s.current_date.year-1)) and (st.meta.get('type','') == 'income')) # using list comprehensions, we can easily calculate a few sums #m_income = sum(income.value) m_brutto = sum(payment.meta['tax']['brutto'] for payment in income_report) m_paid = sum(payment.meta['tax']['paid'] for payment in income_report) # get all accounts which have the field tax.outcome == 'yearly_interests' loans = [account for account in s.accounts if account.meta.get('tax', {}).get('outcome','') == 'yearly_interests'] # get only the reports of last year interests_reports = [loan.report.subset(lambda st: st.date.year == (s.current_date.year-1)) for loan in loans] # sum up all interests from all interests reports m_interests = sum(sum(report.interest) for report in interests_reports) # as interests for loans are negative, we effectively # subtract the payed interests from the brutto we earned in the last year m_tax_relevant_money = m_brutto + m_interests # now, we apply german tax rules from 2016 to the tax-relevant money m_tax, m_tax_percentage = tax_ger.tax_to_pay(2016, m_tax_relevant_money) # this is the money we either receive from the state (positive value # or we need to pay (negative value) m_diff = m_paid - m_tax s.add_unique('State', account, m_diff, date = s.current_date + timedelta(days=1), name = 'Tax', fixed = True, meta = { 'taxpayment': { 'tax_relevant_money': m_tax_relevant_money, 'tax_to_pay': m_tax, 'tax_percentage': m_tax_percentage, 'paid': m_paid, 'difference': m_diff } } ) def example_meta_controller(print_it = True): """ This example shows, how meta-information for payments and account data could be used to calculate annual tax-return """ account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account', date="01.09.2016") # define meta-data for accounts. here: some fields that are relevant for # tax calculations loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit', date="01.09.2016", meta = {'tax': { 'outcome': 'yearly_interests' } } ) # add these accounts to the simulation simulation = a.Simulation(account, loan, date='01.09.2016') # our employee receives monthly 2000 netto, coming from 2500 brutto, # 310 are subtracted directly from the loan, which is less than she # needs to pay. 190 are paid for insurance simulation.add_regular('Income', account, 2000, interval = 'monthly', date_start="01.09.2016", meta={'type': 'income', 'tax': { 'brutto': 2500, 'paid': 310, 'insurance': 190 } } ) simulation.add_regular(account, loan, lambda: min(1500, -loan.account), interval = 'monthly', date_start="01.09.2016") simulation.add_controller(controller_tax) # simulate for ten years simulation.simulate(delta = timedelta(days=365*10)) # this function is also part of a unittest. Therefore, we want to be able to # control, whether we print some information or not if print_it: # print reports summarized in years print(account.report.with_meta()) #print(loan.report.with_meta()) return simulation if __name__ == '__main__': example_meta_controller() ================================================ FILE: financial_life/examples/simple_examples.py ================================================ ''' Created on 14.08.2016 @author: martin ''' # standard libraries from datetime import timedelta, datetime import os # third-party libraries from matplotlib.pyplot import show # own libraries from financial_life.financing import accounts as a from financial_life.reports import html def example1(): # create a private bank account and a loan account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account') loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit') # add these accounts to the simulation simulation = a.Simulation(account, loan) # describe single or regular payments between accounts. note, that # a string can be used for external accounts that you don't want to model. # also note the lambda function for the payments to the loan. simulation.add_regular('Income', account, 2000, interval = 'monthly') # you can also use lambda function to dynamically decide how much money # you would like to transfer simulation.add_regular(account, loan, lambda: min(1500, -loan.account), interval = 'monthly') # simulate for ten years simulation.simulate(delta = timedelta(days=365*10)) # plot the data simulation.plt_summary() show() # print reports summarized in years print(account.report.yearly().as_df()) print(loan.report.yearly().as_df()) # analyze data print("Interests on bank account: %.2f" % sum(account.report.yearly().interest)) print("Interests on loan account: %.2f" % sum(loan.report.yearly().interest)) return simulation def example2(): # create a private bank account and a loan account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account') savings = a.Bank_Account(amount = 5000, interest = 0.007, name = 'Savings') loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit') # add these accounts to the simulation simulation = a.Simulation(account, savings, loan) # describe single or regular payments between accounts. note, that # a string can be used for external accounts that you don't want to model. # also note the lambda function for the payments to the loan. simulation.add_regular('Income', account, 2000, interval = 'monthly') simulation.add_regular(account, savings, 500, interval = 'monthly') simulation.add_regular(account, loan, lambda: min(1500, -loan.account), interval = 'monthly') simulation.add_unique(savings, 'Vendor for car', 10000, '17.03.2019') # simulate for ten years simulation.simulate(delta = timedelta(days=365*10)) # plot the data simulation.plt_summary() # print reports summarized in years print(account.report.yearly().as_df()) print(loan.report.yearly().as_df()) # analyze data print("Bank account: %.2f" % (account.account + savings.account)) cwd = os.path.dirname(os.path.realpath(__file__)) result_folder = cwd + '/example2' html.report(simulation, style="standard", output_dir = result_folder) show() def example3(): account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account') savings = a.Bank_Account(amount = 5000, interest = 0.013, name = 'Savings') loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit') simulation = a.Simulation(account, savings, loan, name = 'Testsimulation') simulation.add_regular(from_acc = 'Income', to_acc = account, payment = 2000, interval = 'monthly', date_start = datetime(2016,9,15), day = 15, name = 'Income') simulation.add_regular(from_acc = account, to_acc = savings, payment = 500, interval = 'monthly', date_start = datetime(2016,9,30), day = 30, name = 'Savings') simulation.add_regular(from_acc = account, to_acc= loan, payment = 1000, interval = 'monthly', date_start = datetime(2016,9,15), day = 15, name = 'Debts', fixed = False, date_stop = lambda cdate: loan.is_finished()) simulation.add_regular(from_acc = account, to_acc= loan, payment = lambda : min(8000, max(0,account.get_account()-4000)), interval = 'yearly', date_start = datetime(2016,11,20), day = 20, name = 'Debts', fixed = False, date_stop = lambda cdate: loan.is_finished()) simulation.simulate(delta=timedelta(days=2000)) simulation.plt_summary() show() print(account.report) print(loan.report) if __name__ == '__main__': example1() ================================================ FILE: financial_life/financing/__init__.py ================================================ """ some basic classes for creating financing classes and reports """ # standard libraries from datetime import datetime from calendar import monthrange import warnings from copy import deepcopy from collections import defaultdict from collections import Callable # third-party libraries from tabulate import tabulate import numpy as np from numpy.core.numeric import result_type import pandas as pd # own libraries from financial_life.calendar_help import Bank_Date from financial_life.financing.identity import id_generator from financial_life.financing import validate pd.set_option('display.width', 1000) # degrees of precision. the higher the number the more # precise is the category C_precisions = {'year' : 1., 'month': 2., 'day': 3., } C_default_payment = {'date': Bank_Date.max, 'payment': 0., 'name': 'End reached'} # semantic of reports. Columns of the report can be assigned # to one or several of this categories. This allows to # visualize collection of reports in a semantical meaningful manner report_semantics = {'input_abs': [], # money transfered to the financial product as absolute number 'input_cum': [], # ...as increment 'output_abs': [], # money transfered from the financial product 'output_cum': [], # ...as decrement 'cost_abs': [], # created costs by the f.p. 'cost_cum': [], # ...as increment 'win_abs': [], # created win by the f.p. 'win_cum': [], # ...as increment 'debt_abs': [], # debt value 'debt_cum': [], # ...as increment 'debtpayment_abs': [], # payment to equalize debts 'debtpayment_cum': [], # ...as increment 'saving_abs': [], # saving value 'saving_cum': [], # ...as increment 'none': [], # none of the above things } def conv_payment_func(x): """ converts any payment to what is needed in order to be applicable in the simulation process. if it is a number, it is multiplied by 100, if it is a function, the function is called and the result is multiplied by 100. """ return Payment_Value(x) def create_stop_criteria(date_stop): """ This is a function that returns a functions, which defines a stop criteria for the iterators. If date_stop is a date, the resulting functions simply compares date_stop with a given date. if date_stop is a callable, it executes the callable """ if isinstance(date_stop, datetime) or isinstance(date_stop, Bank_Date): def compare_dates(cdate): return cdate < date_stop return compare_dates elif isinstance(date_stop, Callable): def check_callable(cdate): return not date_stop(cdate) return check_callable else: raise ValueError("date_stop is %s but should be either date-type or Callable" % type(date_stop)) def iter_regular_month(regular, date_start = None): """ creates an iterator for a regular payment. this function is for example used by payment to create iterators for every item in _regular regular: item of the structure Payments._regular date_start: date the payment generator wants to start the payments, this can be a date after regular['date_start'] """ if not date_start: date_start = regular['date_start'] else: # determine the greater date date_start = max(date_start, regular['date_start']) # if day is bigger than start_date.day, than this month is gonna # be the first payment if date_start.day <= regular['day']: i = 0 # otherwise it will be in the next month else: i = 1 date_start = Bank_Date(year = date_start.year, month = date_start.month, day = min(regular['day'], monthrange(date_start.year, date_start.month)[1]), hour = date_start.hour, minute = date_start.minute, second = date_start.second) date_stop = regular.get('date_stop', Bank_Date.max) stop_criteria = create_stop_criteria(date_stop) current_date = date_start.add_month(i) while stop_criteria(current_date): yield Payment(from_acc = regular['from_acc'], to_acc = regular['to_acc'], date = current_date, name = regular['name'], kind = 'regular', payment = regular['payment'], fixed = regular['fixed'], meta = regular['meta'] ) i += 1 current_date = date_start.add_month(i) def iter_regular_year(regular, date_start = None): """ creates an iterator for a yearly payment. this function is used by payment to create iterators for every item in _regular It takes the day and month in regular['date_start'] to schedule the payment regular: item of the structure Payments._regular date_start: date the payment generator wants to start the payments, this can be a date after regular['date_start'] """ if not date_start: date_start = regular['date_start'] else: # determine the greater date date_start = max(date_start, regular['date_start']) current_date = datetime(year=date_start.year, month=regular['date_start'].month, day=regular['date_start'].day) if current_date < date_start: current_date = datetime(year=date_start.year + 1, month=regular['date_start'].month, day=regular['date_start'].day) date_stop = regular.get('date_stop', Bank_Date.max) stop_criteria = create_stop_criteria(date_stop) while stop_criteria(current_date): yield Payment(from_acc = regular['from_acc'], to_acc = regular['to_acc'], date = current_date, name = regular['name'], kind = 'regular', payment = regular['payment'], fixed = regular['fixed'], meta = regular['meta'] ) current_date = datetime(year = current_date.year + 1, month=regular['date_start'].month, day=regular['date_start'].day) # functions for generating regular payments C_interval = { 'monthly': iter_regular_month, 'yearly': iter_regular_year, } class Status(object): """ This class represents the status of a financing product at a particular date """ def __init__(self, date, **kwargs): """ Creates a new status object. Note, that all of kwargs elements are written to _status, except 'meta', which is treated special, as it may contain dict-data again """ if not isinstance(date, datetime): raise TypeError("date must be from type datetime") self._date = date self._meta = {} if 'meta' in kwargs: self._meta = kwargs.pop('meta') self._status = kwargs self._format = "%d.%m.%Y" def __str__(self): result = "Date: %s" % self._date.strftime(self._format) + '\n' for key, value in self._status.items(): result += ("%s: %s\n" % (key, str(value))) return result def keys(self): """ Returns a list of keys """ return self._status.keys() @property def date(self): return self._date @property def strdate(self): return self._date.strftime(self._format) @property def status(self): return self._status @property def meta(self): return self._meta def __getitem__(self, key): if key == 'date': return self._date return self._status[key] def __getattr__(self, name): return self.__getitem__(name) def get(self, attr, default): """ Get attribute or default value from data-dictionary """ if attr == 'date': return self._date return self._status.get(attr, default) class Report(object): """ A report is a collection of statuses with some additional functionallity in order to merge and plot reports. One key feature of report is that it can handle heterogenous types of statuses """ def __init__(self, name=None, format_date = "%d.%m.%Y", precision = 'daily' ): self._statuses = [] self._keys = [] # list of all keys used so far self._format_date = format_date # precision for merging statuses with similar date self._precision = precision self._semantics = deepcopy(report_semantics) if not name: name = id_generator(8) self._name = name def add_semantics(self, key, semantics=None): """ adds semantic description to the report, important for plotting usage: Single assignments .add_semantics('loan', 'debt_abs') Group assignments .add_semantics(['interest', 'insurence'], 'cost_cum') Entire assignments .add_semantics({'cost_cum':['interest', 'insurence']}) """ if isinstance(key, dict): for k, items in key.items(): if k in self._semantics: self._semantics[k] = items else: raise AttributeError('Key %s not in semantics' % k) return if semantics not in self._semantics: raise AttributeError('Semantic "%s" not in semantics' % semantics) if isinstance(key, list): self._semantics[semantics] = self._semantics[semantics] + key self._keys = list(set(self._keys) | set(key)) return if isinstance(key, str): self._semantics[semantics].append(key) self._keys = list(set(self._keys) | set((key,))) return def semantics(self, semantic): """ returns list of elements in semantic """ return self._semantics[semantic] def semantics_of(self, key): """ returns the semantic in which the key appears """ for semantic, values in self._semantics.items(): if key in values: return semantic return '' def append(self, status = None, date = None, **kwargs): """ adds either an instance of status to the list or data given to the append method as keyword arguments """ assert((status and not date) or (date and not status)) if date: status = Status( Bank_Date.fromtimestamp(date.timestamp()), **kwargs ) if not isinstance(status, Status): raise TypeError("status must be of type Status") self._statuses.append(status) # add potential new keys to the list self._keys = list(set(self._keys) | set(status.keys())) @property def size(self): """ Returns the number of status entries""" return len(self._statuses) @property def name(self): return self._name @name.setter def name(self, name): self._name = name @property def precision(self): return self._precision def get_from_date(self, date, interval): """ help function to make the creation monthly, yearly reports more generic. This function returns e.g. month or year from a given date """ if interval == 'yearly': return Bank_Date(date.year, 1, 1) if interval == 'monthly': return Bank_Date(date.year, date.month, 1) if interval == 'daily': return date raise TypeError("interval has to be either monthly or yearly") def monthly(self): return self.create_report(interval='monthly') def yearly(self): return self.create_report(interval='yearly') def create_report(self, interval='yearly'): """ generic function for returning a report for certain intervals """ def add_data(data, status): """ add status data to existing dictionary """ for key, value in status.status.items(): # for cumulative data, we need to add, for other we just # need to take the latest value if "cum" in self.semantics_of(key): data[key] += value elif not self.semantics_of(key) is "none": data[key] = value return data if interval == 'daily': return self i = 0 result = Report(name = self._name, format_date = self._format_date, precision = interval ) result._semantics = deepcopy(self._semantics) while (i < len(self._statuses)): # get the value of the interval type, e.g. exact month or exact year frame = self.get_from_date(self._statuses[i].date, interval) # create a new dictionary and add the current status to it data = add_data(defaultdict(int), self._statuses[i]) i += 1 # iterate to the following statuses as long as frame equals the current value of the # the given interval type (e.g. as long as the month is the same) while (i < len(self._statuses)) and (frame == self.get_from_date(self._statuses[i].date, interval)): data = add_data(data, self._statuses[i]) i += 1 # if the while loop ended because of i, we need to correct it again if (i == len(self._statuses)): result.append(date=self._statuses[i-1].date, **data) else: result.append(date=self._statuses[i-1].date, **data) return result def subset(self, lambda_func): """ creates a subset of report based on lambda-function that is used within a list comprehension. The lambda function gets every status element of report and returns either true (to be inlcuded in subset) or false (excluded). This is a very generic way of applying queries to the report in order to reduce its the report to its requsted items. Note, that this is not a deepcopy of the report. Therefore, the it is more appropriate to use it for reading data rather than writing data. """ if not isinstance(lambda_func, Callable): raise TypeError('lambda_func must be of the form lambda status: True False') result = Report(name = self._name, format_date = self._format_date, precision = 'custom' ) result._semantics = self._semantics result._keys = self._keys result._statuses = [s for s in self._statuses if lambda_func(s)] return result def table_rows(self): """ Creates a list of lists, where each inner list represents a row of a table. This is used by the tabulate package for plotting tables. """ records = [] for s in self._statuses: data = [s.date.strftime(self._format_date)] + [s.get(key, '') for key in self._keys] records.append(data) return records def with_meta(self): """ Returns the table with meta-information """ print(self.name) records = [] for s in self._statuses: data = [s.date.strftime(self._format_date)] + [s.get(key, '') for key in self._keys] + [str(s._meta)] records.append(data) return tabulate(records, headers=(['Date'] + self._keys + ['Meta']), floatfmt=".2f") def sum_of(self, semantic): """ Returns the sum of a given semantic, e.g. .sum_of('cost') for all cost_cum and cost_abs items, or .sum_of('cost_cum') for cost_cum items only """ result = 0 for sem in self._semantics: if semantic in sem: # if this is a cumulative list, we need to calculate the sum if '_cum' in sem: for key in self._semantics[sem]: result += np.sum(self.get(key, num_only = True)) # if abs, get only the last element if '_abs' in sem: for key in self._semantics[sem]: result += self.get(key, num_only = True)[-1] return result def __getitem__(self, key): result = Report(format_date = self._format_date, precision = self._precision) for s in self._statuses: if key in s.keys(): result.append(date = s['date'], **{key: s[key]}) return result def __getattr__(self, name): result = [s.get(name, 'None') for s in self._statuses] return result if (name == 'date'): return result else: return np.array(result) def __len__(self): return len(self._statuses) def get(self, name, num_only = False): replace = 0 if num_only else 'None' result = [s.get(name, replace) for s in self._statuses] return result if (name == 'date'): return result else: return np.array(result) def __str__(self): """ Prints all statuses in table view """ print(self.name) records = self.table_rows() return tabulate(records, headers=(['Date'] + self._keys), floatfmt=".2f") def __iter__(self): """ Iteratores through all statuses """ return self._statuses.__iter__() def as_df(self): """ Returns the report as pandas.DataFrame """ dates, data = list(zip(*((s.date, s.status) for s in self._statuses))) return pd.DataFrame(list(data), index=dates) class Payment_Value(object): """ This is a class that represents a payment value. If the payment is an integer or float it is returned right away, if it is a function it is evaluated during runtime. Furthermore, this class can return a clear statement, whether it is a payment or not to any reporting instance (e.g. html reports) """ def __init__(self, payment): """ checks, whether x is a number or a function and prepares the reporting variable """ self._name = "could not be determined" self._payment = payment if isinstance(payment, int) or isinstance(payment, float): self._name = str('%0.2f' % payment) if isinstance(payment, Callable): self._name = "dynamic" @property def name(self): return self._name def __call__(self): if isinstance(self._payment, int) or isinstance(self._payment, float): return int(self._payment * 100) if isinstance(self._payment, Callable): return int(self._payment() * 100) class Payment(object): """ Class that describes one specific payment between two accounts accounts can here be of type "Account" or str, which is an abstract account that always complies """ def __init__(self, from_acc, to_acc, date, name, kind, payment, fixed=True, meta={}): """ Initialization of Payment from_acc: sending account to_acc: receiving account date: due date name: Name of the payment payment: Amount of the payment (money value) fixed: Whether the receiving side needs to take the entire money or can accept less (useful for loans) """ self._data = { 'from_acc': from_acc, 'to_acc': to_acc, 'date': date, 'name': name, 'kind': kind, 'payment': payment, 'fixed': fixed, 'meta': meta } @property def from_acc(self): return self._data['from_acc'] @property def to_acc(self): return self._data['to_acc'] @property def date(self): return self._data['date'] @property def name(self): return self._data['name'] @property def kind(self): return self._data['kind'] @property def payment(self): return self._data['payment'] @property def json(self): return {'from_acc': self._data['from_acc'].name, 'to_acc': self._data['to_acc'].name, 'date': self._data['date'].date(), 'name': self._data['name'], 'kind': self._data['kind'], 'payment': self._data['payment'].name, 'fixed': self._data['fixed'], 'meta': self._data['meta'], } def __getitem__(self, key): return self._data[key] class PaymentList(object): """ Hanldes the complexities of payments including unique payments and regular payments """ def __init__(self): self._uniques = [] self._regular = [] @property def uniques(self): return self._uniques @property def regular(self): return self._regular def check_errors_payment(self, payment): """ checks for any errors in the payment variable """ if (not isinstance(payment, int) and not isinstance(payment, float) and not isinstance(payment, Callable)): raise TypeError("Payment must be int, float or a function") def add_unique(self, from_acc, to_acc, payment, date, name = '', fixed = True, meta={}): """ adds a one-time payment to the list, optional give it a name """ if not isinstance(date, datetime): raise TypeError("Date must be at least from type datetime") self.check_errors_payment(payment) # converts any input to a function that returns the right value conv_payment = conv_payment_func(payment) self._uniques.append( Payment( from_acc = from_acc, to_acc = to_acc, date = Bank_Date.fromtimestamp(date.timestamp()), name = name, kind = 'unique', payment = conv_payment, fixed = fixed, meta = meta ) ) # sort the whole list with date as key self._uniques = sorted(self._uniques, key = lambda p: p['date'] ) def add_regular(self, from_acc, to_acc, payment, interval, date_start, day=1, name='', date_stop = None, fixed = False, meta={}): """ Adds a regular payment to the list, with a given payment: amount to pay interval: 'monthly': every month 'quarter': every quarter with date_start as start month 'quarter_year': every quarter of a year (mar, jun, sep, dec) 'yearly': every year day: day to start with date_start: start date of this payment name : optional name fixed: only everything or nothing must be transfered (true) or depending on the receiving account a smaller amount can be transfered (false) """ if not interval in C_interval.keys(): raise ValueError("interval must be one of '" + '\',\''.join(C_interval)) if day >= 29: warnings.warn(("note that in months which have less days than {} the " + "payment will be transferred earlier").format(day) ) self.check_errors_payment(payment) if not date_stop: date_stop = Bank_Date.max else: date_stop = validate.valid_stop_date(date_stop) # converts any payment to a function conv_payment = conv_payment_func(payment) self._regular.append({'from_acc': from_acc, 'to_acc': to_acc, 'interval': interval, 'day' : day, 'date_start': Bank_Date.fromtimestamp(date_start.timestamp()), 'date_stop': date_stop, 'payment': conv_payment, 'name' : name, 'fixed': fixed, 'meta': meta } ) def clear_regular(self): """ Removes all regular payments """ self._regular = [] def payment(self, start_date): """ returns an interator that iterates through all payments """ assert isinstance(start_date, datetime), "start_date must be of type datetime" # creates for each item an iterator that returns just this # item. this list is later on amended by iterators for regular # payments iters = [iter([u]) for u in self._uniques if u['date']>= start_date] for r in self._regular: # creates an iterator based on the interval in r iters.append(C_interval[r['interval']](r, start_date)) # list of next dates. this list is inline with the iters list # the second parameter in next prevents the command to raise a # StopIteration Exception dates = [next(iter, C_default_payment) for iter in iters] # as long as there is still a date, below infinity min_date = min(dates, key = lambda d: d['date']) # in this routine, the next command must be called after yield, as there might # be some callables which need to be called right after the payments, but not # before while min_date['date'] != Bank_Date.max: # find all indices and payments in the list that have the same daet indices, payments = zip(*[(i, d) for (i, d) in enumerate(dates) if (d['date'].date() == min_date['date'].date())]) yield payments for i in indices: dates[i] = next(iters[i], C_default_payment) min_date = min(dates, key = lambda d: d['date']) class Currency(): """ Standard class for currencies to assure correct computing of numbers. Right now this class is not in use """ def __init__(self, value, digits = 2): self._value = int(value * (10**digits)) ================================================ FILE: financial_life/financing/accounts.py ================================================ ''' Created on 24.03.2016 @author: martin ''' # standard libraries from datetime import datetime, timedelta from collections import Callable import warnings import logging # third-party libraries # own libraries from financial_life.financing import PaymentList from financial_life.financing import Report from financial_life.financing import C_default_payment from financial_life.calendar_help import Bank_Date, get_days_per_year from financial_life.financing import plotting as plt from financial_life.financing import validate logger = logging.getLogger(__name__) # maximal time span that will be simulated C_max_time = 365 * 100 # format for dates C_format_date = '%d.%m.%Y' # generic transfer codes C_transfer_OK = 0 # transfer confirmed C_transfer_NA = 1 # transfer not allowed C_transfer_NEM = 2 # not enough money on the account C_transfer_ERR = 3 # general transfer error C_transfer_codes = {C_transfer_OK: 'OK', C_transfer_NA: 'Not allowed', C_transfer_NEM: 'Not enough money', C_transfer_ERR: 'ERROR'} def neg_func(func): """ negates the outcome of func. this function is used as a wrapper to negate the output of payments which are determined at runtime. This wrapper is used e.g. by the class Transfers """ def foo(): result = func() return -result return foo def valid_account_type(*accounts): """ Checks whether all accounts given to this function are either from type Account or from type string Accounts of type string are converted to DummyAccount The corrected list is returned The only reason this method is not in the validate module is because it would create an import loop """ result = [] for account in accounts: if (isinstance(account, Account)): result.append(account) elif (isinstance(account, str)): result.append(DummyAccount(account)) else: raise TypeError('the given account must be either derived from type Account or of type string') return tuple(result) class TransferMessage(object): """ Message returned by a transfer function with some information about the success or failure of the money transfer """ def __init__(self, code, money, message = ''): if code in C_transfer_codes: self._code = code else: raise ValueError("Transfercode is not in C_transfer_codes") self._message = message self._money = money @property def code(self): return self._code @property def message(self): return self._message @property def money(self): return self._money class Simulation(object): """ This class simulates the interaction between different accounts. It provides the framework in which dependencies between accounts and state- dependent changes of account-modi can be managed """ def __init__(self, *accounts, name = None, date = None, meta = None): """ Simulations can be initialized with names, to make differentiate between different simulations """ # check for errors in the input of accounts for account in accounts: if not isinstance(account, Account): raise TypeError(str(account) + " is not of type or subtype Account") if name is None: self._name = 'Simulation ' + str(datetime.now()) else: self._name = name # a simuation can also store meta information self._meta = meta self._report = Report(self._name) self._report.add_semantics('from_acc', 'none') self._report.add_semantics('to_acc', 'none') self._report.add_semantics('value', 'input_cum') self._report.add_semantics('kind', 'none') self._report.add_semantics('name', 'none') self._report.add_semantics('code', 'none') self._report.add_semantics('message', 'none') self._payments = PaymentList() self._payments_iter = None self._next_pay = None self._date_start = validate.valid_date(date) self._day = 0 self._current_date = self._date_start # list of accounts to manage self._accounts = list(accounts) # list of controller-functions executed before day-simulation. # controller functions are executed before the day to check custom # states of the accounts and perform actions self._controller = [] @property def name(self): return self._name @name.setter def name(self, name): self._name = name @property def meta(self): return self._meta @property def accounts(self): return self._accounts @property def current_date(self): return self._current_date @property def report(self): return self._report def as_df(self): df = self.report.as_df() df = df[['from_acc', 'to_acc', 'value', 'kind', 'name', ]] return df def get_report_jinja(self, interval="yearly"): """ creates a data-structure of the report data that can be used for displaying the report as table in html files (in jinja2 templates) interval can be one of the common intervals of the report class (e.g. yearly or monthly) or None. In this case the raw data are exported """ if interval is None: report = self._report else: report = self._report.create_report(interval) header = ['date', 'from', 'to', 'value', 'kind', 'name', 'code', 'message'] rows = [] for status in report._statuses: item = [status.strdate, status._data['from_acc'].name, status._data['to_acc'].name, '%.02f EUR' % status._data['value'], status._data['kind'], status._data['name'], status._data['code'], status._data['message'], ] rows.append(item) return {'header': header, 'rows': rows} def get_payments_unique_json(self): """ returns a list of all unique payments in json format for html rendering """ return {'payments_unique': [u.json for u in self._payments.uniques]} def get_payments_regular_json(self): """ returns a list of all unique payments in json format for html rendering """ return {'payments_regular': [ { 'from_acc': r['from_acc'].name, 'to_acc': r['to_acc'].name, 'interval': r['interval'], 'day': r['day'], 'date_start': r['date_start'].date(), 'date_stop': r['date_stop'].date() if isinstance(r['date_stop'], datetime) else '', 'payment': r['payment'].name, 'name': r['name'], 'fixed': r['fixed'], } for r in self._payments.regular] } def get_accounts_json(self): return {'accounts': [ { 'index': i, 'name': a.name, 'type': a.__class__.__name__, 'start_value': a._account / 100., 'start_date': a.date_start.date() } for i, a in enumerate(self.accounts)] } def add_unique(self, from_acc, to_acc, payment, date, name = '', fixed = False, meta = {} ): """ Transfers money from one account to the other """ from_acc, to_acc = valid_account_type(from_acc, to_acc) date = validate.valid_date(date) self._payments.add_unique( from_acc, to_acc, payment, date, name, fixed, meta) self.update_payment_iterators() def add_regular(self, from_acc, to_acc, payment, interval, date_start=datetime(1971,1,1), day=1, name = '', date_stop = None, fixed = False, meta = {} ): """ Transfers money from one account to the other on regular basis date_stop can be a function of the form lambda x: x > datetime(...) If it returns true, the payment is stopped """ from_acc, to_acc = valid_account_type(from_acc, to_acc) date_start = validate.valid_date(date_start) if date_stop is not None: date_stop = validate.valid_stop_date(date_stop) self._payments.add_regular( from_acc, to_acc, payment, interval, date_start, day, name, date_stop, fixed, meta) self.update_payment_iterators() def update_payment_iterators(self): """ Whenever a new payment is added via add_unique or add_regular, this function is triggered to update the payment iterator. This is necessary, as payments could be dynamically added during the simulation as well """ self._payments_iter = self._payments.payment(self._current_date) try: self._next_pay = next(self._payments_iter, C_default_payment) except StopIteration: # if there are no payments, create a date for a payment # that lies in the distant future self._next_pay = [{'date': Bank_Date.max}] def add_account(self, account): """ adds an account to the simulation and returns it to the user so that he/she can proceed with it """ if isinstance(account, Account): self._accounts.append(account) else: raise TypeError(("account must be of type Account but is of type " + str(type(account)))) return account def add_controller(self, controller): if isinstance(controller, Callable): self._controller.append(controller) else: raise TypeError(("controller must be of type Callable but is of type " + str(type(controller)))) def get_payment(self, payment): """ functions that returns the amount of payment for the current day. it handles the distinction between variables that represent just numbers and variables that represent functions to be executed """ payed = 0 if isinstance(payment['payment'], int) or isinstance(payment['payment'], float): payed = payment['payment'] elif isinstance(payment['payment'], Callable): payed = payment['payment']() else: raise TypeError("payment must be int, float or Callable but is " + str(type(payment['payment']))) return payed def make_report(self, from_acc, to_acc, value, kind, name, code, message, meta): self._report.append( date = self._current_date, from_acc = from_acc, to_acc = to_acc, value = value / 100, kind = kind, name = name, code = code, message = message, meta = meta ) def make_transfer(self, payment): """ Transfers money from one account to the other and tries to assure full consistency. The idea is that a payments gets started by the sender. If this succeeds, the money is tried to move on the account of the receiver. If this fails, the money is transfered back to the sender. If the money to be transfered is zero, no payment procedure will be initiated """ if not (isinstance(payment['from_acc'], DummyAccount)): assert payment['from_acc']._date_start <= self._current_date, (str(payment['from_acc']) + ' has a later creation date than the payment ' + payment['name']) if not (isinstance(payment['to_acc'], DummyAccount)): assert payment['to_acc']._date_start <= self._current_date, (str(payment['to_acc']) + ' has a later creation date than the payment ' + payment['name']) try: # this is now the money that will be transfered, if there is # a receiver. this amount of money remains fixed for the transfer money = self.get_payment(payment) if money == 0: self.make_report( from_acc = payment['from_acc'], to_acc = payment['to_acc'], value = 0, kind = payment['kind'], name = payment['name'], code = C_transfer_NA, message = "Transfer with zero money will not be initiated", meta = payment['meta'] ) return False except TypeError as e: logger.debug("make_transfer: money of wrong type") self.make_report( from_acc = payment['from_acc'], to_acc = payment['to_acc'], value = 0, kind = payment['kind'], name = payment['name'], code = C_transfer_ERR, message = e.message(), meta = payment['meta'] ) return False # first, try to get the money from the sender account, tm = TransferMessage() tm_sender = payment['from_acc'].payment_output( account_str = payment['to_acc'].name, payment = -money, kind = payment['kind'], description = payment['name'], meta = payment['meta'] ) # if sending money succeeded, try the receiver side if tm_sender.code == C_transfer_OK: logger.debug("make_transfer: sender code is OK") # in the wired case that money is less than what has been returned by the sender, # throw an error message if money < (-tm_sender.money): raise ValueError("%f was requested from account '%s' but %f returned" % (money, payment['from_acc'].name, -tm_sender.money)) if money > (-tm_sender.money): # if payment is fixed, throw an error, otherwise proceed if payment['fixed']: raise ValueError("%f was requested from account '%s' but %f returned" % (money, payment['from_acc'].name, -tm_sender.money)) else: money = -tm_sender.money tm_receiver = payment['to_acc'].payment_input( account_str = payment['from_acc'].name, payment = money, kind = payment['kind'], description = payment['name'], meta = payment['meta'] ) # if receiving succeeded, return success if tm_receiver.code == C_transfer_OK: # in the wired case that money is less than what has been returned by the sender, # throw an error message if money < tm_receiver.money: raise ValueError("%f was submitted to account '%s' but %f returned" % (money, payment['to_acc'].name, tm_receiver.money)) # if the receiver does not accept the entir money if money > tm_receiver.money: # check, whether payment is fixed if payment['fixed']: raise ValueError("%f was submitted to account '%s' but %f returned because it is fixed" % (money, payment['to_acc'].name, tm_receiver.money)) else: # if payment is not fixed, we need to transfer the difference back to # the sender account payment['from_acc'].return_money( money - tm_receiver.money) logger.debug("make_transfer: receiver code is OK") self.make_report( from_acc = payment['from_acc'], to_acc = payment['to_acc'], value = tm_receiver.money, kind = payment['kind'], name = payment['name'], code = C_transfer_OK, message = '', meta = payment['meta'] ) return True else: # if an error on the receiver side happened, # return the money back and report that logger.debug("make_transfer: receiver code is not ok") payment['from_acc'].return_money(money) self.make_report( from_acc = payment['from_acc'], to_acc = payment['to_acc'], value = tm_sender.money, kind = payment['kind'], name = payment['name'], code = tm_receiver.code, message = tm_receiver.message, meta = payment['meta'] ) return False else: # if an error occured on the sending side, report this and return false logger.debug("make_transfer: sending code is not OK") self.make_report( from_acc = payment['from_acc'], to_acc = payment['to_acc'], value = money, kind = payment['kind'], name = payment['name'], code = tm_sender.code, message = tm_sender.message, meta = payment['meta'] ) return False def simulate(self, date_stop = None, delta = None, last_report = True): """ Simulation routine for the entire simulation """ # Initialization date_stop = validate.valid_date_stop(date_stop) if (not self._payments_iter): self._payments_iter = self._payments.payment(self._current_date) if (not self._next_pay): try: self._next_pay = next(self._payments_iter, C_default_payment) except StopIteration: # if there are no payments, create a date for a payment # that lies in the distant future self._next_pay = [{'date': Bank_Date.max}] delta = validate.valid_delta(delta) temp_delta = 0 while ((self._current_date < date_stop) and # ...stop-date is reached (temp_delta < delta.days) and # and delta has not been exeeded ((self._current_date - self._date_start).days < C_max_time)): # ...number of simulated days exceeds max # 0. set the current day for account in self._accounts: if account._date_start <= self._current_date: account.set_date(self._current_date) # 1. execute start-of-day function # everything that should happen before the money transfer for account in self._accounts: if account._date_start <= self._current_date: account.start_of_day() # 2. execute all controller functions for controller in self._controller: controller(self) # 3. apply all payments for the day in correct temporal order if self._next_pay[0]['date'].date() == self._current_date.date(): for payment in self._next_pay: self.make_transfer(payment) self._next_pay = next(self._payments_iter, C_default_payment) # 4. execute end-of-day function # everything that should happen after the money transfer for account in self._accounts: if account._date_start <= self._current_date: account.end_of_day() # go to the next day within the simulation self._day += 1 self._current_date = self._date_start + timedelta(days = self._day) temp_delta += 1 def reports(self, interval='yearly'): """ Returns a tuple of reports for a given interval """ return (account.report.create_report(interval) for account in self._accounts) def plt_summary(self, interval='yearly'): """ plots a summary of the simulation """ reports = self.reports(interval=interval) plt.summary(*reports) def report_sum_of(self, semantic): """ creates the sum for every report.sum_of(semantic) of each account """ return sum([a.report.sum_of(semantic) for a in self._accounts]) def print_reports(self, interval): """ Creates for every account a report for a given interval """ for a in self._accounts: print(a.name) print(a.report.create_report(interval)) print(' ') class Account(object): """ Basic class for all types of accounts with reporting and simulation functionality obligatory methods for each account to be part of a simulation - set_date - start_of_day - end_of_day - payment_output - payment_input - return_money """ def __init__(self, amount, interest, date=None, name = None, meta = {}): self._date_start = validate.valid_date(date) self._name = validate.valid_name(name) self._meta = meta # check for problems assert((isinstance(amount, int) or (isinstance(amount, float)))) if interest > 1.: interest = interest / 100. ## generic variables, which are basically used in any class ## ## that inherits from account ## # setting up the report and the semantics self._report = Report(name = self._name) self._account = int(amount * 100) # amount of money to start with self._interest = interest # interest rate self._current_date = self._date_start # current date of the simulation self._caccount = self._account # current account, this variable # is used in all subclasses # sum helper variables for interest calculation and keep self._sum_interest = 0 def __str__(self): return self._name @property def date(self): return self._date @property def date_start(self): return self._date_start @property def name(self): return self._name @name.setter def name(self, name): self._name = name self._report.name = self._name + ' - ' + str(self._date_start.strftime(C_format_date)) @property def meta(self): return self._meta @property def account(self): return self._caccount / 100 def get_account(self): """ alternative method to get the current account value. this method can be used, e.g. in payment-definitions to transfer the amount of money that a specific account has in the moment this payment is done. Instead of using an actual value, this method is called, evaluated and the return value is used """ return self.account @property def interest(self): return self._interest / 100 @property def payments(self): return self._payments @property def current_date(self): return self._current_date @property def report(self): return self._report def as_df(self): return self.report.as_df() def report_time(self, date): """ returns true, if the requirements for a report are met """ return True def get_table_json(self, report): """ Creates a table for a given report """ return {'header': [], 'rows': []} def get_all_tables_json(self): """ Creates tables for all intervals in report """ # create all intervals daily = self._report monthly = daily.create_report(interval='monthly') yearly = monthly.create_report(interval='yearly') return [{'category': 'Yearly', 'data': self.get_table_json(yearly)}, {'category': 'Monthly', 'data': self.get_table_json(monthly)}, {'category': 'Daily', 'data': self.get_table_json(daily)} ] def get_report_json(self, interval="yearly"): """ creates a data-structure of the report data that can be used for displaying the report as table in html files (in jinja2 templates). interval can be one of the common intervals of the report class (e.g. yearly, monthly, daily) or None. If None, thee raw data are exported. If interval is 'all', all intervals will be returned with a different json structure """ if interval is 'all': # create all intervals return self.get_all_tables_json() else: if interval is None: report = self._report else: report = self._report.create_report(interval) return self.get_table_json(report) def payment_input(self, account_str, payment, kind, description, meta): """ Input function for payments. This account is the receiver of a transfer. This function, if derived from, can account for special checks for input operations """ return TransferMessage(C_transfer_OK, money = payment) def payment_output(self, account_str, payment, kind, description, meta): """ Output function for payments. This account is the sender of a transfer. This function, if derived from, can account for special checks for output operations """ return TransferMessage(C_transfer_OK, money = payment) def return_money(self, money): """ this is a hard return of transfer-money, in case the receiving side rejected the transfer """ pass def set_date(self, date): """ This function is called by the simulation class to set the current date for the simulation """ # if there is an inconsistency in the date progression, report # a warning on the command line delta = (date - self._current_date).days if delta != 1: warnings.warn('Difference between current date and next date is %i and not 1' % delta) if date < self._date_start: warnings.warn('Date is before start date of account.') self._current_date = date def start_of_day(self): """ Things that should happen on the start of the day, before any money transfer happens """ pass def end_of_day(self): """ Things that should happen at the end of the day, after all money transfers have been accomplished """ pass class DummyAccount(Account): """ This account is used when the user creates a Transfer using a String as the from-account or to-account. This account basically agrees to everything. It can be used to create payments for loans or for outgoing costs """ def __init__(self, name): """ Creates a dummy account class """ self._name = validate.valid_name(name) # now the implementation of the real, usable classes begins. In contrast to the account class, # in these classes, report gets some semantic information about how to handle different # properties of the class class Bank_Account(Account): """ This is a normal bank account that can be used to manage income and outgoings within a normal household """ def __init__(self, amount, interest, date = None, name = None, meta = {}): """ Creates a bank account class """ # call inherited method __init__ super().__init__( amount = amount, interest = interest, date = date, name = name, meta = meta) self._report_input = 0 self._report_output = 0 self._report.add_semantics('account', 'saving_abs') self._report.add_semantics('interest', 'win_cum') self._report.add_semantics('input', 'input_cum') self._report.add_semantics('output', 'output_cum') self._report.add_semantics('foreign_account', 'none') self._report.add_semantics('kind', 'none') self._report.add_semantics('description', 'none') self._interest_paydate = {'month': 12, 'day': 31} # reporting functionality self._report_interest = 0 self.make_report() # overwriting function def make_report(self, interest=0, input=0, output=0, foreign_account = '', kind = '', description = '', meta = {}): """ creates a report entry and resets some variables """ self._report.append(date = self._current_date, account = self._caccount / 100, interest = float('%.2f' % (interest / 100)), input = input / 100, output = output / 100, foreign_account = foreign_account, kind = kind, description = description, meta = meta ) def exec_interest_time(self): """ Does all things, when self.interest_time() returns true (like adding interests to the account """ self._caccount = int(round(self._caccount + self._sum_interest)) self.make_report( interest = self._sum_interest, kind = 'yearly interest' ) self._sum_interest = 0 def as_df(self): df = self.report.as_df() df = df[['foreign_account', 'description', 'input', 'output', 'interest', 'account']] return df def get_table_json(self, report): """ Creates a table for a given report """ rows = [] if report.precision is 'daily': header = ['date', 'from', 'description', 'input', 'output', 'interest', 'account'] for status in report._statuses: item = [status.strdate, status._status['foreign_account'], status._status['description'], '%.02f EUR' % status._status['input'], '%.02f EUR' % status._status['output'], '%.02f EUR' % status._status['interest'], '%.02f EUR' % status._status['account']] rows.append(item) else: header = ['date', 'input', 'output', 'interest', 'account'] for status in report._statuses: item = [status.strdate, '%.02f EUR' % status._status['input'], '%.02f EUR' % status._status['output'], '%.02f EUR' % status._status['interest'], '%.02f EUR' % status._status['account']] rows.append(item) return {'header': header, 'rows': rows} def interest_time(self): """ Checks, whether it is time to book the interests to the account """ return ((self._current_date.day == self._interest_paydate['day']) and (self._current_date.month == self._interest_paydate['month'])) def payment_input(self, account_str, payment, kind, description, meta): """ Input function for payments. This account is the receiver of a transfer. This function, if derived from, can account for special checks for input operations """ return self.payment_move(account_str, payment, kind, description, meta) def payment_output(self, account_str, payment, kind, description, meta): """ Output function for payments. This account is the sender of a transfer. This function, if derived from, can account for special checks for output operations """ return self.payment_move(account_str, payment, kind, description, meta) def payment_move(self, account_str, payment, kind, description, meta): """ in the base class, payment_input and payment_output have almost the same behavior. Only the type of reporting differs account_str : the opposite account, sender or receiver payment : the int or function which includes the payment kind : whether this is a regular payment or a unique one description: description of the payment (usually its name) move_type: "input" or "output" for indicating the direction of movement """ move_type = 'input' if payment < 0: move_type = 'output' self._caccount = int(self._caccount + payment) report = {'foreign_account': account_str, move_type: payment, 'kind': kind, 'description': description, 'meta': meta} self.make_report(**report) return TransferMessage(C_transfer_OK, money = payment) def return_money(self, money): """ this is a hard return of transfer-money, in case the receiving side rejected the transfer """ self._caccount = int(self._caccount + money) report = { 'input': money, 'kind': 'storno', 'description': 'transfer did not succeeded'} self.make_report(**report) def start_of_day(self): """ Things that should happen on the start of the day, before any money transfer happens """ pass def end_of_day(self): """ Things that should happen at the end of the day, after all money transfers have been accomplished """ # TODO: needs to be replaced by a mechanism that checks not every day days_per_year = get_days_per_year(self._current_date.year) # calculate interest for this day interest = self._caccount * (self._interest / days_per_year) # store interest for later calculations self._sum_interest += interest # if paydate is there, add the summed interest to the account if self.interest_time(): self.exec_interest_time() class Loan(Account): """ This is the default account class that should capture the essential functionalities of account models """ def __init__(self, amount, interest, date = None, name = None, meta = {}): """ Creates the data for a basic account model """ # call inherited method __init__ super().__init__( amount = -amount, interest = interest, date = date, name = name, meta = meta) # reporting functionality self._report_payment = 0 self._report.add_semantics('account', 'debt_abs') self._report.add_semantics('interest', 'cost_cum') self._report.add_semantics('payment', 'debtpayment_cum') self._report.add_semantics('foreign_account', 'none') self._report.add_semantics('kind', 'none') self._report.add_semantics('description', 'none') self._interest_paydate = {'month': 12, 'day': 31} self.make_report() def as_df(self): df = self.report.as_df() df = df[['foreign_account', 'description', 'payment', 'interest', 'account']] return df def get_table_json(self, report): rows = [] if report.precision is 'daily': header = ['date', 'from', 'description', 'payment', 'interest', 'account'] for status in report._statuses: item = [status.strdate, status._status['foreign_account'], status._status['description'], '%.02f EUR' % status._status['payment'], '%.02f EUR' % status._status['interest'], '%.02f EUR' % status._status['account']] rows.append(item) else: header = ['date', 'payment', 'interest', 'account'] for status in report._statuses: item = [status.strdate, '%.02f EUR' % status._status['payment'], '%.02f EUR' % status._status['interest'], '%.02f EUR' % status._status['account']] rows.append(item) return {'header': header, 'rows': rows} def is_finished(self): """ Returns true, if the loan has been payed back, including interest for the current year """ return (self._caccount + self._sum_interest) >= 0. def make_report(self, payment = 0, interest = 0, foreign_account = '', kind = '', description = '', meta = {}): """ creates a report entry and resets some variables """ self._report.append( date = self._current_date, account = self._caccount / 100, payment = payment / 100, interest = float('%.2f' % (interest / 100)), foreign_account = foreign_account, kind = kind, description = description, meta = meta ) @property def account(self): return (self._caccount + self._sum_interest) / 100 def get_account(self): return self.account def exec_interest_time(self): """ Does all things, when self.interest_time() returns true (like adding interests to the account """ self._caccount = int(round(self._caccount + self._sum_interest)) self.make_report( interest = self._sum_interest, kind = 'yearly interest' ) self._sum_interest = 0 def interest_time(self): """ Checks, whether it is time to book the interests to the account """ return (((self._current_date.day == self._interest_paydate['day']) and (self._current_date.month == self._interest_paydate['month'])) or (self._caccount > 0)) def payment_input(self, account_str, payment, kind, description, meta): """ Input function for payments. This account is the receiver of a transfer. This function, if derived from, can account for special checks for input operations """ if ((self._caccount + self._sum_interest) >= 0): return TransferMessage(C_transfer_NA, money = 0, message = "No credit to pay for") payed = min(-(self._caccount + self._sum_interest), payment) if payed == payment: self._caccount = int(self._caccount + payed) report = {'payment': payed, 'foreign_account': account_str, 'kind': kind, 'description': description, 'meta': meta} self.make_report(**report) else: self._caccount = int(self._caccount + self._sum_interest + payed) report = {'payment': payed, 'interest': self._sum_interest, 'foreign_account': account_str, 'kind': kind, 'description': description + ' + Interests', 'meta': meta} self.make_report(**report) self._sum_interest = 0 return TransferMessage(C_transfer_OK, money = payed) def payment_output(self, account_str, payment, kind, description, meta): """ Output function for payments. This account is the sender of a transfer. This function, if derived from, can account for special checks for output operations """ return TransferMessage(C_transfer_NA, money = 0, message = "Credit cannot be increased") def return_money(self, money): """ this is a hard return of transfer-money, in case the receiving side rejected the transfer """ self._caccount = int(self._caccount + money) report = {'date': self._current_date, 'account': self._caccount, 'payment': money, 'kind': 'storno', 'description': 'transfer did not succeeded'} self._report.append(**report) def start_of_day(self): """ Things that should happen on the start of the day, before any money transfer happens """ pass def end_of_day(self): """ Things that should happen at the end of the day, after all money transfers have been accomplished """ # TODO: needs to be replaced by a mechanism that checks not every day days_per_year = get_days_per_year(self._current_date.year) # calculate interest for this day interest = self._caccount * (self._interest / days_per_year) # store interest for later calculations self._sum_interest += interest # if paydate is there, add the summed interest to the account if self.interest_time(): self.exec_interest_time() class Property(Account): """ This class can be used to reflect the amount of property that is gained from filling up a loan. This account does nothing else than adjusting the amount of property depending on the payments transfered to the loan class """ def __init__(self, property_value, amount, loan, date = None, name = None, meta = {}): """ For a property with a given value (property_value), the current amount that is transfered to the owner (amount) is reflected by the amount of money that has been transfered to the loan. Loan must here of class loan property_value : value of the property amount : amount of money that represents the ownership. if amount=property_value, the property totally belongs to the owner, if amount= amount, 'property_value must be greater than amount' self._name = validate.valid_name(name) self._date_start = validate.valid_date(date) self._meta = meta self._property_value = int(property_value * 100) self._account = int(amount*100) # amount of money already invested self._caccount = self._account self._loan = loan # setting up the report and the semantics self._report = Report(name = self._name) self._report.add_semantics('account', 'saving_abs') self._report.add_semantics('property_value', 'none') self._current_date = self._date_start self.make_report() def make_report(self): """ creates a report entry and resets some variables """ self._report.append( date = self._current_date, account = self._caccount / 100, property_value = self._property_value / 100 ) def get_table_json(self, report): """ Creates a table for a given report """ header = ['date', 'account'] rows = [] for status in report._statuses: item = [status.strdate, '%.02f' % status._status['account'] ] rows.append(item) return {'header': header, 'rows': rows} def get_account(self): return self._caccount / 100 def payment_input(self, account_str, payment, kind, description, meta): """ Input function for payments. This account is the receiver of a transfer. This function, if derived from, can account for special checks for input operations """ return TransferMessage(C_transfer_ERR, money = payment, message="Properties cannot be involved in transfers") def payment_output(self, account_str, payment, kind, description, meta): """ Output function for payments. This account is the sender of a transfer. This function, if derived from, can account for special checks for output operations """ return TransferMessage(C_transfer_ERR, money = payment, message="Properties cannot be involved in transfers") def return_money(self, money): """ this is a hard return of transfer-money, in case the receiving side rejected the transfer """ pass def end_of_day(self): """ Things that should happen at the end of the day, after all money transfers have been accomplished """ new_caccount = self._account + (1- (self._loan._caccount / self._loan._account)) * (self._property_value - self._account) # this if-clause is included to avoid daily reporting. Reports are # just updates, if account volume changes or if if is the end of a year if ((new_caccount != self._caccount) or ((self._current_date.day == 31) and (self._current_date.month == 12))): self._caccount = new_caccount self.make_report() ================================================ FILE: financial_life/financing/colors.py ================================================ ''' Created on 30.03.2016 @author: martin ''' import xml.etree.ElementTree as ET import re def import_colors(): group_tag = '{http://www.w3.org/2000/svg}g' rect_tag = '{http://www.w3.org/2000/svg}rect' color_file = '/home/martin/ownCloud/Software/credit/misc/colors.svg' tree = ET.parse(color_file) root = tree.getroot() layer = root.find(group_tag) groups = layer.findall(group_tag) colorgroups = [] for group in groups: schemas = group.findall(group_tag) colorgroup = [] for schema in schemas: rects = schema.findall(rect_tag) colors = [] for rect in rects: colors.append(re.search('#[0-9a-f]{6}', rect.attrib['style']).group()) colorgroup.append(colors) colorgroups.append(colorgroup) print(colorgroups) colors = [ [ ['#9e63e7', '#b283ed', '#cbaaf5', '#e0ccf9', '#884dd7', '#6925c5', '#5512b0', '#430596'], ['#6463e6', '#8483ec', '#aaa9f5', '#cccbf9', '#4c4dd7', '#2526c4', '#1113b0', '#050596'], ['#629ce6', '#83b1eb', '#a8caf5', '#cbdef9', '#4b8bd7', '#256dc3', '#105ab0', '#054696'], ['#62cae5', '#82d5eb', '#a7e4f5', '#cbeef9', '#4bbbd6', '#25a4c3', '#0f92b0', '#057896'], ['#62e4df', '#82eae6', '#a6f5f2', '#cbf9f8', '#4ad6ce', '#25c3ba', '#0fb0a5', '#05968f'] ], [ ['#c6e662', '#d2eb83', '#e2f5a8', '#edf9cb', '#b7d74b', '#a0c325', '#8cb010', '#749605'], ['#e6e462', '#ebeb83', '#f5f4a8', '#f8f9cb', '#d7d34b', '#c3c025', '#b0ab10', '#969305'], ['#e5a962', '#ebbb82', '#f5d1a7', '#f9e5cb', '#d6944b', '#c37825', '#b0630f', '#965205'], ['#e47a62', '#ea9682', '#f5b6a6', '#f9d5cb', '#d6634a', '#c34125', '#b02a0f', '#962005'], ['#e36269', '#e98287', '#f4a6aa', '#f9cbcc', '#d64954', '#c32530', '#b00f1c', '#96050e'] ], [ ['#a2a2a2', '#b5b5b5', '#cdcdcd', '#e2e2e2', '#8f8f8f', '#747474', '#5f5f5f', '#4d4d4d'] ], [ ['#7a68b3', '#9080c1', '#aa9dd3', '#c1b7df', '#6b5e97', '#4f4378', '#392b68', '#261953'], ['#6876b2', '#808dc0', '#9ca6d3', '#b6bddf', '#5d6997', '#434e77', '#2a3768', '#192553'], ['#6797b2', '#81a8be', '#9bbfd3', '#b6cfdf', '#5d8396', '#426577', '#295368', '#193f53'], ['#68b0b0', '#80bdbe', '#9ad2d3', '#b6dddf', '#5d9595', '#427777', '#286867', '#195353'], ['#68af9e', '#81bdae', '#99d3c5', '#b6dfd6', '#5d9486', '#427769', '#286857', '#195345'] ], [ ['#aeb267', '#bcbe81', '#d1d39b', '#dcdfb6', '#95965d', '#767742', '#676829', '#515319'], ['#b2a267', '#beb281', '#d3c79b', '#dfd8b6', '#96895d', '#776b42', '#685a29', '#534619'], ['#b08168', '#be9580', '#d3ad9a', '#dfc5b6', '#956f5d', '#775342', '#683d28', '#532c19'], ['#af6869', '#bd8181', '#d39999', '#dfb7b6', '#945d5e', '#774244', '#68282a', '#53191a'], ['#ad697a', '#bb818f', '#d19aa8', '#dfb6bf', '#945c6c', '#774251', '#68283a', '#531928'] ] ] no_colors = len(colors[0]) ================================================ FILE: financial_life/financing/identity.py ================================================ ''' Created on 18.11.2016 Some basic classes for generating and managing identities @author: martin ''' # standard libraries import string import random def id_generator( size=6, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits ): """ Creates a unique ID consisting of a given number of characters with a given set of characters """ return ''.join(random.choice(chars) for _ in range(size)) ================================================ FILE: financial_life/financing/plotting.py ================================================ ''' Created on 30.03.2016 @author: martin ''' #standard libraries from datetime import timedelta # custom libraries from matplotlib.pyplot import * from matplotlib import pyplot as plt import matplotlib.patches as mpatches import numpy as np # own libraries from .colors import colors, no_colors from financial_life.calendar_help import Bank_Date #ion() # indices for colors in the colors-list C_cold_colors = 0 C_warm_colors = 1 C_format_date = '%d.%m.%Y' def bank_account(*reports): fig = figure(figsize=(16, 13)) plot_stack_mult_abs(['input_cum', 'output_cum'], *reports, color_themes = [C_warm_colors, C_cold_colors], color_offset = 1) title('Demands on the account') return fig def summary(*reports): """ Summary plot for all reports """ fig = figure(figsize=(16, 13)) ax_date = subplot(3,2,1) plot_stack_abs('saving_abs', *reports, color_theme = C_warm_colors, color_offset = 0) title('Wealth') subplot(3,2,2, sharex = ax_date) plot_stack_abs('debt_abs', *reports, color_theme = C_cold_colors, color_offset = 0) title('Debts') subplot(3,2,3, sharex = ax_date) plot_stack_mult_abs(['input_cum', 'output_cum'], *reports, color_themes = [C_warm_colors, C_cold_colors], color_offset = 1) title('Input and output') subplot(3,2,4, sharex = ax_date) plot_stack_abs('debtpayment_cum', *reports, color_offset = 1) title('Yearly payments') ax_winloss = subplot(3,2,5, sharex = ax_date) plot_stack_cum('win_cum', *reports, color_theme = C_warm_colors, color_offset = 2) title('Cumulated win') subplot(3,2,6, sharex = ax_date, sharey = ax_winloss) plot_stack_cum('cost_cum', *reports, color_offset = 2) title('Cumulated costs') plt.tight_layout() return fig def summary_img(*reports, target = './', figsize = (10, 5), dpi = 100, prefix=''): """ creates a series of images and stores them in the target directory """ data = {} fig = figure(figsize = figsize) plot_stack_abs('saving_abs', *reports, color_theme = C_warm_colors, color_offset = 0) title('Wealth') fig.savefig(target + prefix + 'wealth.png', dpi = dpi) plt.close(fig) data['img_wealth'] = target + prefix + 'wealth.png' fig = figure(figsize = figsize) plot_stack_abs('debt_abs', *reports, color_theme = C_cold_colors, color_offset = 0) title('Debts') fig.savefig(target + prefix + 'debts.png', dpi = dpi) plt.close(fig) data['img_debts'] = target + prefix + 'debts.png' fig = figure(figsize = figsize) plot_stack_mult_abs(['input_cum', 'output_cum'], *reports, color_themes = [C_warm_colors, C_cold_colors], color_offset = 1) title('Input and output') fig.savefig(target + prefix + 'io_money.png', dpi = dpi) plt.close(fig) data['img_io_money'] = target + prefix + 'io_money.png' fig = figure(figsize = figsize) plot_stack_abs('debtpayment_cum', *reports, color_offset = 1) title('Yearly payments') fig.savefig(target + prefix + 'debtpayment.png', dpi = dpi) plt.close(fig) data['img_debtpayment'] = target + prefix + 'debtpayment.png' fig = figure(figsize = figsize) plot_stack_cum('win_cum', *reports, color_theme = C_warm_colors, color_offset = 2) title('Cumulated win') fig.savefig(target + prefix + 'win_cum.png', dpi = dpi) plt.close(fig) data['img_win_cum'] = target + prefix + 'win_cum.png' fig = figure(figsize = figsize) plot_stack_cum('cost_cum', *reports, color_offset = 2) title('Interests') fig.savefig(target + prefix + 'cost_cum.png', dpi = dpi) plt.close(fig) data['img_cost_cum'] = target + prefix + 'cost_cum.png' return data def extract_data(semantic, *reports, color_theme = C_cold_colors, color_offset = 0): """ helping function that extracts the dates and data from the reports """ X = [] Y = [] c = [] # create cost plots for j, r in enumerate(reports): X = X + [[d.timestamp() for d in r.date]] Y = Y + [[r.get(k) for k in r.semantics(semantic)]] c = c + [colors[color_theme][j % no_colors][i+color_offset] for i, k in enumerate(r.semantics(semantic))] return X, Y, c def add_labels(semantic, *reports, color_theme = C_cold_colors, color_offset = 0): """ routine that adds labels to the plot in order to add a legend """ # these empty plots are needed as stack plot does not support labels for legends. bit of # a hack from http://stackoverflow.com/questions/14534130/legend-not-showing-up-in-matplotlib-stacked-area-plot for j, r in enumerate(reports): for i, k in enumerate(r.semantics(semantic)): plot([], [], color=colors[color_theme][j % no_colors][i+color_offset], label=r.name + ': ' + k, linewidth=10) def plot_stack_generic(X, Y, c, semantic, *reports, color_theme = C_cold_colors, color_offset = 0): """ generic function for plotting stacks """ # bring the data together dates, data = join_data(X, Y) # format the dates to a readable format str_dates = [Bank_Date.fromtimestamp(d).strftime(C_format_date) for d in dates] add_labels(semantic, *reports, color_theme = color_theme, color_offset = color_offset) if len(data) > 0: stackplot(dates, data, colors = c) else: # if data is empty, plot at least zeros to make this plot more complete plot(dates, np.zeros(len(dates))) xticks(dates, str_dates, rotation=45) def remove_nones(X): """ Removes all Nones from a list and replaces them by zero """ return [[[0. if v is 'None' else v for v in d] for d in data] for data in X] def add_zeros(X, Y): """ add zeros at the beginning and end to prevent interpolation in join_data from stacking to much up """ for x in X: x.insert(0, x[0] - 1) x.append(x[-1] + 1) for data in Y: for d in data: d.insert(0, 0.) d.append(0.) return X, Y def plot_stack_abs(semantic, *reports, color_theme = C_cold_colors, color_offset = 0): """ Creates a stacked plot with cumulated sums for a given semantic """ X, Y, c = extract_data(semantic, *reports, color_theme = color_theme, color_offset = color_offset) Y = remove_nones(Y) X, Y = add_zeros(X, Y) # create the generic plot plot_stack_generic(X, Y, c, semantic, *reports, color_theme = color_theme, color_offset = color_offset) legend(loc = 'upper right', fancybox=True, framealpha=0.4, prop={'size':10}) def plot_stack_mult_abs(semantics, *reports, color_themes, color_offset = 0): """ Creates a stacked plot with cumulated sums for a given semantic """ for semantic, color_theme in zip(semantics, color_themes): plot_stack_abs(semantic, *reports, color_theme = color_theme, color_offset = color_offset) def plot_stack_cum(semantic, *reports, color_theme = C_cold_colors, color_offset = 0): """ Creates a stacked plot with cumulated sums for a given semantic """ X, Y, c = extract_data(semantic, *reports, color_theme = color_theme, color_offset = color_offset) Y = remove_nones(Y) # add zeros only at the beginning for x in X: x.insert(0, x[0] - 1) for data in Y: for d in data: d.insert(0, 0.) # create the cumulated sum Y = [np.cumsum(np.array(yi), 1) if yi else [] for yi in Y] plot_stack_generic(X, Y, c, semantic, *reports, color_theme = color_theme, color_offset = color_offset) legend(loc = 'upper left', fancybox=True, framealpha=0.4, prop={'size':10}) def join_data(dates_list, data_list): """ This functions makes heterogenous time series data align with one time series axis dates : list of date-lists data : list of data-lists_lock Returns: dates, and data, but this time, data shares the same date-points """ # first get all unique dates from every sublist and make one list out of them rdates = sorted(list(set([date for sublist in dates_list for date in sublist]))) rdata = [] # go through each vector and interpolate data if necessary for dates, data_vecs in zip(dates_list, data_list): for data in data_vecs: if len(data) > 0: rdata.append(np.interp(rdates,dates, data).tolist()) else: # if data is empty, then just create a zero-length vector rdata.append(np.zeros(len(rdates))) return rdates, rdata ================================================ FILE: financial_life/financing/test_financing.py ================================================ # -*- coding: utf-8 -*- """ Created on Thu Jun 23 21:40:47 2016 @author: martin """ import unittest import financial_life.financing as financing from financial_life.calendar_help import Bank_Date from datetime import datetime class Test_Create_Stop_Criteria(unittest.TestCase): def test_date(self): t = datetime(2016,10,15) foo = financing.create_stop_criteria(t) self.assertTrue(foo(datetime(2016,10,14))) self.assertFalse(foo(datetime(2016,10,16))) def test_callable(self): t = lambda x: x < datetime(2016,11,18) foo = financing.create_stop_criteria(t) self.assertFalse(foo(datetime(2016,10,14))) self.assertFalse(foo(datetime(2016,11,14))) self.assertTrue(foo(datetime(2016,11,19))) class TestRegular_Month_Payment(unittest.TestCase): def setUp(self): self.regular = {'from_acc': 'Dummy', 'to_acc': 'Dummy', 'interval': 'month', 'day' : 15, 'date_start': Bank_Date(2015, 3, 15), 'date_stop': Bank_Date(2015, 6, 15), 'payment': 3000, 'name' : 'Test', 'fixed': True, 'meta': {} } self.infinite = {'from_acc': 'Dummy', 'to_acc': 'Dummy', 'interval': 'month', 'day' : 15, 'date_start': Bank_Date(2015, 3, 15), 'date_stop': Bank_Date.max, 'payment': 3000, 'name' : 'Test', 'fixed': True, 'meta': {} } self.lastcal = {'from_acc': 'Dummy', 'to_acc': 'Dummy', 'interval': 'month', 'day' : 31, 'date_start': Bank_Date(2015, 1, 31), 'date_stop': Bank_Date(2015, 6, 15), 'payment': 3000, 'name' : 'Test', 'fixed': True, 'meta': {} } def test_begin_payment_next_month(self): iterator = financing.iter_regular_month(self.regular, date_start = datetime(2015,3, 16)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2015,4,15)) iterator = financing.iter_regular_month(self.regular, date_start = datetime(2016,3, 16)) self.assertRaises(StopIteration, next, iterator) def test_begin_payment_next_year(self): iterator = financing.iter_regular_month(self.infinite, date_start = datetime(2016,3, 16)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2016, 4, 15)) def test_begin_payment_this_month(self): iterator = financing.iter_regular_month(self.regular, date_start = datetime(2015,3, 15)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2015,3,15)) def test_proper_sequence(self): iterator = financing.iter_regular_month(self.regular, date_start = datetime(2015,3, 20)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2015,4,15)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2015,5,15)) self.assertRaises(StopIteration, next, iterator) def test_last_calender_day(self): iterator = financing.iter_regular_month(self.lastcal, date_start = datetime(2015,2, 28)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2015,2,28)) class TestRegular_Year_Payment(unittest.TestCase): def setUp(self): self.regular = {'from_acc': 'Dummy', 'to_acc': 'Dummy', 'interval': 'yearly', 'day' : 15, 'date_start': Bank_Date(2015, 3, 15), 'date_stop': Bank_Date(2018, 3, 14), 'payment': 3000, 'name' : 'Test', 'fixed': True, 'meta': {} } self.infinite = {'from_acc': 'Dummy', 'to_acc': 'Dummy', 'interval': 'yearly', 'day' : 15, 'date_start': Bank_Date(2015, 3, 15), 'date_stop': Bank_Date.max, 'payment': 3000, 'name' : 'Test', 'fixed': True, 'meta': {} } self.lastcal = {'from_acc': 'Dummy', 'to_acc': 'Dummy', 'interval': 'yearly', 'day' : 31, 'date_start': Bank_Date(2015, 1, 31), 'date_stop': Bank_Date(2017, 6, 15), 'payment': 3000, 'name' : 'Test', 'fixed': True, 'meta': {} } def test_begin_payment_same_year(self): iterator = financing.iter_regular_year(self.regular, date_start = datetime(2015,3, 15)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2015,3,15)) def test_begin_payment_next_year(self): iterator = financing.iter_regular_year(self.infinite, date_start = datetime(2015,3, 16)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2016, 3, 15)) def test_proper_sequence(self): iterator = financing.iter_regular_year(self.regular, date_start = datetime(2015,3, 20)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2016,3,15)) payment = next(iterator) self.assertEqual(payment['date'], datetime(2017,3,15)) self.assertRaises(StopIteration, next, iterator) if __name__ == '__main__': unittest.main() ================================================ FILE: financial_life/financing/test_meta.py ================================================ ''' Created on 04.01.2017 @author: martin ''' # standard libraries from datetime import datetime, timedelta import unittest # own libraries from financial_life.examples import meta_data class Test(unittest.TestCase): def setUp(self): # run a simulation that makes use of meta-information self.s = meta_data.example_meta_controller(print_it=False) def tearDown(self): pass def test_account_and_simulation(self): """ Test that meta-information are in the account class and in the simulation class """ accounts = self.s.accounts[0] s_income_report = self.s.report.subset( lambda st: st.meta.get('type','') == 'income') a_income_report = accounts.report.subset( lambda st: st.meta.get('type','') == 'income') s_interests = sum(s_income_report.value) a_interests = sum(a_income_report.input) self.assertTrue(len(s_income_report) == len(a_income_report), 'Reprots in Simulation and Account class with meta-information have not the same length') self.assertTrue(s_interests == a_interests, 'Values in the account and simulation class are not the same') if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() ================================================ FILE: financial_life/financing/test_status.py ================================================ ''' Created on 15.12.2016 @author: martin ''' # standard libraries from datetime import datetime, timedelta import unittest # own libraries from financial_life.financing import accounts as a from financial_life.financing import Status class Test(unittest.TestCase): def setUp(self): """ Create setup for making sure that meta-data are transfered to the status of payments """ account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account', date=datetime(2016,9, 1)) savings = a.Bank_Account(amount = 5000, interest = 0.013, name = 'Savings', date=datetime(2016,9, 1)) loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit', date=datetime(2016,9, 1)) simulation = a.Simulation(account, savings, loan, name = 'Testsimulation', date=datetime(2016,9, 1)) simulation.add_regular(from_acc = 'Income', to_acc = account, payment = 2000, interval = 'monthly', date_start = datetime(2016,9,15), day = 15, name = 'Income') simulation.add_regular(from_acc = account, to_acc = savings, payment = 500, interval = 'monthly', date_start = datetime(2016,9,30), day = 30, name = 'Savings', meta = {'tax': 100}) simulation.simulate(delta=timedelta(days=2000)) self.simulation = simulation def test_InitStatus(self): # initialize without data field s = Status(datetime(2016,9,1), a=2, b=3, ceta=4) self.assertDictEqual(s._status, {'a':2, 'b':3, 'ceta':4}) self.assertDictEqual(s._meta, {}) # initialize with data field s = Status(datetime(2016,9,1), a=2, b=3, meta={'test': 3}) self.assertDictEqual(s._status, {'a':2, 'b':3}) self.assertDictEqual(s._meta, {'test': 3}) def test_str(self): s = Status(datetime(2016,9,1), a=2, b=3, ceta=4) str(s) def test_SimulationReport(self): # the report of the simulation-class itself should be displayable status1 = self.simulation.report._statuses[0] status2 = self.simulation.report._statuses[1] self.assertDictEqual(status1._meta, {}) print(status2._meta) self.assertDictEqual(status2._meta, {'tax': 100}) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() ================================================ FILE: financial_life/financing/validate.py ================================================ # -*- coding: utf-8 -*- """ Created on Mon Jun 27 21:27:23 2016 Collection of validation messages @author: martin """ # standard libraries from datetime import datetime, timedelta from collections import Callable # custom libraries # own libraries from financial_life.financing.identity import id_generator from financial_life.calendar_help import Bank_Date date_formats = [ '%d.%m.%Y', '%d.%m.%y', '%m/%d/%Y', '%m/%d/%y', '%Y-%m-%d', '%y-%m-%d', ] def parse_datestring(datestr): """ Tries to parse the datestring against a few common formats """ for format in date_formats: try: date = Bank_Date.strptime(datestr, format) return date except ValueError: pass def valid_date(date): """ routine for making a date out of anything that the user might have given to the function """ if date is None: return Bank_Date.today() if isinstance(date, Bank_Date): return date if isinstance(date, datetime): return Bank_Date.fromtimestamp(date.timestamp()) if isinstance(date, str): return parse_datestring(date) raise TypeError("Date must be from type datetime, callable or a parseable string") def valid_stop_date(date): """ routine for makig a date out of anything that the user might have given to the function """ if isinstance(date, Callable): return date else: return valid_date(date) raise TypeError("Date must be at least from type datetime or callable") def valid_name(name): if not name: name = id_generator(8) return name def valid_date_stop(date_stop): """ checks, whether date_stop has a valid value """ if not date_stop: date_stop = Bank_Date.max return date_stop def valid_delta(delta): """ converts delta, if necessary, into a timedelta instance """ if not delta: delta = timedelta.max if isinstance(delta, int): delta = timedelta(days = delta) assert isinstance(delta, timedelta), 'delta must be of type timedelta or int' assert delta.days > 0, 'delta must be positive' return delta ================================================ FILE: financial_life/products/__init__.py ================================================ ================================================ FILE: financial_life/products/germany/__init__.py ================================================ ================================================ FILE: financial_life/products/germany/lbs/__init__.py ================================================ ''' This is a reconstruction of the LBS Bauspar Tarife WARNING: This is a manual attempt to simualate some of the LBS products. This is by no means a reliable and accurate simulation! For a realistic simulation of your financial plans, please consult a pofessional assistent from the company selling these products. ''' # standard libraries from datetime import datetime, timedelta from calendar import monthrange from decimal import * # custom libraries import numpy as np # own libraries from financial_life.financing import Report, Payments from financial_life.financing import C_default_payment, id_generator from financial_life.financing.colors import colors from financial_life.financing import validate from financial_life.financing.accounts import Account, C_format_date, C_max_time from financial_life.calendar_help import Bank_Date flex_l5 = { 'C_POINT_PER_DAY': 0.0563, 'C_POINT_PER_EUR': 1 / 750., 'C_POINT_LIMIT': 171, 'guthabenzins' : 0.0025, 'entgelt' : 7.2, 'bausparanteil': 0.4, 'darlehenszins': 0.0215, 'wartemonate': 4, 'agio': 0.02, 'versicherung': 0.003, 'name' : 'Flex L5', } direkt_10 = { 'C_POINT_PER_DAY': 0.0403, 'C_POINT_PER_EUR': 1 / 650., 'C_POINT_LIMIT': 176, 'guthabenzins' : 0.001, 'entgelt' : 7.2, 'bausparanteil': 0.4, 'darlehenszins': 0.0195, 'wartemonate': 3, 'agio': 0.02, 'versicherung': 0.003, 'name' : 'Direkt 10', } direkt_15 = { 'C_POINT_PER_DAY': 0.0403, 'C_POINT_PER_EUR': 1 / 650., 'C_POINT_LIMIT': 176, 'guthabenzins' : 0.001, 'entgelt' : 7.2, 'bausparanteil': 0.4, 'darlehenszins': 0.0175, 'wartemonate': 3, 'agio': 0.02, 'versicherung': 0.003, 'name' : 'Direkt 15', } alternative = { 'C_POINT_PER_DAY': 0.0403, 'C_POINT_PER_EUR': 1 / 650., 'C_POINT_LIMIT': 176, 'guthabenzins' : 0.001, 'entgelt' : 7.2, 'bausparanteil': 0.4, 'darlehenszins': 0.025, 'wartemonate': 3, 'agio': 0.02, 'versicherung': 0.003, 'name' : 'Direkt 15', } tarife = { 'flex_l5': flex_l5, 'direkt_10': direkt_10, 'direkt_15': direkt_15, 'alternative': alternative } class Bauspar(Account): """ This is a generic class for the german LBS Bauspar product """ def __init__(self, guthaben, bausparsumme, punkte, tarif, date = None, name = None): if tarif not in tarife: raise TypeError("Contract type not found in contract list: {}".format(tarif)) self._tarif = tarife[tarif] self._date_start = self.valid_date(date) self._name = self.valid_name(name) self._report = Report( name = self._name + ' - ' + str(self._date_start.strftime(C_format_date))) self._report.add_semantics('account', 'saving_abs') self._report.add_semantics('loan', 'debt_abs') self._report.add_semantics('loan_interest', 'cost_cum') self._report.add_semantics('account_interest', 'win_cum') self._report.add_semantics('points', 'none') self._report.add_semantics('payments', 'debtpayment_cum') self._report.add_semantics('agio', 'cost_cum') self._report.add_semantics('insurance', 'cost_cum') self._report.add_semantics('entgelt', 'cost_cum') self._guthaben = int(guthaben * 100) self._bausparsumme = int(bausparsumme * 100) self._darlehen = self._bausparsumme - np.max( [ self._bausparsumme * self._tarif['bausparanteil'], self._guthaben ] ) self._punkte = punkte self._payments = Payments() self._payments_iter = None self._sum_interest = 0 # this value is used because it is inherited from account self._sum_loan_interest = 0 # but we need an extra variable for the loan interest self._sum_loan_insurance = 0 self._day = -1 self._current_date = self._date_start self._caccount = self._guthaben self._cdarlehen = self._darlehen self._next_pay = None self._interest_paydate = {'month': 12, 'day': 31} # this function determines, in which phase of the product we are self._phase = self.saving_phase # reporting functionality self._record = { 'loan_interest': 0, 'account_interest': 0, 'payments' : 0, 'entgelt' : 0, 'insurance': 0, 'agio': 0 } @property def loan(self): return self._cdarlehen / 100 def get_loan(self): """ alternative method to get the current loan value. this method can be used, e.g. in payment-definitions to transfer the amount of money that a specific account has in the moment this payment is done. Instead of using an actual value, this method is called, evaluated and the return value is used """ return self.loan def simulate(self, date_stop = None, delta = None, last_report = True): """ Simulates the state of the account to the end-date. If there is no end_date, the simulation will run until account is either zero or the account continuously increases 10 times in a row delta: Time (e.g. days) to simulate. This argument can be used along with date_stop. Whatever comes first, aborts the while-loop last_report: if True, after the while-loop a report will be added. For simulations along with other products, this can be omitted by setting this argument to False """ date_stop = validate.valid_date_stop(date_stop) delta = validate.valid_delta(delta) # if this is not the time for this product, abort if date_stop < self._current_date: return if (not self._payments_iter): self._payments_iter = self._payments.payment(self._current_date) if (not self._next_pay): self._next_pay = next(self._payments_iter, C_default_payment) self._phase(date_stop, delta, last_report) def get_credit(self): """ Switches to a new modus in this product, in which the loan is given to the customer. Depending on the amount of points and the account, the customer needs to enter the so-called "zwischenfinanzierung" """ self._cdarlehen = self._bausparsumme - self._caccount if (self._punkte < self._tarif['C_POINT_LIMIT'] or self._caccount < (self._tarif['bausparanteil'] * self._bausparsumme)): self._phase = self.zwischen_phase else: self._phase = self.loan_phase agio = self._cdarlehen * self._tarif['agio'] self._cdarlehen += agio self._report.append(date = self._current_date, agio = (agio / 100)) def loan_track_data(self, loan_interest, loan_insurance, payed): self._record['loan_interest'] += loan_interest self._record['insurance'] += loan_insurance self._record['payments'] += payed def loan_make_report(self): self._report.append(date = self._current_date, loan_interest = self._record['loan_interest'] / 100, insurance = self._record['insurance'] / 100, payments = self._record['payments'] / 100, loan = self._cdarlehen / 100, ) # set everything to zero self._record = dict.fromkeys(self._record, 0) def loan_exec_interest_time(self): self._cdarlehen = int(round(self._cdarlehen + self._sum_loan_interest + self._sum_loan_insurance)) self._sum_loan_interest = 0 self._sum_loan_insurance = 0 def loan_phase(self, date_stop = None, delta = None, last_report = True): """ Routine for the payment of the loan """ temp_delta = 0 while ((self._current_date < date_stop) and # ...stop-date is reached (temp_delta < delta.days) and # and delta has not been exeeded ((self._current_date - self._date_start).days < C_max_time) and (self._cdarlehen > 0)): # ...number of simulated days exceeds max # go to next day self._day += 1 self._current_date = self._date_start + timedelta(days = self._day) temp_delta += 1 # calculate the day self._cdarlehen, loan_interest, loan_insurance, payed = self.loan_simulate_day() # store interest for later calculations self._sum_loan_interest += loan_interest self._sum_loan_insurance += loan_insurance # if paydate is there, add the summed interest to the account if self.interest_time(): self.loan_exec_interest_time() # tracking for reports self.loan_track_data(loan_interest, loan_insurance, payed) # make a report if self.report_time(self._current_date): self.loan_make_report() # create report at the end of the simulation if last_report: # as the simulation might not end at the end of the year, # we need to apply exec_interest_time() one last time self.exec_interest_time() self.loan_make_report() def loan_simulate_day(self): days_per_year = self.get_days_per_year() payed = self.get_payments() payed = min(self._cdarlehen + self._sum_loan_interest + self._sum_loan_insurance, payed) new_darlehen = int(self._cdarlehen - payed) loan_interest = new_darlehen * (self._tarif['darlehenszins'] / days_per_year) loan_insurance = new_darlehen * (self._tarif['versicherung'] / days_per_year) return new_darlehen, loan_interest, loan_insurance, payed def zwischen_track_data(self, account_interest, loan_interest, payed, entgelt): """ tracks data during saving phase """ self._record['account_interest'] += account_interest self._record['loan_interest'] += loan_interest self._record['payments'] += payed self._record['entgelt'] += entgelt def zwischen_make_report(self): self._report.append(date = self._current_date, account = self._caccount / 100, account_interest = self._record['account_interest'] / 100, loan_interest = self._record['loan_interest'] / 100, payments = self._record['payments'] / 100, entgelt = self._record['entgelt'] / 100, loan = self._cdarlehen / 100, points = self._punkte ) # set everything to zero self._record = dict.fromkeys(self._record, 0) def zwischen_exec_interest_time(self): self._caccount = int(round(self._caccount + self._sum_interest - self._sum_loan_interest)) self._sum_interest = 0 self._sum_loan_interest = 0 def zwischen_phase(self, date_stop = None, delta = None, last_report = True): """ Routine for the phase called 'Zwischenfinanzierung' """ temp_delta = 0 while ((self._current_date < date_stop) and # ...stop-date is reached (temp_delta < delta.days) and # and delta has not been exeeded ((self._current_date - self._date_start).days < C_max_time) and (self._punkte < self._tarif['C_POINT_LIMIT'] or self._caccount < (self._tarif['bausparanteil'] * self._bausparsumme) )): # ...number of simulated days exceeds max # go to next day self._day += 1 self._current_date = self._date_start + timedelta(days = self._day) temp_delta += 1 # calculate the day self._caccount, account_interest, loan_interest, payed, entgelt = self.zwischen_simulate_day() # store interest for later calculations self._sum_interest += account_interest self._sum_loan_interest += loan_interest # if paydate is there, add the summed interest to the account if self.interest_time(): self.zwischen_exec_interest_time() self._cdarlehen = self._bausparsumme - self._caccount # tracking for reports self.zwischen_track_data(account_interest, loan_interest, payed, entgelt) # make a report if self.report_time(self._current_date): self.zwischen_make_report() # when the while loop ended because the points are above the limit, then we can # switch to the next phase if (self._punkte >= self._tarif['C_POINT_LIMIT']): self.get_credit() # if simulation time is not over yet, continue with simulating the loan_phase if ((self._current_date < date_stop) and (temp_delta < delta.days) and ((self._current_date - self._date_start).days < C_max_time)): self.loan_phase(date_stop, delta, last_report) else: # create report at the end of the simulation if last_report: # as the simulation might not end at the end of the year, # we need to apply exec_interest_time() one last time self.exec_interest_time() self.zwischen_make_report() def zwischen_simulate_day(self): days_per_year = self.get_days_per_year() new_account = self._caccount entgelt = 0 if (self._current_date.day == 1) and (self._current_date.month == 1): entgelt = self._tarif['entgelt'] * 100 new_account -= entgelt payed = self.get_payments() new_account = int(new_account + payed) self._punkte += (payed / 100) * self._tarif['C_POINT_PER_EUR'] account_interest = new_account * (self._tarif['guthabenzins'] / days_per_year) loan_interest = self._bausparsumme * (self._tarif['darlehenszins'] / days_per_year) self._punkte += self._tarif['C_POINT_PER_DAY'] return new_account, account_interest, loan_interest, payed, entgelt def saving_track_data(self, interest, payed, entgelt): """ tracks data during saving phase """ self._record['account_interest'] += interest self._record['payments'] += payed self._record['entgelt'] += entgelt def saving_make_report(self): self._report.append(date = self._current_date, account = self._caccount / 100, account_interest = self._record['account_interest'] / 100, payments = self._record['payments'] / 100, entgelt = self._record['entgelt'] / 100, points = self._punkte ) # set everything to zero self._record = dict.fromkeys(self._record, 0) def saving_phase(self, date_stop = None, delta = None, last_report = True): temp_delta = 0 while ((self._current_date < date_stop) and # ...stop-date is reached (temp_delta < delta.days) and # and delta has not been exeeded ((self._current_date - self._date_start).days < C_max_time)): # ...number of simulated days exceeds max # go to next day self._day += 1 self._current_date = self._date_start + timedelta(days = self._day) temp_delta += 1 # calculate the day self._caccount, interest, payed, entgelt = self.saving_simulate_day() # store interest for later calculations self._sum_interest += interest # if paydate is there, add the summed interest to the account if self.interest_time(): self.exec_interest_time() # tracking for reports self.saving_track_data(interest, payed, entgelt) # make a report if self.report_time(self._current_date): self.saving_make_report() # create report at the end of the simulation if last_report: # as the simulation might not end at the end of the year, # we need to apply exec_interest_time() one last time self.exec_interest_time() self.saving_make_report() def saving_simulate_day(self): days_per_year = self.get_days_per_year() new_account = self._caccount entgelt = 0 if (self._current_date.day == 1) and (self._current_date.month == 1): entgelt = self._tarif['entgelt'] * 100 new_account -= entgelt payed = self.get_payments() new_account = int(new_account + payed) self._punkte += (payed / 100) * self._tarif['C_POINT_PER_EUR'] interest = new_account * (self._tarif['guthabenzins'] / days_per_year) self._punkte += self._tarif['C_POINT_PER_DAY'] return new_account, interest, payed, entgelt ================================================ FILE: financial_life/reports/__init__.py ================================================ import platform sl = '/' if platform.system() == 'Windows': sl = '\\' ================================================ FILE: financial_life/reports/excel.py ================================================ ''' Created on 29.05.2017 @author: martin ''' # standard libraries import os # third-party libraries import pandas as pd # own libraries from financial_life.reports import sl def report(simulation, filename='report.xls'): """ This function generates a report as an excel sheet. simulation the simualation that should be exported to excel filename filename of the excel file """ writer = pd.ExcelWriter(filename) for account in simulation.accounts: df = account.report.as_df() df.to_excel(writer, sheet_name=account.name) writer.save() ================================================ FILE: financial_life/reports/html.py ================================================ ''' Created on 03.10.2016 @author: martin ''' # standard libraries import os import imp import platform # third-party libraries from jinja2 import Template # own libraries from financial_life.reports import sl path_template = '..{sl}templates{sl}html'.format(sl=sl) def report(simulation, style = 'standard', output_dir = 'report'): """ This is a generic report function that renders html-templates defined by the style-argument. 'style' refers to a template-folder in '../templates/html'. A file render.py must be included in the template-folder with a method 'render(simulation, output_dir)' that does the job. New html-templates can be easily added by creating new subfolders in '../templates/html/' with html files and a render.py """ cwd = os.path.dirname(os.path.realpath(__file__)) template_folder = cwd + sl + path_template + sl + style + sl render_module = imp.load_source('render', template_folder + 'render.py') render_module.render(simulation, output_dir) ================================================ FILE: financial_life/tax/__init__.py ================================================ ================================================ FILE: financial_life/tax/germany/__init__.py ================================================ """ help functions for german tax declaration """ def tax_to_pay(year, *args, **kwargs): """ generic functions to call the tax-function for any year. This functions simply calls the year-dependent function with the given parameters, but this function don't really care about the content of the other arguments """ return tax_functions[year](*args, **kwargs) def tax_to_pay_2016(tax_relevant_money, splitting = False): """ calculates the tax for year 2016 Returns the tax and the percentage of the tax """ if tax_relevant_money <= 0: return 0, 0 if splitting: trm = tax_relevant_money / 2. else: trm = tax_relevant_money if trm > 250730: tax = trm * 0.45 - 15783.19 elif trm > 52881: tax = trm * 0.42-8261.29 elif trm > 13469: tax = (trm-13469)*((trm-13469)*0.0000022874+0.2397)+948.68 elif trm > 8472: tax = (trm-8472)*((trm-8472)*0.000009976+0.14) else: tax = 0 if splitting: return tax*2, ((tax*2) / tax_relevant_money) else: return float(int(tax*100)/100), (tax / tax_relevant_money) tax_functions = {2016: tax_to_pay_2016} ================================================ FILE: financial_life/templates/__init__.py ================================================ ================================================ FILE: financial_life/templates/html/__init__.py ================================================ ================================================ FILE: financial_life/templates/html/standard/__init__.py ================================================ ================================================ FILE: financial_life/templates/html/standard/account_details.html ================================================ Account Detail: {{account_name}}

{{account_name}}

back to overview

{% for table in tables %}

{{ table.category }}

{% for head in table.data.header %} {% endfor %} {% for row in table.data.rows %} {% for item in row %} {% endfor %} {% endfor %}
{{head}}
{{item}}
{% endfor %} ================================================ FILE: financial_life/templates/html/standard/index.html ================================================ Finanzuebersicht

{{title}}

{{date}}

Accounts

{% for a in accounts %} {% endfor %}
No. name type start value start date
{{ a.index }} {{ a.name }} {{ a.type }} {{ "{:,.2f}".format(a.start_value) }} {{ a.start_date }}

Regular Payments

{% for r in payments_regular %} {% endfor %}
from to interval day start date stop date payment name fixed
{{ r.from_acc }} {{ r.to_acc }} {{ r.interval }} {{ r.day }} {{ r.date_start }} {{ r.date_stop }} {{ r.payment }} {{ r.name }} {{ r.fixed }}

Unique Payments

{% for u in payments_unique %} {% endfor %}
from to date payment name fixed
{{ u.from_acc }} {{ u.to_acc }} {{ u.date }} {{ u.payment }} {{ u.name }} {{ u.fixed }}
================================================ FILE: financial_life/templates/html/standard/render.py ================================================ ''' Created on 03.10.2016 @author: martin ''' # standard libraries import os from datetime import datetime # third-party libraries from jinja2 import Template # own libraries from financial_life.financing import plotting as plt from financial_life.reports import sl path_img = 'img' path_accounts = 'accounts' def render(simulation, output_dir = 'report'): print("Calling render function") template_folder = os.path.dirname(os.path.realpath(__file__)) img_folder = output_dir + sl + path_img + sl accounts_folder = path_accounts + sl print('Template Folder: %s' % template_folder) # makedirs creates also all intermediate folders, therefore, we don't need # to create result_folder explicitely if not os.path.exists(img_folder): os.makedirs(img_folder) if not os.path.exists(output_dir + sl + accounts_folder): os.makedirs(output_dir + sl + accounts_folder) img_data = plt.summary_img(*simulation.reports('yearly'), target = img_folder) data = {} data['title'] = 'Kalkulation: ' + output_dir data['date'] = datetime.now().strftime("%d.%m.%Y - %H:%M:%S") data.update(img_data) data.update(simulation.get_payments_unique_json()) data.update(simulation.get_payments_regular_json()) accounts = simulation.get_accounts_json() links = render_accounts(simulation, template_folder, output_dir, accounts_folder) # get_accounts_json and render_accounts iterate through simulation.account # therefore, the order in both is equal and we can add the link to the # accounts json. this is not the safest way but I did not wanted to put # the get_accounts_json routine into this render-function for a, l in zip(accounts['accounts'], links): a['link'] = l data.update(accounts) index_file = "index.html" with open(template_folder + sl + index_file, 'r') as f: content = f.read() t = Template(content) with open(output_dir + sl + index_file, 'w') as o: o.write(t.render(**data)) def render_accounts(simulation, template_folder, output_dir = 'report', accounts_folder = path_accounts): """ Renders for each account a detailed page with all account-specific data """ accounts = simulation.accounts links = [] # image folder for the account related pictures img_folder = output_dir + sl + accounts_folder + 'img' + sl if not os.path.exists(img_folder): os.makedirs(img_folder) for (i, a) in enumerate(accounts): account_name = a.name account_name.replace(' ', '_') prefix = 'account_details_%03i_%s' % (i, account_name) account_link = accounts_folder + prefix + '.html' print('Render %s' % account_link) links.append(account_link) data = {} data['account_name'] = a.name data['tables'] = a.get_report_json(interval='all') data['backlink'] = '..' + sl + 'index.html' img_data = plt.summary_img(a.report.yearly(), target = img_folder, prefix = prefix) data.update(img_data) template_file = 'account_details.html' with open(template_folder + sl + template_file, 'r') as f: content = f.read() t = Template(content) with open(output_dir + sl + account_link, 'w') as o: o.write(t.render(**data)) return links ================================================ FILE: financial_life/test_general.py ================================================ ''' Created on 12.12.2016 @author: martin ''' # standard libraries from datetime import timedelta, datetime import os import unittest # own libraries from financial_life.financing import accounts as a from financial_life.reports import html class Test(unittest.TestCase): def setUp(self): """ Mostly taken from examples/simple_example.py """ account = a.Bank_Account(amount = 1000, interest = 0.001, name = 'Main account', date=datetime(2016,9, 1)) savings = a.Bank_Account(amount = 5000, interest = 0.013, name = 'Savings', date=datetime(2016,9, 1)) loan = a.Loan(amount = 100000, interest = 0.01, name = 'House Credit', date=datetime(2016,9, 1)) simulation = a.Simulation(account, savings, loan, name = 'Testsimulation', date=datetime(2016,9, 1)) simulation.add_regular(from_acc = 'Income', to_acc = account, payment = 2000, interval = 'monthly', date_start = datetime(2016,9,15), day = 15, name = 'Income') simulation.add_regular(from_acc = account, to_acc = savings, payment = 500, interval = 'monthly', date_start = datetime(2016,9,30), day = 30, name = 'Savings') simulation.add_regular(from_acc = account, to_acc= loan, payment = 1000, interval = 'monthly', date_start = datetime(2016,9,15), day = 15, name = 'Debts', fixed = False, date_stop = lambda cdate: loan.is_finished()) simulation.add_regular(from_acc = account, to_acc= loan, payment = lambda : min(8000, max(0,account.get_account()-4000)), interval = 'yearly', date_start = datetime(2016,11,20), day = 20, name = 'Debts', fixed = False, date_stop = lambda cdate: loan.is_finished()) simulation.simulate(delta=timedelta(days=2000)) self.simulation = simulation self.account = account self.loan = loan def tearDown(self): pass def testGeneral(self): interests = sum(self.account.report.yearly().interest)+sum(self.loan.report.yearly().interest) self.assertTrue(abs(interests - (-3086.08)) < 0.00001) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() ================================================ FILE: requirements.txt ================================================ Jinja2>=2.7.2 matplotlib>=1.5.3 numpy>=1.11.2 pandas>=0.25.3 tabulate>=0.7.5 xlwt ================================================ FILE: setup.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- """ Created on Tue Nov 8 21:26:07 2016 @author: martin """ try: from setuptools import setup have_setuptools = True except ImportError: from distutils.core import setup have_setuptools = False skw = dict( name='financial_life', version='0.9.4', description='A framework for analysing financial products in personalized contexts', author='Martin Pyka', author_email='martin.pyka@gmail.com', maintainer='Martin Pyka', maintainer_email='martin.pyka@gmail.com', url='https://github.com/MartinPyka/financial_life', keywords=["finance", "analysis", "simulation", "loan", "bank"], license="Apache License, Version 2.0", packages=['financial_life', 'financial_life.calendar_help', 'financial_life.examples', 'financial_life.financing', 'financial_life.products.germany.lbs', 'financial_life.reports', 'financial_life.tax.germany', 'financial_life.templates.html.standard', ], package_data={'financial_life': ['templates/html/standard/*.html']} ) if have_setuptools is True: skw['install_requires'] = [ 'Jinja2>=2.7.2,<3', 'matplotlib>=1.3.1,<2', 'numpy>=1.8.1,<2', 'pandas>=0.18.1,<1', 'tabulate>=0.7.5,<1', 'xlwt>=1.2.0', ] setup(**skw) ================================================ FILE: unittests.sh ================================================ #!/bin/bash # run all unittests in this package python3 -m unittest discover -v