├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── financial_functions ├── core.py ├── lambda_handlers.py ├── lib │ └── __init__.py ├── log_helper.py ├── validation_json_schemas.py └── wrapper_handler.py ├── templates ├── effect.yaml ├── fv.yaml ├── fvschedule.yaml ├── irr.yaml ├── mirr.yaml ├── nominal.yaml ├── nper.yaml ├── npv.yaml ├── pmt.yaml ├── ppmt.yaml ├── pv.yaml ├── rate.yaml ├── sln.yaml ├── wrapper.yaml ├── xirr.yaml └── xnpv.yaml └── test ├── __init__.py ├── effect.json ├── fv.json ├── fvschedule.json ├── irr.json ├── mirr.json ├── nominal.json ├── nper.json ├── npv.json ├── pmt.json ├── ppmt.json ├── pv.json ├── rate.json ├── sln.json ├── unit ├── __init__.py ├── test_core.py ├── test_lambda_handlers.py └── test_wrapper_handler.py ├── wrapper-fv.json ├── wrapper-irr.json ├── xirr.json └── xnpv.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .pytest_cache/ 103 | 104 | # vim files 105 | .*.sw[op] 106 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/aws-serverless-financial-functions/issues), or [recently closed](https://github.com/awslabs/aws-serverless-financial-functions/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-serverless-financial-functions/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/aws-serverless-financial-functions/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/sh 2 | PY_VERSION := 3.8 3 | 4 | export PYTHONUNBUFFERED := 1 5 | 6 | BUILD_DIR := dist 7 | TEMPLATES_BUILD_DIR := $(BUILD_DIR)/templates 8 | PACKAGED_TEMPLATES_DIR := $(BUILD_DIR)/packaged_templates 9 | 10 | # user needs to set PACKAGE_BUCKET as env variable 11 | PACKAGE_BUCKET ?= 12 | AWS_DEFAULT_REGION ?= us-east-1 13 | 14 | PYTHON := $(shell /usr/bin/which python$(PY_VERSION)) 15 | 16 | .DEFAULT_GOAL := build 17 | 18 | init: 19 | $(PYTHON) -m pip install pipenv --user 20 | 21 | test: init 22 | pipenv sync --dev 23 | pipenv run py.test -v test/unit 24 | 25 | build: test 26 | 27 | pre-package: init 28 | mkdir -p $(BUILD_DIR) $(TEMPLATES_BUILD_DIR) 29 | cp -r financial_functions $(BUILD_DIR) 30 | cp templates/*.yaml $(TEMPLATES_BUILD_DIR) 31 | 32 | pipenv lock --requirements > $(BUILD_DIR)/requirements.txt 33 | pipenv run pip install -t $(BUILD_DIR)/financial_functions/lib -r $(BUILD_DIR)/requirements.txt 34 | 35 | package: pre-package 36 | mkdir -p $(PACKAGED_TEMPLATES_DIR) 37 | aws cloudformation package --template-file $(SOURCE_TEMPLATE) --s3-bucket $(PACKAGE_BUCKET) --output-template-file $(PACKAGED_TEMPLATES_DIR)/$$(basename $(SOURCE_TEMPLATE)) 38 | 39 | deploy: 40 | aws cloudformation deploy --template-file $(PACKAGED_TEMPLATE) --stack-name $$(basename $(PACKAGED_TEMPLATE) .yaml) --capabilities CAPABILITY_IAM 41 | 42 | set-fv-template: 43 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/fv.yaml) 44 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/fv.yaml) 45 | package-fv: set-fv-template package 46 | deploy-fv: set-fv-template package-fv deploy 47 | 48 | set-fvschedule-template: 49 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/fvschedule.yaml) 50 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/fvschedule.yaml) 51 | package-fvschedule: set-fvschedule-template package 52 | deploy-fvschedule: set-fvschedule-template package-fvschedule deploy 53 | 54 | set-irr-template: 55 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/irr.yaml) 56 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/irr.yaml) 57 | package-irr: set-irr-template package 58 | deploy-irr: set-irr-template package-irr deploy 59 | 60 | set-mirr-template: 61 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/mirr.yaml) 62 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/mirr.yaml) 63 | package-mirr: set-mirr-template package 64 | deploy-mirr: set-mirr-template package-mirr deploy 65 | 66 | set-xirr-template: 67 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/xirr.yaml) 68 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/xirr.yaml) 69 | package-xirr: set-xirr-template package 70 | deploy-xirr: set-xirr-template package-xirr deploy 71 | 72 | set-nper-template: 73 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/nper.yaml) 74 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/nper.yaml) 75 | package-nper: set-nper-template package 76 | deploy-nper: set-nper-template package-nper deploy 77 | 78 | set-npv-template: 79 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/npv.yaml) 80 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/npv.yaml) 81 | package-npv: set-npv-template package 82 | deploy-npv: set-npv-template package-npv deploy 83 | 84 | set-xnpv-template: 85 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/xnpv.yaml) 86 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/xnpv.yaml) 87 | package-xnpv: set-xnpv-template package 88 | deploy-xnpv: set-xnpv-template package-xnpv deploy 89 | 90 | set-pmt-template: 91 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/pmt.yaml) 92 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/pmt.yaml) 93 | package-pmt: set-pmt-template package 94 | deploy-pmt: set-pmt-template package-pmt deploy 95 | 96 | set-ppmt-template: 97 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/ppmt.yaml) 98 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/ppmt.yaml) 99 | package-ppmt: set-ppmt-template package 100 | deploy-ppmt: set-ppmt-template package-ppmt deploy 101 | 102 | set-pv-template: 103 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/pv.yaml) 104 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/pv.yaml) 105 | package-pv: set-pv-template package 106 | deploy-pv: set-pv-template package-pv deploy 107 | 108 | set-rate-template: 109 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/rate.yaml) 110 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/rate.yaml) 111 | package-rate: set-rate-template package 112 | deploy-rate: set-rate-template package-rate deploy 113 | 114 | set-effect-template: 115 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/effect.yaml) 116 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/effect.yaml) 117 | package-effect: set-effect-template package 118 | deploy-effect: set-effect-template package-effect deploy 119 | 120 | set-nominal-template: 121 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/nominal.yaml) 122 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/nominal.yaml) 123 | package-nominal: set-nominal-template package 124 | deploy-nominal: set-nominal-template package-nominal deploy 125 | 126 | set-sln-template: 127 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/sln.yaml) 128 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/sln.yaml) 129 | package-sln: set-sln-template package 130 | deploy-sln: set-sln-template package-sln deploy 131 | 132 | set-wrapper-template: 133 | $(eval SOURCE_TEMPLATE := $(TEMPLATES_BUILD_DIR)/wrapper.yaml) 134 | $(eval PACKAGED_TEMPLATE := $(PACKAGED_TEMPLATES_DIR)/wrapper.yaml) 135 | package-wrapper: set-wrapper-template package 136 | deploy-wrapper: set-wrapper-template package-wrapper deploy 137 | 138 | clean: 139 | rm -rf $(BUILD_DIR) 140 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | numpy_financial = "*" 8 | scipy = "*" 9 | jsonschema = "*" 10 | 11 | [dev-packages] 12 | pytest = "*" 13 | 14 | [requires] 15 | python_version = "3.8" 16 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d7939074824ee345b070c9bd1032bf96eea4adfb33a9eec5eab1e3b63e31515d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "attrs": { 20 | "hashes": [ 21 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 22 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 23 | ], 24 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 25 | "version": "==21.4.0" 26 | }, 27 | "importlib-resources": { 28 | "hashes": [ 29 | "sha256:5481e97fb45af8dcf2f798952625591c58fe599d0735d86b10f54de086a61681", 30 | "sha256:f78a8df21a79bcc30cfd400bdc38f314333de7c0fb619763f6b9dabab8268bb7" 31 | ], 32 | "markers": "python_version < '3.9'", 33 | "version": "==5.9.0" 34 | }, 35 | "jsonschema": { 36 | "hashes": [ 37 | "sha256:73764f461d61eb97a057c929368610a134d1d1fffd858acfe88864ee94f1f1d3", 38 | "sha256:c7448a421b25e424fccfceea86b4e3a8672b4436e1988ccbde92c80828d4f085" 39 | ], 40 | "index": "pypi", 41 | "version": "==4.7.2" 42 | }, 43 | "numpy": { 44 | "hashes": [ 45 | "sha256:1408c3527a74a0209c781ac82bde2182b0f0bf54dea6e6a363fe0cc4488a7ce7", 46 | "sha256:173f28921b15d341afadf6c3898a34f20a0569e4ad5435297ba262ee8941e77b", 47 | "sha256:1865fdf51446839ca3fffaab172461f2b781163f6f395f1aed256b1ddc253622", 48 | "sha256:3119daed207e9410eaf57dcf9591fdc68045f60483d94956bee0bfdcba790953", 49 | "sha256:35590b9c33c0f1c9732b3231bb6a72d1e4f77872390c47d50a615686ae7ed3fd", 50 | "sha256:37e5ebebb0eb54c5b4a9b04e6f3018e16b8ef257d26c8945925ba8105008e645", 51 | "sha256:37ece2bd095e9781a7156852e43d18044fd0d742934833335599c583618181b9", 52 | "sha256:3ab67966c8d45d55a2bdf40701536af6443763907086c0a6d1232688e27e5447", 53 | "sha256:47f10ab202fe4d8495ff484b5561c65dd59177949ca07975663f4494f7269e3e", 54 | "sha256:55df0f7483b822855af67e38fb3a526e787adf189383b4934305565d71c4b148", 55 | "sha256:5d732d17b8a9061540a10fda5bfeabca5785700ab5469a5e9b93aca5e2d3a5fb", 56 | "sha256:68b69f52e6545af010b76516f5daaef6173e73353e3295c5cb9f96c35d755641", 57 | "sha256:7e8229f3687cdadba2c4faef39204feb51ef7c1a9b669247d49a24f3e2e1617c", 58 | "sha256:8002574a6b46ac3b5739a003b5233376aeac5163e5dcd43dd7ad062f3e186129", 59 | "sha256:876f60de09734fbcb4e27a97c9a286b51284df1326b1ac5f1bf0ad3678236b22", 60 | "sha256:9ce242162015b7e88092dccd0e854548c0926b75c7924a3495e02c6067aba1f5", 61 | "sha256:a35c4e64dfca659fe4d0f1421fc0f05b8ed1ca8c46fb73d9e5a7f175f85696bb", 62 | "sha256:aeba539285dcf0a1ba755945865ec61240ede5432df41d6e29fab305f4384db2", 63 | "sha256:b15c3f1ed08df4980e02cc79ee058b788a3d0bef2fb3c9ca90bb8cbd5b8a3a04", 64 | "sha256:c2f91f88230042a130ceb1b496932aa717dcbd665350beb821534c5c7e15881c", 65 | "sha256:d748ef349bfef2e1194b59da37ed5a29c19ea8d7e6342019921ba2ba4fd8b624", 66 | "sha256:e0d7447679ae9a7124385ccf0ea990bb85bb869cef217e2ea6c844b6a6855073" 67 | ], 68 | "markers": "python_version >= '3.8'", 69 | "version": "==1.23.1" 70 | }, 71 | "numpy-financial": { 72 | "hashes": [ 73 | "sha256:bae534b357516f12258862d1f0181d911032d0467f215bfcd1c264b4da579047", 74 | "sha256:f84341bc62b2485d5604a73d5fac7e91975b4b9cd5f4a5a9cf608902ea00cb40" 75 | ], 76 | "index": "pypi", 77 | "version": "==1.0.0" 78 | }, 79 | "pyrsistent": { 80 | "hashes": [ 81 | "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c", 82 | "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc", 83 | "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e", 84 | "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26", 85 | "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec", 86 | "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286", 87 | "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045", 88 | "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec", 89 | "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8", 90 | "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c", 91 | "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca", 92 | "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22", 93 | "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a", 94 | "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96", 95 | "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc", 96 | "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1", 97 | "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07", 98 | "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6", 99 | "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b", 100 | "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5", 101 | "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6" 102 | ], 103 | "markers": "python_version >= '3.7'", 104 | "version": "==0.18.1" 105 | }, 106 | "scipy": { 107 | "hashes": [ 108 | "sha256:02b567e722d62bddd4ac253dafb01ce7ed8742cf8031aea030a41414b86c1125", 109 | "sha256:1166514aa3bbf04cb5941027c6e294a000bba0cf00f5cdac6c77f2dad479b434", 110 | "sha256:1da52b45ce1a24a4a22db6c157c38b39885a990a566748fc904ec9f03ed8c6ba", 111 | "sha256:23b22fbeef3807966ea42d8163322366dd89da9bebdc075da7034cee3a1441ca", 112 | "sha256:28d2cab0c6ac5aa131cc5071a3a1d8e1366dad82288d9ec2ca44df78fb50e649", 113 | "sha256:2ef0fbc8bcf102c1998c1f16f15befe7cffba90895d6e84861cd6c6a33fb54f6", 114 | "sha256:3b69b90c9419884efeffaac2c38376d6ef566e6e730a231e15722b0ab58f0328", 115 | "sha256:4b93ec6f4c3c4d041b26b5f179a6aab8f5045423117ae7a45ba9710301d7e462", 116 | "sha256:4e53a55f6a4f22de01ffe1d2f016e30adedb67a699a310cdcac312806807ca81", 117 | "sha256:6311e3ae9cc75f77c33076cb2794fb0606f14c8f1b1c9ff8ce6005ba2c283621", 118 | "sha256:65b77f20202599c51eb2771d11a6b899b97989159b7975e9b5259594f1d35ef4", 119 | "sha256:6cc6b33139eb63f30725d5f7fa175763dc2df6a8f38ddf8df971f7c345b652dc", 120 | "sha256:70de2f11bf64ca9921fda018864c78af7147025e467ce9f4a11bc877266900a6", 121 | "sha256:70ebc84134cf0c504ce6a5f12d6db92cb2a8a53a49437a6bb4edca0bc101f11c", 122 | "sha256:83606129247e7610b58d0e1e93d2c5133959e9cf93555d3c27e536892f1ba1f2", 123 | "sha256:93d07494a8900d55492401917a119948ed330b8c3f1d700e0b904a578f10ead4", 124 | "sha256:9c4e3ae8a716c8b3151e16c05edb1daf4cb4d866caa385e861556aff41300c14", 125 | "sha256:9dd4012ac599a1e7eb63c114d1eee1bcfc6dc75a29b589ff0ad0bb3d9412034f", 126 | "sha256:9e3fb1b0e896f14a85aa9a28d5f755daaeeb54c897b746df7a55ccb02b340f33", 127 | "sha256:a0aa8220b89b2e3748a2836fbfa116194378910f1a6e78e4675a095bcd2c762d", 128 | "sha256:d3b3c8924252caaffc54d4a99f1360aeec001e61267595561089f8b5900821bb", 129 | "sha256:e013aed00ed776d790be4cb32826adb72799c61e318676172495383ba4570aa4", 130 | "sha256:f3e7a8867f307e3359cc0ed2c63b61a1e33a19080f92fe377bc7d49f646f2ec1" 131 | ], 132 | "index": "pypi", 133 | "version": "==1.8.1" 134 | }, 135 | "zipp": { 136 | "hashes": [ 137 | "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2", 138 | "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009" 139 | ], 140 | "markers": "python_version < '3.10'", 141 | "version": "==3.8.1" 142 | } 143 | }, 144 | "develop": { 145 | "attrs": { 146 | "hashes": [ 147 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 148 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 149 | ], 150 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 151 | "version": "==21.4.0" 152 | }, 153 | "iniconfig": { 154 | "hashes": [ 155 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 156 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 157 | ], 158 | "version": "==1.1.1" 159 | }, 160 | "packaging": { 161 | "hashes": [ 162 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 163 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 164 | ], 165 | "markers": "python_version >= '3.6'", 166 | "version": "==21.3" 167 | }, 168 | "pluggy": { 169 | "hashes": [ 170 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 171 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 172 | ], 173 | "markers": "python_version >= '3.6'", 174 | "version": "==1.0.0" 175 | }, 176 | "py": { 177 | "hashes": [ 178 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 179 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 180 | ], 181 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 182 | "version": "==1.11.0" 183 | }, 184 | "pyparsing": { 185 | "hashes": [ 186 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 187 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 188 | ], 189 | "markers": "python_full_version >= '3.6.8'", 190 | "version": "==3.0.9" 191 | }, 192 | "pytest": { 193 | "hashes": [ 194 | "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", 195 | "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" 196 | ], 197 | "index": "pypi", 198 | "version": "==7.1.2" 199 | }, 200 | "tomli": { 201 | "hashes": [ 202 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 203 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 204 | ], 205 | "markers": "python_version >= '3.7'", 206 | "version": "==2.0.1" 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Serverless Financial Functions 2 | 3 | This is a collection of serverless apps that wrap common financial functions in AWS Lambda functions. The financial functions' names and interfaces are identical to the corresponding functions in Microsoft Excel for convenience. 4 | 5 | In addition to the individual function apps, an API app is included, which stands up an Amazon API Gateway REST endpoint surfacing all of the functions. 6 | 7 | ### Financial Functions 8 | 9 | 1. FV - Returns the future value of an investment based on periodic, constant payments and a constant interest rate. 10 | 1. FVSCHEDULE - Returns the future value of an initial principal after applying a series of compound interest rates. 11 | 1. PV - Returns the present value of an investment: the total amount that a series of future payments is worth now. 12 | 1. NPV - Returns the net present value of an investment based on a discount rate and a series of future payments (negative values) and income (positive values). 13 | 1. XNPV - Returns the net present value for a schedule of cash flows. 14 | 1. PMT - Calculates the payment for a loan based on constant payments and a constant interest rate. 15 | 1. PPMT - Returns the payment on the principal for a given investment based on periodic, constant payments and a constant interest rate. 16 | 1. IRR - Returns the internal rate of return for a series of cash flows. 17 | 1. MIRR - Returns the internal rate of return for a series of periodic cash flows, considering both cost of investment and interest on reinvestment of cash. 18 | 1. XIRR - Returns the internal rate of return for a schedule of cash flows. 19 | 1. NPER - Returns the number of periods for an investment based on periodic, constant payments and a constant interest rate. 20 | 1. RATE - Returns the interest rate per period of a loan or an investment. For example, use 6%/4 for quarterly payments at 6% APR. 21 | 1. EFFECT - Returns the effective annual interest rate. 22 | 1. NOMINAL - Returns the annual nominal interest rate. 23 | 1. SLN - Returns the straight-line depreciation of an asset for one period. 24 | 25 | ## Installation Steps 26 | 27 | 1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and login 28 | 1. Search for the desired financial function application in [the AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications?query=aws-serverless-financial-functions) 29 | 1. Click on the desired financial function application and click "Deploy" 30 | 31 | ## License Summary 32 | 33 | This sample code is made available under a modified MIT license. See the LICENSE file. 34 | -------------------------------------------------------------------------------- /financial_functions/core.py: -------------------------------------------------------------------------------- 1 | # Additional financial functions not already provided by numpy 2 | 3 | import datetime 4 | import functools 5 | from scipy import optimize 6 | 7 | def fvschedule(principal, schedule=[]): 8 | """ 9 | Calculates future value with a variable interest rate schedule. 10 | """ 11 | return functools.reduce(lambda x, y: x + (x * y), schedule, principal) 12 | 13 | def xnpv(rate, values=[], dates=[]): 14 | """ 15 | Calculates the Net Present Value for a schedule of cash flows that is not necessarily periodic. 16 | """ 17 | if len(values) != len(dates): 18 | raise ValueError('values and dates must be the same length') 19 | if sorted(dates) != dates: 20 | raise ValueError('dates must be in chronological order') 21 | 22 | first_date = dates[0] 23 | return sum([value / ((1 + rate) ** ((date - first_date).days/365.0)) for (value, date) in zip(values, dates)]) 24 | 25 | def xirr(values=[], dates=[], guess=0.1): 26 | """ 27 | Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic. 28 | """ 29 | return optimize.newton(lambda r: xnpv(r, values, dates), guess) 30 | 31 | def effect(nominal_rate, npery): 32 | """ 33 | Returns the effective annual interest rate, given the nominal annual interest rate and the number of compounding periods per year. 34 | """ 35 | return ((1 + (nominal_rate / npery)) ** npery) - 1 36 | 37 | def __nroot(value, n): 38 | """ 39 | Returns the nth root of the given value. 40 | """ 41 | return value ** (1.0 / n) 42 | 43 | def nominal(effect_rate, npery): 44 | """ 45 | Returns the nominal annual interest rate, given the effective rate and the number of compounding periods per year. 46 | """ 47 | return (__nroot(effect_rate + 1, npery) - 1) * npery 48 | 49 | def sln(cost, salvage, life): 50 | """ 51 | Returns the straight-line depreciation of an asset for one period. 52 | """ 53 | return (float(cost) - float(salvage)) / float(life) 54 | -------------------------------------------------------------------------------- /financial_functions/lambda_handlers.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | import log_helper 4 | sys.path.append('lib') 5 | import numpy_financial as numpy 6 | from jsonschema import validate 7 | from jsonschema.exceptions import ValidationError 8 | import validation_json_schemas as schemas 9 | import core as ff 10 | from datetime import datetime 11 | 12 | logger = log_helper.getLogger(__name__) 13 | 14 | 15 | def __validate_arguments(function_name, arguments_json, json_schema): 16 | """ 17 | Validate the arguments in the provided JSON against the provided json schema 18 | :param function_name: 19 | :param arguments_json: 20 | :param json_schema: 21 | :return: Dict containing whether the provided json is valid and an error message if validation failed. 22 | """ 23 | try: 24 | validate(arguments_json, json_schema) 25 | return {'isValid': True} 26 | except ValidationError as err: 27 | logger.error("Invalid {} request with args: {}. Exception: {}".format(function_name, arguments_json, err)) 28 | return {'isValid': False, 'error': err.message} 29 | 30 | 31 | def __call_numpy(method, args): 32 | """ 33 | Call a NumPy method with a given set of arguments 34 | :param method: NumPy method to call 35 | :param args: Arguments for the provided NumPy method 36 | :return: Result from NumPy 37 | """ 38 | logger.info("Calling numpy.{} with args: {}".format(method, args)) 39 | return {'result': getattr(numpy, method)(*args)} 40 | 41 | def __call_ff(method, args): 42 | """ 43 | Call a core financial function method with a given set of arguments 44 | :param method: Method to call 45 | :param args: Arguments for the provided method 46 | :return: Calculation result 47 | """ 48 | logger.info("Calling ff.{} with args: {}".format(method, args)) 49 | return {'result': getattr(ff, method)(*args)} 50 | 51 | 52 | def fv_handler(request, context): 53 | """ 54 | Future Value calculation 55 | :param request: Dict containing the parameters to pass to the formula. 56 | :param context: Lambda execution context 57 | :return: Dict with a 'result' entry containing the result of the calculation 58 | """ 59 | logger.info("FV request: {}".format(request)) 60 | 61 | validation_result = __validate_arguments('FV', request, schemas.fv_schema) 62 | if not validation_result.get('isValid'): 63 | return {'error': validation_result.get('error')} 64 | 65 | args = [request['rate'], request['nper'], request.get('pmt', 0), request.get('pv', 0), request.get('type', 0)] 66 | return __call_numpy('fv', args) 67 | 68 | def fvschedule_handler(request, context): 69 | """ 70 | Returns the future value of an initial principal after applying a series of compound interest rates. 71 | :param request: Dict containing the parameters to pass to the formula. 72 | :param context: Lambda execution context 73 | :return: Dict with a 'result' entry containing the result of the calculation 74 | """ 75 | logger.info("FVSCHEDULE request: {}".format(request)) 76 | 77 | validation_result = __validate_arguments('FVSCHEDULE', request, schemas.fvschedule_schema) 78 | if not validation_result.get('isValid'): 79 | return {'error': validation_result.get('error')} 80 | 81 | args = [request['principal'], request.get('schedule', [])] 82 | return __call_ff('fvschedule', args) 83 | 84 | def pv_handler(request, context): 85 | """ 86 | Present Value calculation 87 | :param request: Dict containing the parameters to pass to the formula. 88 | :param context: Lambda execution context 89 | :return: Dict with a 'result' entry containing the result of the calculation 90 | """ 91 | logger.info("PV request: {}".format(request)) 92 | 93 | validation_result = __validate_arguments('PV', request, schemas.pv_schema) 94 | if not validation_result.get('isValid'): 95 | return {'error': validation_result.get('error')} 96 | 97 | args = [request['rate'], request['nper'], request.get('pmt', 0), request.get('fv', 0), request.get('type', 0)] 98 | return __call_numpy('pv', args) 99 | 100 | 101 | def npv_handler(request, context): 102 | """ 103 | Net Present Value of a cash flow series 104 | :param request: Dict containing the parameters to pass to the formula. 105 | :param context: Lambda execution context 106 | :return: Dict with a 'result' entry containing the result of the calculation 107 | """ 108 | logger.info("NPV request: {}".format(request)) 109 | 110 | validation_result = __validate_arguments('NPV', request, schemas.npv_schema) 111 | if not validation_result.get('isValid'): 112 | return {'error': validation_result.get('error')} 113 | 114 | # Need to append a 0 entry to the beginning of the values list for NumPy NPV to align with Excel 115 | # Excel assumes the investment begins one period before the first value cash flow whereas 116 | # NumPy assumes they begin at the same time. 117 | args = [request['rate'], [0] + request['values']] 118 | return __call_numpy('npv', args) 119 | 120 | 121 | def xnpv_handler(request, context): 122 | """ 123 | Net Present Value of a cash flow series that's not necessarily periodic. 124 | :param request: Dict containing the parameters to pass to the formula. 125 | :param context: Lambda execution context 126 | :return: Dict with a 'result' entry containing the result of the calculation 127 | """ 128 | logger.info("XNPV request: {}".format(request)) 129 | 130 | validation_result = __validate_arguments('XNPV', request, schemas.xnpv_schema) 131 | if not validation_result.get('isValid'): 132 | return {'error': validation_result.get('error')} 133 | 134 | if len(request['values']) != len(request['dates']): 135 | return {'error': 'values and dates must have the same length'} 136 | 137 | dates = list(map(lambda s: datetime.strptime(s, '%Y-%m-%d'), request['dates'])) 138 | args = [request['rate'], request['values'], dates] 139 | return __call_ff('xnpv', args) 140 | 141 | 142 | def pmt_handler(request, context): 143 | """ 144 | Compute the payment against loan principal plus interest 145 | :param request: Dict containing the parameters to pass to the formula. 146 | :param context: Lambda execution context 147 | :return: Dict with a 'result' entry containing the result of the calculation 148 | """ 149 | logger.info("PMT request: {}".format(request)) 150 | 151 | validation_result = __validate_arguments('PMT', request, schemas.pmt_schema) 152 | if not validation_result.get('isValid'): 153 | return {'error': validation_result.get('error')} 154 | 155 | args = [request['rate'], request['nper'], request['pv'], request.get('fv', 0), request.get('type', 0)] 156 | return __call_numpy('pmt', args) 157 | 158 | 159 | def ppmt_handler(request, context): 160 | """ 161 | Compute the payment against loan principal 162 | :param request: Dict containing the parameters to pass to the formula. 163 | :param context: Lambda execution context 164 | :return: Dict with a 'result' entry containing the result of the calculation 165 | """ 166 | logger.info("PPMT request: {}".format(request)) 167 | 168 | validation_result = __validate_arguments('PPMT', request, schemas.ppmt_schema) 169 | if not validation_result.get('isValid'): 170 | return {'error': validation_result.get('error')} 171 | 172 | args = [request['rate'], request['per'], request['nper'], request['pv'], request.get('fv', 0), request.get('type', 0)] 173 | return __call_numpy('ppmt', args) 174 | 175 | 176 | def irr_handler(request, context): 177 | """ 178 | Internal Rate of Return calculation. 179 | :param request: Dict containing the parameters to pass to the formula. 180 | :param context: Lambda execution context 181 | :return: Dict with a 'result' entry containing the result of the calculation 182 | """ 183 | logger.info("IRR request: {}".format(request)) 184 | 185 | validation_result = __validate_arguments('IRR', request, schemas.irr_schema) 186 | if not validation_result.get('isValid'): 187 | return {'error': validation_result.get('error')} 188 | 189 | # IRR requires at least one positive and one negative value 190 | sorted_values = sorted(request.get('values')) 191 | values_length = len(request.get('values')) 192 | if sorted_values[0] > 0 or sorted_values[values_length - 1] <= 0: 193 | return {'error': "IRR requires at least one positive and one negative value"} 194 | 195 | args = [request['values']] 196 | return __call_numpy('irr', args) 197 | 198 | 199 | def mirr_handler(request, context): 200 | """ 201 | Modified Internal Rate of Return calculation. 202 | :param request: Dict containing the parameters to pass to the formula. 203 | :param context: Lambda execution context 204 | :return: Dict with a 'result' entry containing the result of the calculation 205 | """ 206 | logger.info("MIRR request: {}".format(request)) 207 | 208 | validation_result = __validate_arguments('MIRR', request, schemas.mirr_schema) 209 | if not validation_result.get('isValid'): 210 | return {'error': validation_result.get('error')} 211 | 212 | # MIRR requires at least one positive and one negative value 213 | sorted_values = sorted(request.get('values')) 214 | values_length = len(request.get('values')) 215 | if sorted_values[0] > 0 or sorted_values[values_length - 1] <= 0: 216 | return {'error': "MIRR requires at least one positive and one negative value"} 217 | 218 | args = [request['values'], request['finance_rate'], request['reinvest_rate']] 219 | return __call_numpy('mirr', args) 220 | 221 | 222 | def xirr_handler(request, context): 223 | """ 224 | Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic. 225 | :param request: Dict containing the parameters to pass to the formula. 226 | :param context: Lambda execution context 227 | :return: Dict with a 'result' entry containing the result of the calculation 228 | """ 229 | logger.info("XIRR request: {}".format(request)) 230 | 231 | validation_result = __validate_arguments('XIRR', request, schemas.xirr_schema) 232 | if not validation_result.get('isValid'): 233 | return {'error': validation_result.get('error')} 234 | 235 | # XIRR requires at least one positive and one negative value 236 | sorted_values = sorted(request.get('values')) 237 | values_length = len(request.get('values')) 238 | if sorted_values[0] > 0 or sorted_values[values_length - 1] <= 0: 239 | return {'error': "XIRR requires at least one positive and one negative value"} 240 | 241 | if len(request['values']) != len(request['dates']): 242 | return {'error': 'values and dates must have the same length'} 243 | 244 | dates = list(map(lambda s: datetime.strptime(s, '%Y-%m-%d'), request['dates'])) 245 | args = [request['values'], dates, request.get('guess', 0.1)] 246 | return __call_ff('xirr', args) 247 | 248 | 249 | def nper_handler(request, context): 250 | """ 251 | Number of periodic payments required to pay off a loan. 252 | :param request: Dict containing the parameters to pass to the formula. 253 | :param context: Lambda execution context 254 | :return: Dict with a 'result' entry containing the result of the calculation 255 | """ 256 | logger.info("NPER request: {}".format(request)) 257 | 258 | validation_result = __validate_arguments('NPER', request, schemas.nper_schema) 259 | if not validation_result.get('isValid'): 260 | return {'error': validation_result.get('error')} 261 | 262 | args = [request['rate'], request.get('pmt', 0), request['pv'], request.get('fv', 0), request.get('type', 0)] 263 | result = __call_numpy('nper', args) 264 | # numpy.nper returns a numpy.ndarray object with the result in it . Need to unwrap the result. 265 | return dict(map(lambda entry: (entry[0], entry[1].item(0)), result.items())) 266 | 267 | 268 | def rate_handler(request, context): 269 | """ 270 | Rate of interest period. 271 | :param request: Dict containing the parameters to pass to the formula. 272 | :param context: Lambda execution context 273 | :return: Dict with a 'result' entry containing the result of the calculation 274 | """ 275 | logger.info("Rate request: {}".format(request)) 276 | 277 | validation_result = __validate_arguments('Rate', request, schemas.rate_schema) 278 | if not validation_result.get('isValid'): 279 | return {'error': validation_result.get('error')} 280 | 281 | args = [request['nper'], request.get('pmt', 0), request['pv'], request.get('fv', 0), request.get('type', 0), request.get('guess', 0.10)] 282 | return __call_numpy('rate', args) 283 | 284 | 285 | def effect_handler(request, context): 286 | """ 287 | Effective annual interest rate 288 | :param request: Dict containing the parameters to pass to the formula. 289 | :param context: Lambda execution context 290 | :return: Dict with a 'result' entry containing the result of the calculation 291 | """ 292 | logger.info("Effect request: {}".format(request)) 293 | 294 | validation_result = __validate_arguments('Effect', request, schemas.effect_schema) 295 | if not validation_result.get('isValid'): 296 | return {'error': validation_result.get('error')} 297 | 298 | args = [request['nominal_rate'], int(request['npery'])] 299 | return __call_ff('effect', args) 300 | 301 | 302 | def nominal_handler(request, context): 303 | """ 304 | Nominal annual interest rate 305 | :param request: Dict containing the parameters to pass to the formula. 306 | :param context: Lambda execution context 307 | :return: Dict with a 'result' entry containing the result of the calculation 308 | """ 309 | logger.info("Nominal request: {}".format(request)) 310 | 311 | validation_result = __validate_arguments('nominal', request, schemas.nominal_schema) 312 | if not validation_result.get('isValid'): 313 | return {'error': validation_result.get('error')} 314 | 315 | args = [request['effect_rate'], int(request['npery'])] 316 | return __call_ff('nominal', args) 317 | 318 | 319 | def sln_handler(request, context): 320 | """ 321 | Straight line depreciation of an asset for one period 322 | :param request: Dict containing the parameters to pass to the formula. 323 | :param context: Lambda execution context 324 | :return: Dict with a 'result' entry containing the result of the calculation 325 | """ 326 | logger.info("SLN request: {}".format(request)) 327 | 328 | validation_result = __validate_arguments('sln', request, schemas.sln_schema) 329 | if not validation_result.get('isValid'): 330 | return {'error': validation_result.get('error')} 331 | if request['life'] == 0: 332 | return {'error': 'life cannot be zero'} 333 | 334 | args = [request['cost'], request['salvage'], request['life']] 335 | return __call_ff('sln', args) 336 | 337 | -------------------------------------------------------------------------------- /financial_functions/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-serverless-financial-functions/f8775151b575db77c2c4bcbe9397d5dca4ab5210/financial_functions/lib/__init__.py -------------------------------------------------------------------------------- /financial_functions/log_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | 5 | def getLogger(name): 6 | """ 7 | Initializes logger with given name. Sets log level based on lambda environment variable value. 8 | """ 9 | # get logger level from function env var and create logger 10 | log_level = os.getenv('LOG_LEVEL', 'INFO') 11 | numeric_level = getattr(logging, log_level.upper(), None) 12 | if not isinstance(numeric_level, int): 13 | raise ValueError('Invalid log level: %s' % log_level) 14 | 15 | logger = logging.getLogger(name) 16 | logger.setLevel(numeric_level) 17 | return logger 18 | -------------------------------------------------------------------------------- /financial_functions/validation_json_schemas.py: -------------------------------------------------------------------------------- 1 | # TODO validate type is in valid set of values for all below 2 | 3 | fv_schema = { 4 | "type": "object", 5 | "properties": { 6 | "rate": { 7 | "type": "number" 8 | }, 9 | "nper": { 10 | "type": "number" 11 | }, 12 | "pmt": { 13 | "type": "number" 14 | }, 15 | "pv": { 16 | "type": "number" 17 | }, 18 | "type": { 19 | "type": "integer", 20 | "enum": [0, 1] 21 | } 22 | }, 23 | "anyOf": [ 24 | { 25 | "required": ["rate", "nper", "pmt"] 26 | }, 27 | { 28 | "required": ["rate", "nper", "pv"] 29 | } 30 | ], 31 | "additionalProperties": False 32 | } 33 | 34 | fvschedule_schema = { 35 | "type": "object", 36 | "properties": { 37 | "principal": { 38 | "type": "number" 39 | }, 40 | "schedule": { 41 | "type": "array", 42 | "items": { 43 | "type": "number" 44 | } 45 | } 46 | }, 47 | "required": ["principal", "schedule"], 48 | "additionalProperties": False 49 | } 50 | 51 | pv_schema = { 52 | "type": "object", 53 | "properties": { 54 | "rate": { 55 | "type": "number" 56 | }, 57 | "nper": { 58 | "type": "number" 59 | }, 60 | "pmt": { 61 | "type": "number" 62 | }, 63 | "fv": { 64 | "type": "number" 65 | }, 66 | "type": { 67 | "type": "integer", 68 | "enum": [0, 1] 69 | } 70 | }, 71 | "anyOf": [ 72 | { 73 | "required": ["rate", "nper", "pmt"] 74 | }, 75 | { 76 | "required": ["rate", "nper", "fv"] 77 | } 78 | ], 79 | "additionalProperties": False 80 | } 81 | 82 | npv_schema = { 83 | "type": "object", 84 | "properties": { 85 | "rate": { 86 | "type": "number" 87 | }, 88 | "values": { 89 | "type": "array", 90 | "items": { 91 | "type": "number", 92 | "minItems": 1 93 | } 94 | } 95 | }, 96 | "required": ["rate", "values"], 97 | "additionalProperties": False 98 | } 99 | 100 | xnpv_schema = { 101 | "type": "object", 102 | "properties": { 103 | "rate": { 104 | "type": "number" 105 | }, 106 | "values": { 107 | "type": "array", 108 | "items": { 109 | "type": "number", 110 | "minItems": 1 111 | } 112 | }, 113 | "dates": { 114 | "type": "array", 115 | "items": { 116 | "type": "string", 117 | "minItems": 1, 118 | "pattern": "^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$" 119 | } 120 | } 121 | }, 122 | "required": ["rate", "values", "dates"], 123 | "additionalProperties": False 124 | } 125 | 126 | pmt_schema = { 127 | "type": "object", 128 | "properties": { 129 | "rate": { 130 | "type": "number" 131 | }, 132 | "nper": { 133 | "type": "number" 134 | }, 135 | "pv": { 136 | "type": "number" 137 | }, 138 | "fv": { 139 | "type": "number" 140 | }, 141 | "type": { 142 | "type": "integer", 143 | "enum": [0, 1] 144 | } 145 | }, 146 | "required": ["rate", "nper", "pv"], 147 | "additionalProperties": False 148 | } 149 | 150 | ppmt_schema = { 151 | "type": "object", 152 | "properties": { 153 | "rate": { 154 | "type": "number" 155 | }, 156 | "per": { 157 | "type": "number", 158 | "minimum": 1 159 | }, 160 | "nper": { 161 | "type": "number" 162 | }, 163 | "pv": { 164 | "type": "number" 165 | }, 166 | "fv": { 167 | "type": "number" 168 | }, 169 | "type": { 170 | "type": "integer", 171 | "enum": [0, 1] 172 | } 173 | }, 174 | "required": ["rate", "per", "nper", "pv"], 175 | "additionalProperties": False 176 | } 177 | 178 | irr_schema = { 179 | "type": "object", 180 | "properties": { 181 | "values": { 182 | "type": "array", 183 | "items": { 184 | "type": "number" 185 | }, 186 | "minItems": 2 187 | } 188 | }, 189 | "required": ["values"], 190 | "additionalProperties": False 191 | } 192 | 193 | mirr_schema = { 194 | "type": "object", 195 | "properties": { 196 | "values": { 197 | "type": "array", 198 | "items": { 199 | "type": "number" 200 | }, 201 | "minItems": 2 202 | }, 203 | "finance_rate": { 204 | "type": "number" 205 | }, 206 | "reinvest_rate": { 207 | "type": "number" 208 | } 209 | }, 210 | "required": ["values", "finance_rate", "reinvest_rate"], 211 | "additionalProperties": False 212 | } 213 | 214 | xirr_schema = { 215 | "type": "object", 216 | "properties": { 217 | "values": { 218 | "type": "array", 219 | "items": { 220 | "type": "number" 221 | }, 222 | "minItems": 2 223 | }, 224 | "dates": { 225 | "type": "array", 226 | "items": { 227 | "type": "string", 228 | "minItems": 2, 229 | "pattern": "^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$" 230 | } 231 | }, 232 | "guess": { 233 | "type": "number" 234 | } 235 | }, 236 | "required": ["values", "dates"], 237 | "additionalProperties": False 238 | } 239 | 240 | nper_schema = { 241 | "type": "object", 242 | "properties": { 243 | "rate": { 244 | "type": "number" 245 | }, 246 | "pmt": { 247 | "type": "number" 248 | }, 249 | "pv": { 250 | "type": "number" 251 | }, 252 | "fv": { 253 | "type": "number" 254 | }, 255 | "type": { 256 | "type": "integer", 257 | "enum": [0, 1] 258 | } 259 | }, 260 | "anyOf": [ 261 | { 262 | "required": ["rate", "pmt", "pv"] 263 | }, 264 | { 265 | "required": ["rate", "pv", "fv"] 266 | } 267 | ], 268 | "additionalProperties": False 269 | } 270 | 271 | rate_schema = { 272 | "type": "object", 273 | "properties": { 274 | "nper": { 275 | "type": "number" 276 | }, 277 | "pmt": { 278 | "type": "number" 279 | }, 280 | "pv": { 281 | "type": "number" 282 | }, 283 | "fv": { 284 | "type": "number" 285 | }, 286 | "type": { 287 | "type": "integer", 288 | "enum": [0, 1] 289 | }, 290 | "guess": { 291 | "type": "number" 292 | } 293 | }, 294 | "anyOf": [ 295 | { 296 | "required": ["nper", "pmt", "pv"] 297 | }, 298 | { 299 | "required": ["nper", "pv", "fv"] 300 | } 301 | ], 302 | "additionalProperties": False 303 | } 304 | 305 | effect_schema = { 306 | "type": "object", 307 | "properties": { 308 | "nominal_rate": { 309 | "type": "number" 310 | }, 311 | "npery": { 312 | "type": "number", 313 | "minimum": 1 314 | } 315 | }, 316 | "required": ["nominal_rate", "npery"], 317 | "additionalProperties": False 318 | } 319 | 320 | nominal_schema = { 321 | "type": "object", 322 | "properties": { 323 | "effect_rate": { 324 | "type": "number" 325 | }, 326 | "npery": { 327 | "type": "number", 328 | "minimum": 1 329 | } 330 | }, 331 | "required": ["effect_rate", "npery"], 332 | "additionalProperties": False 333 | } 334 | 335 | sln_schema = { 336 | "type": "object", 337 | "properties": { 338 | "cost": { 339 | "type": "number" 340 | }, 341 | "salvage": { 342 | "type": "number" 343 | }, 344 | "life": { 345 | "type": "number" 346 | } 347 | }, 348 | "required": ["cost", "salvage", "life"], 349 | "additionalProperties": False 350 | } 351 | 352 | wrapper_schema = { 353 | "type": "object", 354 | "properties": { 355 | "function_name": { 356 | "type": "string" 357 | }, 358 | "args": { 359 | "type": "object" 360 | } 361 | }, 362 | "required": ["function_name", "args"], 363 | "additionalProperties": False 364 | } 365 | -------------------------------------------------------------------------------- /financial_functions/wrapper_handler.py: -------------------------------------------------------------------------------- 1 | import lambda_handlers as handlers 2 | import validation_json_schemas as schemas 3 | from jsonschema import validate 4 | from jsonschema.exceptions import ValidationError 5 | import log_helper 6 | import sys 7 | 8 | logger = log_helper.getLogger(__name__) 9 | 10 | def financial_functions_handler(request, context): 11 | """ 12 | This function takes in an arbritary financial function and its parameters as inputs and returns the result of that calculation 13 | :param request: Dict containing the financial function name (function_name) and its parameters (args) 14 | :param context: Lambda execution context 15 | :return: Dict with a 'result' entry containing the result of the calculation 16 | """ 17 | logger.info("financial function request: {}".format(request)) 18 | 19 | try: 20 | validate(request, schemas.wrapper_schema) 21 | except ValidationError as err: 22 | logger.info("Invalid request: {}. Exception: {}".format(request, err)) 23 | return {'error': err.message} 24 | 25 | function_name = request['function_name'] 26 | function_handler_name = function_name + "_handler" 27 | if hasattr(handlers, function_handler_name): 28 | return getattr(handlers, function_handler_name)(request['args'], context) 29 | else: 30 | return {"error": "Invalid function name: " + function_name + ". Please see documentation for help on supported functions"} 31 | -------------------------------------------------------------------------------- /templates/effect.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Effective annual interest rate 6 | EFFECT: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.effect_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/fv.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Future Value financial function 6 | FV: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.fv_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/fvschedule.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Future Value financial function 6 | FVSCHEDULE: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.fvschedule_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/irr.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Internal Rate of Return financial function 6 | IRR: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.irr_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/mirr.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Modified Internal Rate of Return financial function 6 | MIRR: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.mirr_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/nominal.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Nominal annual interest rate 6 | NOMINAL: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.nominal_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/nper.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Number of Periodic Payments financial function 6 | NPER: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.nper_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/npv.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Net Present Value financial function 6 | NPV: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.npv_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/pmt.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Loan payment financial function 6 | PMT: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.pmt_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/ppmt.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Payment on loan principal financial function 6 | PPMT: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.ppmt_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/pv.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Present Value financial function 6 | PV: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.pv_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/rate.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Interest rate calculation financial function 6 | RATE: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.rate_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/sln.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Straight line depreciation for one period 6 | SLN: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.sln_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/wrapper.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Wrapper function for all financial functions 6 | WRAPPER: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'wrapper_handler.financial_functions_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/xirr.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Internal Rate of Return for a schedule of cash flows that is not necessarily periodic 6 | XIRR: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.xirr_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /templates/xnpv.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Resources: 5 | # Net Present Value financial function 6 | XNPV: 7 | Type: 'AWS::Serverless::Function' 8 | Properties: 9 | Handler: 'lambda_handlers.xnpv_handler' 10 | CodeUri: '../financial_functions' 11 | Runtime: 'python3.8' 12 | Timeout: 30 13 | MemorySize: 256 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: INFO 17 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-serverless-financial-functions/f8775151b575db77c2c4bcbe9397d5dca4ab5210/test/__init__.py -------------------------------------------------------------------------------- /test/effect.json: -------------------------------------------------------------------------------- 1 | { 2 | "nominal_rate": 0.12, 3 | "npery": 12 4 | } -------------------------------------------------------------------------------- /test/fv.json: -------------------------------------------------------------------------------- 1 | { 2 | "rate": 0.004166666666667, 3 | "nper": 120, 4 | "pmt": -100, 5 | "pv": -100 6 | } -------------------------------------------------------------------------------- /test/fvschedule.json: -------------------------------------------------------------------------------- 1 | { 2 | "principal": 10000, 3 | "schedule": [0.05, 0.05, 0.035, 0.035, 0.035] 4 | } 5 | -------------------------------------------------------------------------------- /test/irr.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [-100, 39, 59, 55, 20] 3 | } -------------------------------------------------------------------------------- /test/mirr.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [-1000, 300, 400, 400, 300], 3 | "finance_rate": 0.12, 4 | "reinvest_rate": 0.10 5 | } -------------------------------------------------------------------------------- /test/nominal.json: -------------------------------------------------------------------------------- 1 | { 2 | "effect_rate": 0.12, 3 | "npery": 12 4 | } -------------------------------------------------------------------------------- /test/nper.json: -------------------------------------------------------------------------------- 1 | { 2 | "rate": 0.005833333333333, 3 | "pmt": -150, 4 | "pv": 8000 5 | } -------------------------------------------------------------------------------- /test/npv.json: -------------------------------------------------------------------------------- 1 | { 2 | "rate": 0.281, 3 | "values": [-100, 39, 59, 55, 20] 4 | } -------------------------------------------------------------------------------- /test/pmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "rate": 0.00625, 3 | "nper": 180, 4 | "pv": 200000 5 | } -------------------------------------------------------------------------------- /test/ppmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "rate": 0.10, 3 | "per": 1, 4 | "nper": 3, 5 | "pv": 1000 6 | } -------------------------------------------------------------------------------- /test/pv.json: -------------------------------------------------------------------------------- 1 | { 2 | "rate": 0.004166666666666666, 3 | "nper": 120, 4 | "pmt": -100, 5 | "fv": 15692.93 6 | } -------------------------------------------------------------------------------- /test/rate.json: -------------------------------------------------------------------------------- 1 | { 2 | "nper": 6, 3 | "pmt": -200, 4 | "pv": 1000, 5 | "fv": 0.10 6 | } -------------------------------------------------------------------------------- /test/sln.json: -------------------------------------------------------------------------------- 1 | { 2 | "cost": 5000, 3 | "salvage": 300, 4 | "life": 10 5 | } -------------------------------------------------------------------------------- /test/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # make sure we can find the app code 2 | import sys, os 3 | my_path = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.insert(0, my_path + '/../../financial_functions/') 5 | -------------------------------------------------------------------------------- /test/unit/test_core.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from datetime import date 4 | import core as ff 5 | 6 | def test_fvschedule(): 7 | assert ff.fvschedule(10000, [0.05, 0.05, 0.035, 0.035, 0.035]) == 12223.614571875 8 | assert ff.fvschedule(100, [0.04, 0.06, 0.05]) == 115.752 9 | 10 | def test_xnpv(): 11 | assert ff.xnpv( 12 | 0.05, 13 | [-10000, 2000, 2400, 2900, 3500, 4100], 14 | [date(2016, 1, 1), date(2016, 2, 1), date(2016, 5, 1), date(2016, 7, 1), date(2016, 9, 1), date(2017, 1, 1)] 15 | ) == 4475.448794482614 16 | 17 | assert ff.xnpv( 18 | 0.05, 19 | [-10000, 2000, 2400, 2900, 3500, 4100, 5300], 20 | [date(2016, 1, 1), date(2016, 2, 1), date(2016, 5, 1), date(2016, 7, 1), date(2016, 9, 1), date(2017, 1, 1), date(2017, 2, 3)] 21 | ) == 9500.179287007002 22 | 23 | assert ff.xnpv( 24 | 0.05, 25 | [-1000, 300, 400, 400, 300], 26 | [date(2011, 12, 1), date(2012, 1, 1), date(2013, 2, 1), date(2014, 3, 1), date(2015, 4, 1)] 27 | ) == 289.90172260419456 28 | 29 | def test_xnpv_mismatched_lists(): 30 | with pytest.raises(ValueError): 31 | ff.xnpv(0.05, [-100], []) 32 | 33 | def test_xnpv_dates_not_chronological_order(): 34 | with pytest.raises(ValueError): 35 | ff.xnpv( 36 | 0.05, 37 | [-10000, 2000], 38 | [date(2016, 2, 1), date(2016, 1, 1)]) 39 | 40 | def test_xirr(): 41 | assert ff.xirr( 42 | [-100, 20, 40, 25], 43 | [date(2016, 1, 1), date(2016, 4, 1), date(2016, 10, 1), date(2017, 2, 1)] 44 | ) == -0.19674386129832788 45 | 46 | assert ff.xirr( 47 | [-100, 20, 40, 25, 8, 15], 48 | [date(2016, 1, 1), date(2016, 4, 1), date(2016, 10, 1), date(2017, 2, 1), date(2017, 3, 1), date(2017, 6, 1)] 49 | ) == 0.09443907444452011 50 | 51 | assert ff.xirr( 52 | [-1000, 300, 400, 400, 300], 53 | [date(2011, 12, 1), date(2012, 1, 1), date(2013, 2, 1), date(2014, 3, 1), date(2015, 4, 1)], 54 | 0.1 55 | ) == 0.23860325587216993 56 | 57 | def test_xirr_mismatched_lists(): 58 | with pytest.raises(ValueError): 59 | ff.xirr([-100], []) 60 | 61 | def test_xirr_dates_not_chronological_order(): 62 | with pytest.raises(ValueError): 63 | ff.xirr( 64 | [-100, 20], 65 | [date(2016, 4, 1), date(2016, 1, 1)]) 66 | 67 | def test_effect(): 68 | assert ff.effect(.12, 12) == 0.12682503013196977 69 | assert ff.effect(.10, 4) == 0.10381289062499954 70 | assert ff.effect(.10, 2) == 0.10250000000000004 71 | assert ff.effect(.025, 2) == 0.02515624999999999 72 | 73 | def test_nominal(): 74 | assert ff.nominal(.12, 12) == 0.11386551521499655 75 | assert ff.nominal(.10, 4) == 0.09645475633778045 76 | assert ff.nominal(.10, 2) == 0.09761769634030326 77 | assert ff.nominal(.025, 12) == 0.02471803523811289 78 | 79 | def test_sln(): 80 | assert ff.sln(5000, 300, 10) == 470 81 | assert ff.sln(10000, 1000, 5) == 1800 82 | assert ff.sln(500, 100, 8) == 50 83 | assert ff.sln(1200, 200, 6) == 166.66666666666666 84 | -------------------------------------------------------------------------------- /test/unit/test_lambda_handlers.py: -------------------------------------------------------------------------------- 1 | import lambda_handlers as handlers 2 | 3 | REQUIRED_PROPERTY_ERR = "'{}' is a required property" 4 | INCORRECT_TYPE_ERR = "'{}' is not of type '{}'" 5 | 6 | def test_fv_handler(): 7 | # TODO test data types 8 | # rate, nper, pmt 9 | response = handlers.fv_handler({ 10 | "rate": 0.004166666666667, 11 | "nper": 120, 12 | "pmt": -100 13 | }, None) 14 | assert 'result' in response 15 | assert round(response.get('result'), 6) == 15528.227945 16 | 17 | # rate, nper, pv 18 | response = handlers.fv_handler({ 19 | "rate": 0.004166666666667, 20 | "nper": 120, 21 | "pv": -100 22 | }, None) 23 | assert 'result' in response 24 | assert round(response.get('result'), 6) == 164.700950 25 | 26 | # rate, nper, pmt, pv 27 | response = handlers.fv_handler({ 28 | "rate": 0.004166666666667, 29 | "nper": 120, 30 | "pmt": -100, 31 | "pv": -100 32 | }, None) 33 | assert 'result' in response 34 | assert round(response.get('result'), 6) == 15692.928894 35 | 36 | # rate, nper, pmt, pv 37 | response = handlers.fv_handler({ 38 | "rate": 0.004166666666667, 39 | "nper": 120, 40 | "pmt": -100, 41 | "pv": -100, 42 | "type": 1 43 | }, None) 44 | assert 'result' in response 45 | assert round(response.get('result'), 6) == 15757.629844 46 | 47 | 48 | def test_fv_missing_rate(): 49 | response = handlers.fv_handler({ 50 | "nper": 120, 51 | "pmt": -100, 52 | "pv": -100 53 | }, None) 54 | 55 | assert 'error' in response 56 | 57 | 58 | def test_fv_missing_nper(): 59 | response = handlers.fv_handler({ 60 | "rate": 0.004166666666667, 61 | "pmt": -100, 62 | "pv": -100 63 | }, None) 64 | 65 | assert 'error' in response 66 | 67 | 68 | def test_fvschedule_handler(): 69 | response = handlers.fvschedule_handler({ 70 | "principal": 10000, 71 | "schedule": [0.05, 0.05, 0.035, 0.035, 0.035] 72 | }, None) 73 | assert 'result' in response 74 | assert round(response.get('result'), 6) == 12223.614572 75 | 76 | 77 | def test_fvschedule_missing_principal(): 78 | response = handlers.fvschedule_handler({}, None) 79 | 80 | assert 'error' in response 81 | 82 | 83 | def test_fvschedule_missing_schedule(): 84 | response = handlers.fvschedule_handler({ 85 | "principal": 10000 86 | }, None) 87 | 88 | assert 'error' in response 89 | 90 | 91 | def test_pv_handler(): 92 | # TODO test data types 93 | response = handlers.pv_handler({ 94 | "rate": 0.004166666666666666, 95 | "nper": 120, 96 | "pmt": -100 97 | }, None) 98 | assert 'result' in response 99 | assert round(response.get('result'), 6) == 9428.135033 100 | 101 | response = handlers.pv_handler({ 102 | "rate": 0.004166666666666666, 103 | "nper": 120, 104 | "fv": 15692.93 105 | }, None) 106 | assert 'result' in response 107 | assert round(response.get('result'), 6) == -9528.135704 108 | 109 | response = handlers.pv_handler({ 110 | "rate": 0.004166666666666666, 111 | "nper": 120, 112 | "pmt": -100, 113 | "fv": 15692.93 114 | }, None) 115 | assert 'result' in response 116 | assert round(response.get('result'), 6) == -100.000671 117 | 118 | response = handlers.pv_handler({ 119 | "rate": 0.004166666666666666, 120 | "nper": 120, 121 | "pmt": -100, 122 | "fv": 15692.93, 123 | "type": 1 124 | }, None) 125 | assert 'result' in response 126 | assert round(response.get('result'), 6) == -60.716775 127 | 128 | 129 | def test_pv_missing_rate(): 130 | response = handlers.pv_handler({ 131 | "nper": 120, 132 | "pmt": -100, 133 | "fv": 15692.93 134 | }, None) 135 | 136 | assert 'error' in response 137 | 138 | 139 | def test_pv_missing_nper(): 140 | response = handlers.pv_handler({ 141 | "rate": 0.004166666666666666, 142 | "pmt": -100, 143 | "fv": 15692.93 144 | }, None) 145 | 146 | assert 'error' in response 147 | 148 | 149 | def test_irr_handler(): 150 | # TODO test data types & empty values array 151 | response = handlers.irr_handler({ 152 | "values": [-100, 39, 59, 55, 20] 153 | }, None) 154 | assert 'result' in response 155 | assert round(response.get('result'), 5) == 0.28095 156 | 157 | 158 | def test_irr_missing_values(): 159 | response = handlers.irr_handler({}, None) 160 | 161 | assert 'error' in response 162 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("values") 163 | 164 | 165 | def test_irr_values_wrong_type(): 166 | response = handlers.irr_handler({ 167 | "values": ["test1", "test2"] 168 | }, None) 169 | 170 | assert 'error' in response 171 | assert response.get('error') == INCORRECT_TYPE_ERR.format("test1", "number") 172 | 173 | 174 | def test_irr_values_too_few(): 175 | response = handlers.irr_handler({ 176 | "values": [100] 177 | }, None) 178 | 179 | assert 'error' in response 180 | assert "is too short" in response.get('error') 181 | 182 | 183 | def test_irr_values_missing_pos(): 184 | response = handlers.irr_handler({ 185 | "values": [-100, -200] 186 | }, None) 187 | 188 | assert 'error' in response 189 | assert response.get('error') == "IRR requires at least one positive and one negative value" 190 | 191 | 192 | def test_irr_values_missing_neg(): 193 | response = handlers.irr_handler({ 194 | "values": [100, 200] 195 | }, None) 196 | 197 | assert 'error' in response 198 | assert response.get('error') == "IRR requires at least one positive and one negative value" 199 | 200 | 201 | def test_mirr_handler(): 202 | # TODO test data types & empty values array 203 | response = handlers.mirr_handler({ 204 | "values": [-1000, 300, 400, 400, 300], 205 | "finance_rate": 0.12, 206 | "reinvest_rate": 0.10 207 | }, None) 208 | assert 'result' in response 209 | assert round(response.get('result'), 5) == 0.12876 210 | 211 | 212 | def test_mirr_missing_values(): 213 | response = handlers.mirr_handler({ 214 | "finance_rate": 0.12, 215 | "reinvest_rate": 0.10 216 | }, None) 217 | 218 | assert 'error' in response 219 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("values") 220 | 221 | 222 | def test_mirr_missing_finance_rate(): 223 | response = handlers.mirr_handler({ 224 | "values": [-1000, 300, 400, 400, 300], 225 | "reinvest_rate": 0.10 226 | }, None) 227 | 228 | assert 'error' in response 229 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("finance_rate") 230 | 231 | 232 | def test_mirr_missing_reinvest_rate(): 233 | response = handlers.mirr_handler({ 234 | "values": [-1000, 300, 400, 400, 300], 235 | "finance_rate": 0.12, 236 | }, None) 237 | 238 | assert 'error' in response 239 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("reinvest_rate") 240 | 241 | 242 | def test_mirr_values_too_few(): 243 | response = handlers.mirr_handler({ 244 | "values": [100], 245 | "reinvest_rate": 0.10, 246 | "finance_rate": 0.12 247 | }, None) 248 | 249 | assert 'error' in response 250 | assert "is too short" in response.get('error') 251 | 252 | 253 | def test_mirr_values_missing_pos(): 254 | response = handlers.mirr_handler({ 255 | "values": [-100, -200], 256 | "reinvest_rate": 0.10, 257 | "finance_rate": 0.12 258 | }, None) 259 | 260 | assert 'error' in response 261 | assert response.get('error') == "MIRR requires at least one positive and one negative value" 262 | 263 | 264 | def test_mirr_values_missing_neg(): 265 | response = handlers.mirr_handler({ 266 | "values": [100, 200], 267 | "reinvest_rate": 0.10, 268 | "finance_rate": 0.12 269 | }, None) 270 | 271 | assert 'error' in response 272 | assert response.get('error') == "MIRR requires at least one positive and one negative value" 273 | 274 | 275 | def test_xirr_handler(): 276 | response = handlers.xirr_handler({ 277 | "values": [-100, 20, 40, 25], 278 | "dates": ['2016-01-01', '2016-4-1', '2016-10-1', '2017-2-1'], 279 | "guess": 0.1 280 | }, None) 281 | assert 'result' in response 282 | assert round(response.get('result'), 5) == -0.19674 283 | 284 | 285 | def test_xirr_missing_values(): 286 | response = handlers.xirr_handler({ 287 | "dates": ['2016-01-01', '2016-4-1', '2016-10-1', '2017-2-1'], 288 | "guess": 0.1 289 | }, None) 290 | 291 | assert 'error' in response 292 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("values") 293 | 294 | 295 | def test_xirr_missing_dates(): 296 | response = handlers.xirr_handler({ 297 | "values": [-100, 20, 40, 25], 298 | "guess": 0.1 299 | }, None) 300 | 301 | assert 'error' in response 302 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("dates") 303 | 304 | 305 | def test_xirr_missing_guess(): 306 | response = handlers.xirr_handler({ 307 | "values": [-100, 20, 40, 25], 308 | "dates": ['2016-01-01', '2016-4-1', '2016-10-1', '2017-2-1'] 309 | }, None) 310 | 311 | assert 'result' in response 312 | assert round(response.get('result'), 5) == -0.19674 313 | 314 | 315 | def test_xirr_values_dates_different_length(): 316 | response = handlers.xirr_handler({ 317 | "values": [-100, 20, 40], 318 | "dates": ['2016-01-01', '2016-4-1', '2016-10-1', '2017-2-1'] 319 | }, None) 320 | 321 | assert 'error' in response 322 | 323 | 324 | def test_xirr_no_negative_value(): 325 | response = handlers.xirr_handler({ 326 | "values": [100, 20, 40, 25], 327 | "dates": ['2016-01-01', '2016-4-1', '2016-10-1', '2017-2-1'] 328 | }, None) 329 | 330 | assert 'error' in response 331 | 332 | 333 | def test_nper_handler(): 334 | # TODO test data types 335 | response = handlers.nper_handler({ 336 | "rate": 0.005833333333333, 337 | "pmt": -150, 338 | "pv": 8000 339 | }, None) 340 | assert 'result' in response 341 | assert round(response.get('result'), 5) == 64.07335 342 | 343 | # TODO Excel docs claim this isn't supported but Excel produces a value... 344 | response = handlers.nper_handler({ 345 | "rate": 0.005833333333333, 346 | "pv": 8000, 347 | "fv": -100 348 | }, None) 349 | assert 'result' in response 350 | assert round(response.get('result'), 5) == -753.39346 351 | 352 | response = handlers.nper_handler({ 353 | "rate": 0.005833333333333, 354 | "pmt": -150, 355 | "pv": 8000, 356 | "fv": -100 357 | }, None) 358 | assert 'result' in response 359 | assert round(response.get('result'), 5) == 63.40344 360 | 361 | response = handlers.nper_handler({ 362 | "rate": 0.005833333333333, 363 | "pmt": -150, 364 | "pv": 8000, 365 | "fv": -100, 366 | "type": 1 367 | }, None) 368 | assert 'result' in response 369 | assert round(response.get('result'), 5) == 62.95762 370 | 371 | 372 | def test_nper_missing_rate(): 373 | response = handlers.nper_handler({ 374 | "pmt": -150, 375 | "pv": 8000 376 | }, None) 377 | 378 | assert 'error' in response 379 | 380 | 381 | def test_nper_missing_pmt(): 382 | response = handlers.nper_handler({ 383 | "rate": 0.005833333333333, 384 | "pv": 8000 385 | }, None) 386 | 387 | assert 'error' in response 388 | 389 | 390 | def test_nper_missing_pv(): 391 | response = handlers.nper_handler({ 392 | "rate": 0.005833333333333, 393 | "pmt": -150, 394 | }, None) 395 | 396 | assert 'error' in response 397 | 398 | 399 | def test_npv_handler(): 400 | # TODO test data types 401 | 402 | response = handlers.npv_handler({ 403 | "rate": 0.1, 404 | "values": [-10000, 3000, 4200, 6800] 405 | }, None) 406 | assert 'result' in response 407 | assert round(response.get('result'), 5) == 1188.44341 408 | 409 | 410 | def test_npv_missing_rate(): 411 | response = handlers.npv_handler({ 412 | "values": [-100, 39, 59, 55, 20] 413 | }, None) 414 | 415 | assert 'error' in response 416 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("rate") 417 | 418 | 419 | def test_npv_missing_values(): 420 | response = handlers.npv_handler({ 421 | "rate": 0.281, 422 | }, None) 423 | 424 | assert 'error' in response 425 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("values") 426 | 427 | 428 | 429 | def test_xnpv_handler(): 430 | response = handlers.xnpv_handler({ 431 | "rate": 0.05, 432 | "values": [-10000, 2000, 2400, 2900, 3500, 4100], 433 | "dates": ['2016-1-1', '2016-2-1', '2016-5-1', '2016-7-1', '2016-9-1', '2017-1-1'] 434 | }, None) 435 | assert 'result' in response 436 | assert round(response.get('result'), 5) == 4475.44879 437 | 438 | 439 | def test_xnpv_missing_rate(): 440 | response = handlers.xnpv_handler({ 441 | "values": [-10000, 2000, 2400, 2900, 3500, 4100], 442 | "dates": ['2016-1-1', '2016-2-1', '2016-5-1', '2016-7-1', '2016-9-1', '2017-1-1'] 443 | }, None) 444 | 445 | assert 'error' in response 446 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("rate") 447 | 448 | 449 | def test_xnpv_missing_values(): 450 | response = handlers.xnpv_handler({ 451 | "rate": 0.05, 452 | "dates": ['2016-01-01', '2016-2-1', '2016-5-1', '2016-7-1', '2016-9-1', '2017-1-1'] 453 | }, None) 454 | 455 | assert 'error' in response 456 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("values") 457 | 458 | 459 | def test_xnpv_missing_dates(): 460 | response = handlers.xnpv_handler({ 461 | "rate": 0.05, 462 | "values": [-10000, 2000, 2400, 2900, 3500, 4100] 463 | }, None) 464 | 465 | assert 'error' in response 466 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("dates") 467 | 468 | 469 | def test_xnpv_values_dates_different_lengths(): 470 | response = handlers.xnpv_handler({ 471 | "rate": 0.05, 472 | "values": [-10000, 2000], 473 | "dates": ['2016-1-1', '2016-2-1', '2016-5-1'] 474 | }, None) 475 | 476 | assert 'error' in response 477 | 478 | 479 | def test_xnpv_invalid_date(): 480 | response = handlers.xnpv_handler({ 481 | "rate": 0.05, 482 | "values": [-10000], 483 | "dates": ['bogus'] 484 | }, None) 485 | 486 | assert 'error' in response 487 | 488 | 489 | def test_pmt_handler(): 490 | # TODO test data types 491 | response = handlers.pmt_handler({ 492 | "rate": 0.00625, 493 | "nper": 180, 494 | "pv": 200000 495 | }, None) 496 | assert 'result' in response 497 | assert round(response.get('result'), 6) == -1854.02472 498 | 499 | response = handlers.pmt_handler({ 500 | "rate": 0.00625, 501 | "nper": 180, 502 | "pv": 200000, 503 | "fv": 300000 504 | }, None) 505 | assert 'result' in response 506 | assert round(response.get('result'), 6) == -2760.06180 507 | 508 | response = handlers.pmt_handler({ 509 | "rate": 0.00625, 510 | "nper": 180, 511 | "pv": 200000, 512 | "fv": 300000, 513 | "type": 1 514 | }, None) 515 | assert 'result' in response 516 | assert round(response.get('result'), 6) == -2742.918559 517 | 518 | 519 | def test_pmt_missing_rate(): 520 | response = handlers.pmt_handler({ 521 | "nper": 180, 522 | "pv": 200000 523 | }, None) 524 | 525 | assert 'error' in response 526 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("rate") 527 | 528 | 529 | def test_pmt_missing_nper(): 530 | response = handlers.pmt_handler({ 531 | "rate": 0.00625, 532 | "pv": 200000 533 | }, None) 534 | 535 | assert 'error' in response 536 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("nper") 537 | 538 | 539 | def test_pmt_missing_pv(): 540 | response = handlers.pmt_handler({ 541 | "rate": 0.00625, 542 | "nper": 180, 543 | }, None) 544 | 545 | assert 'error' in response 546 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("pv") 547 | 548 | 549 | def test_ppmt_handler(): 550 | # TODO test data types 551 | response = handlers.ppmt_handler({ 552 | "rate": 0.10, 553 | "per": 1, 554 | "nper": 3, 555 | "pv": 1000 556 | }, None) 557 | assert 'result' in response 558 | assert round(response.get('result'), 6) == -302.114804 559 | 560 | response = handlers.ppmt_handler({ 561 | "rate": 0.10, 562 | "per": 1, 563 | "nper": 3, 564 | "pv": 1000, 565 | "fv": 2000 566 | }, None) 567 | assert 'result' in response 568 | assert round(response.get('result'), 6) == -906.344411 569 | 570 | response = handlers.ppmt_handler({ 571 | "rate": 0.10, 572 | "per": 1, 573 | "nper": 3, 574 | "pv": 1000, 575 | "fv": 2000, 576 | "type": 1 577 | }, None) 578 | assert 'result' in response 579 | assert round(response.get('result'), 6) == -914.858555 580 | 581 | 582 | def test_ppmt_missing_rate(): 583 | response = handlers.ppmt_handler({ 584 | "per": 1, 585 | "nper": 3, 586 | "pv": 1000 587 | }, None) 588 | 589 | assert 'error' in response 590 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("rate") 591 | 592 | 593 | def test_ppmt_missing_per(): 594 | response = handlers.ppmt_handler({ 595 | "rate": 0.10, 596 | "nper": 3, 597 | "pv": 1000 598 | }, None) 599 | 600 | assert 'error' in response 601 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("per") 602 | 603 | 604 | def test_ppmt_missing_nper(): 605 | response = handlers.ppmt_handler({ 606 | "rate": 0.10, 607 | "per": 1, 608 | "pv": 1000 609 | }, None) 610 | 611 | assert 'error' in response 612 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("nper") 613 | 614 | 615 | def test_ppmt_missing_pv(): 616 | response = handlers.ppmt_handler({ 617 | "rate": 0.10, 618 | "per": 1, 619 | "nper": 3, 620 | }, None) 621 | 622 | assert 'error' in response 623 | assert response.get('error') == REQUIRED_PROPERTY_ERR.format("pv") 624 | 625 | 626 | def test_rate_handler(): 627 | # TODO test data types 628 | response = handlers.rate_handler({ 629 | "nper": 6, 630 | "pmt": -200, 631 | "pv": 1000 632 | }, None) 633 | assert 'result' in response 634 | assert round(response.get('result'), 6) == 0.054718 635 | 636 | response = handlers.rate_handler({ 637 | "nper": 6, 638 | "pv": 1000, 639 | "fv": -100 640 | }, None) 641 | assert 'result' in response 642 | assert round(response.get('result'), 6) == -0.318708 643 | 644 | response = handlers.rate_handler({ 645 | "nper": 6, 646 | "pmt": -200, 647 | "pv": 1000, 648 | "fv": 0.10 649 | }, None) 650 | assert 'result' in response 651 | assert round(response.get('result'), 6) == 0.054695 652 | 653 | response = handlers.rate_handler({ 654 | "nper": 6, 655 | "pmt": -200, 656 | "pv": 1000, 657 | "fv": 0.10, 658 | "type": 1 659 | }, None) 660 | assert 'result' in response 661 | assert round(response.get('result'), 6) == 0.079278 662 | 663 | 664 | def test_rate_missing_nper(): 665 | response = handlers.rate_handler({ 666 | "pmt": -200, 667 | "pv": 1000, 668 | "fv": 0.10 669 | }, None) 670 | 671 | assert 'error' in response 672 | 673 | 674 | def test_rate_missing_pv(): 675 | response = handlers.rate_handler({ 676 | "nper": 6, 677 | "pmt": -200, 678 | "fv": 0.10 679 | }, None) 680 | 681 | assert 'error' in response 682 | 683 | 684 | def test_effect_handler(): 685 | response = handlers.effect_handler({ 686 | "nominal_rate": 0.12, 687 | "npery": 12 688 | }, None) 689 | assert 'result' in response 690 | assert round(response.get('result'), 6) == 0.126825 691 | 692 | 693 | def test_effect_handler_non_int_npery(): 694 | response = handlers.effect_handler({ 695 | "nominal_rate": 0.12, 696 | "npery": 12.7 697 | }, None) 698 | assert 'result' in response 699 | assert round(response.get('result'), 6) == 0.126825 700 | 701 | 702 | def test_effect_missing_nominal_rate(): 703 | response = handlers.effect_handler({ 704 | "npery": 12 705 | }, None) 706 | 707 | assert 'error' in response 708 | 709 | 710 | def test_effect_missing_npery(): 711 | response = handlers.effect_handler({ 712 | "nominal_rate": 0.12 713 | }, None) 714 | 715 | assert 'error' in response 716 | 717 | 718 | def test_nominal_handler(): 719 | response = handlers.nominal_handler({ 720 | "effect_rate": 0.12, 721 | "npery": 12 722 | }, None) 723 | assert 'result' in response 724 | assert round(response.get('result'), 6) == 0.113866 725 | 726 | 727 | def test_nominal_handler_non_int_npery(): 728 | response = handlers.nominal_handler({ 729 | "effect_rate": 0.12, 730 | "npery": 12.7 731 | }, None) 732 | assert 'result' in response 733 | assert round(response.get('result'), 6) == 0.113866 734 | 735 | 736 | def test_nominal_missing_effect_rate(): 737 | response = handlers.nominal_handler({ 738 | "npery": 12 739 | }, None) 740 | 741 | assert 'error' in response 742 | 743 | 744 | def test_nominal_missing_npery(): 745 | response = handlers.nominal_handler({ 746 | "effect_rate": 0.12 747 | }, None) 748 | 749 | assert 'error' in response 750 | 751 | 752 | def test_sln_handler(): 753 | response = handlers.sln_handler({ 754 | "cost": 5000, 755 | "salvage": 300, 756 | "life": 10 757 | }, None) 758 | assert 'result' in response 759 | assert response.get('result') == 470 760 | 761 | 762 | def test_sln_handler_missing_cost(): 763 | response = handlers.sln_handler({ 764 | "salvage": 300, 765 | "life": 10 766 | }, None) 767 | 768 | assert 'error' in response 769 | 770 | 771 | def test_sln_handler_missing_salvage(): 772 | response = handlers.sln_handler({ 773 | "cost": 5000, 774 | "life": 10 775 | }, None) 776 | 777 | assert 'error' in response 778 | 779 | 780 | def test_sln_handler_missing_life(): 781 | response = handlers.sln_handler({ 782 | "cost": 5000, 783 | "salvage": 300 784 | }, None) 785 | 786 | assert 'error' in response 787 | 788 | 789 | def test_sln_handler_zero_life(): 790 | response = handlers.sln_handler({ 791 | "cost": 5000, 792 | "salvage": 300, 793 | "life": 0 794 | }, None) 795 | 796 | assert 'error' in response 797 | -------------------------------------------------------------------------------- /test/unit/test_wrapper_handler.py: -------------------------------------------------------------------------------- 1 | import wrapper_handler as wrapper 2 | 3 | def test_wrapper_handler(): 4 | request = { 5 | "function_name": "fv", 6 | 'args': { 7 | "rate": 0.004166666666667, 8 | "nper": 120, 9 | "pmt": -100 10 | } 11 | } 12 | 13 | response = wrapper.financial_functions_handler(request, None) 14 | assert 'result' in response 15 | assert round(response.get('result'), 6) == 15528.227945 16 | 17 | def test_wrapper_handler_invalid_function_name(): 18 | response = wrapper.financial_functions_handler({"function_name":"not_available", 'args': {'foo': 'bar'}}, None) 19 | assert 'error' in response 20 | 21 | def test_wrapper_handler_invalid_function_args(): 22 | response = wrapper.financial_functions_handler({"function_name":"fv", 'args': {}}, None) 23 | assert 'error' in response 24 | 25 | -------------------------------------------------------------------------------- /test/wrapper-fv.json: -------------------------------------------------------------------------------- 1 | { 2 | "function_name": "fv", 3 | "args": { 4 | "rate": 0.004166666666667, 5 | "nper": 120, 6 | "pmt": -100, 7 | "pv": -100 8 | } 9 | } -------------------------------------------------------------------------------- /test/wrapper-irr.json: -------------------------------------------------------------------------------- 1 | { 2 | "function_name": "irr", 3 | "args": { 4 | "values": [-100, 39, 59, 55, 20] 5 | } 6 | } -------------------------------------------------------------------------------- /test/xirr.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [-100, 20, 40, 25], 3 | "dates": ["2016-01-01", "2016-4-1", "2016-10-1", "2017-2-1"] 4 | } -------------------------------------------------------------------------------- /test/xnpv.json: -------------------------------------------------------------------------------- 1 | { 2 | "rate": 0.05, 3 | "values": [-10000, 2000, 2400, 2900, 3500, 4100], 4 | "dates": ["2016-1-1", "2016-2-1", "2016-5-1", "2016-7-1", "2016-9-1", "2017-1-1"] 5 | } --------------------------------------------------------------------------------