├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── documentation.yml │ └── feature-request.yml └── workflows │ ├── publish_docs_to_pages.yml │ ├── pythonpackage.yml │ └── type_check.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── asv.conf.json ├── benchmarks ├── __init__.py └── benchmarks.py ├── doc ├── Makefile ├── make.bat └── source │ ├── _includes │ └── release-notes.rst │ ├── _static │ ├── numpy_financial_favicon.png │ └── numpy_financial_logov.svg │ ├── api.rst │ ├── conf.py │ ├── dev │ ├── building_the_docs.md │ ├── building_with_spin.md │ ├── checking_out_an_upstream_pr.md │ ├── getting_the_code.md │ ├── index.rst │ └── running_the_benchmarks.md │ └── index.rst ├── docweb ├── README.txt ├── css │ └── home.css ├── images │ ├── numpy_financial_favicon.png │ └── numpy_financial_logoh.svg └── index.html ├── environment.yml ├── meson.build ├── numpy_financial ├── __init__.py ├── _cfinancial.pyi ├── _cfinancial.pyx ├── _financial.py ├── meson.build ├── py.typed └── tests │ ├── strategies.py │ └── test_financial.py └── pyproject.toml /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | All NumPy-Financial participants are expected to honor the NumPy Code of Conduct. 2 | 3 | This code of conduct applies to all spaces managed by the NumPy project, including all public and private mailing lists, issue trackers, wikis, blogs, Twitter, and any other communication channel used by our community. The NumPy project does not organise in-person events, however events related to our community should have a code of conduct similar in spirit to this one. 4 | 5 | This code of conduct should be honored by everyone who participates in the NumPy community formally or informally, or claims any affiliation with the project, in any project-related activities and especially when representing the project, in any role. 6 | 7 | This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment and goals. Please try to follow this code in spirit as much as in letter, to create a friendly and productive environment that enriches the surrounding community. 8 | 9 | To view the full Code of Conduct, please see: https://numpy.org/code-of-conduct/ 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug. For security vulnerabilities see Report a security vulnerability in the templates. 3 | title: "BUG: " 4 | labels: [00 - Bug] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: > 10 | Thank you for taking the time to file a bug report. Before creating a new 11 | issue, please make sure to take a few minutes to check the issue tracker 12 | for existing issues about the bug. 13 | 14 | - type: textarea 15 | attributes: 16 | label: "Describe the issue:" 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | attributes: 22 | label: "Reproduce the code example:" 23 | description: > 24 | A short code example that reproduces the problem/missing feature. It 25 | should be self-contained, i.e., can be copy-pasted into the Python 26 | interpreter or run as-is via `python myproblem.py`. 27 | placeholder: | 28 | import numpy_financial as npf 29 | << your code here >> 30 | render: python 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | attributes: 36 | label: "Error message:" 37 | description: > 38 | Please include full error message, if any. 39 | If you are reporting a segfault please include a GDB traceback, 40 | which you can generate by following 41 | [these instructions](https://github.com/numpy/numpy/blob/main/doc/source/dev/development_environment.rst#debugging). 42 | placeholder: | 43 | << Full traceback starting from `Traceback: ...` >> 44 | render: shell 45 | 46 | - type: textarea 47 | attributes: 48 | label: "Runtime information:" 49 | description: > 50 | Output from `import sys, numpy; print(numpy.__version__); print(sys.version)` 51 | If you are running NumPy 1.24+, also show `print(numpy.show_runtime())` 52 | validations: 53 | required: true 54 | 55 | - type: textarea 56 | attributes: 57 | label: "Context for the issue:" 58 | description: | 59 | Please explain how this issue affects your work or why it should be prioritized. 60 | placeholder: | 61 | << your explanation here >> 62 | validations: 63 | required: false 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Question/Help/Support 3 | url: https://numpy.org/gethelp/ 4 | about: "If you have a question, please look at the listed resources available on the website." 5 | - name: Development-related matters 6 | url: https://numpy.org/community/ 7 | about: "If you would like to discuss development-related matters or need help from the NumPy team, see our community's communication channels." 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Report an issue related to the NumPy-Financial documentation. 3 | title: "DOC: " 4 | labels: [04 - Documentation] 5 | 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: "Issue with current documentation:" 10 | description: > 11 | Please make sure to leave a reference to the document/code you're 12 | referring to. You can also check the development version of the 13 | documentation and see if this issue has already been addressed at 14 | https://numpy.org/devdocs. 15 | 16 | - type: textarea 17 | attributes: 18 | label: "Idea or request for content:" 19 | description: > 20 | Please describe as clearly as possible what topics you think are missing 21 | from the current documentation. Make sure to check 22 | https://github.com/numpy/numpy-tutorials and see if this issue might be 23 | more appropriate there. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Check instructions for submitting your idea on the mailing list first. 3 | title: "ENH: " 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: > 9 | If you're looking to request a new feature or change in functionality, 10 | including adding or changing the meaning of arguments to an existing 11 | function, please post your idea on the 12 | [numpy-discussion mailing list](https://mail.python.org/mailman/listinfo/numpy-discussion) 13 | to explain your reasoning in addition to opening an issue or pull request. 14 | You can also check out our 15 | [Contributor Guide](https://github.com/numpy/numpy/blob/main/doc/source/dev/index.rst) 16 | if you need more information. 17 | 18 | - type: textarea 19 | attributes: 20 | label: "Proposed new feature or change:" 21 | validations: 22 | required: true 23 | -------------------------------------------------------------------------------- /.github/workflows/publish_docs_to_pages.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs to gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.7' 16 | - name: Install dependencies 17 | run: | 18 | export DEBIAN_FRONTEND=noninteractive 19 | sudo apt-get -q -y install dvipng texlive-latex-extra 20 | python -m pip install --upgrade pip 21 | pip install -r requirements.txt 22 | pip install sphinx 23 | pip install numpydoc 24 | - name: Install numpy-financial 25 | run: | 26 | pip install . 27 | - name: Build documentation with Sphinx 28 | run: | 29 | cd doc 30 | make html 31 | mv build/html /tmp 32 | cd .. 33 | - name: Deploy to gh-pages branch 34 | env: 35 | GH_ACTION_PAGES_DEPLOY_KEY: ${{ secrets.GH_ACTION_PAGES_DEPLOY_KEY }} 36 | run: | 37 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 38 | # Set up SSH key 39 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 40 | mkdir -p ~/.ssh/ 41 | echo "$GH_ACTION_PAGES_DEPLOY_KEY" > ~/.ssh/id_rsa 42 | chmod 600 ~/.ssh/id_rsa 43 | ssh-keyscan github.com >> ~/.ssh/known_hosts 44 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 45 | # Reconfigure the git remote to use SSH 46 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 47 | git remote rm origin 48 | git remote add origin git@github.com:numpy/numpy-financial.git 49 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 50 | # Configure the git user 51 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 52 | git config user.name "${GITHUB_ACTOR}" 53 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 54 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 55 | # Go to work on gh-pages... 56 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 57 | echo "Fetching gh-pages" 58 | git fetch origin gh-pages 59 | echo "Checking out gh-pages branch" 60 | git checkout gh-pages 61 | echo "Removing old dev documentation" 62 | git clean -xdf . 63 | cd dev 64 | git rm -r '*' 65 | cd .. 66 | echo "Committing emptied dev directory" 67 | git commit -m "Remove old dev documentation" 68 | mkdir dev 69 | cd dev 70 | echo "Copying /tmp/html here" 71 | cp -v -r /tmp/html/* . 72 | echo "Adding new dev documentation" 73 | git add -A . 74 | git commit -m "Add new dev documentation" 75 | echo "Pushing new documentation to origin" 76 | git push -v origin gh-pages 77 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Test package 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | defaults: 9 | run: 10 | shell: bash -el {0} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | python-version: ["3.10", "3.11", "3.12"] 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: conda-incubator/setup-miniconda@v3 19 | with: 20 | auto-update-conda: true 21 | python-version: ${{ matrix.python-version }} 22 | activate-environment: npf-dev 23 | environment-file: environment.yml 24 | auto-activate-base: false 25 | - name: Conda metadata 26 | run: | 27 | conda info 28 | conda list 29 | - name: Lint 30 | run: | 31 | set -euo pipefail 32 | # Tell us what version we are using 33 | ruff version 34 | # Check the source file, ignore type annotations (ANN) for now. 35 | ruff check numpy_financial/ benchmarks/ --ignore F403 --select E,F,B,I 36 | - name: Build project 37 | run: | 38 | spin build -v 39 | - name: Test with pytest 40 | run: | 41 | spin test -- --doctest-modules 42 | -------------------------------------------------------------------------------- /.github/workflows/type_check.yml: -------------------------------------------------------------------------------- 1 | name: Type-check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | type-check: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 5 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: astral-sh/setup-uv@v6 21 | with: 22 | python-version: "3.11" 23 | activate-environment: true 24 | 25 | - name: env setup 26 | run: uv pip install . numpy mypy 27 | 28 | - name: run mypy 29 | run: mypy --no-incremental --cache-dir=/dev/null . 30 | 31 | - name: run pyright 32 | uses: jakebailey/pyright-action@v2 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor temporary/working/backup files # 2 | ######################################### 3 | .#* 4 | [#]*# 5 | *~ 6 | *$ 7 | *.bak 8 | *.diff 9 | .idea/ 10 | *.iml 11 | *.ipr 12 | *.iws 13 | *.org 14 | .project 15 | pmip 16 | *.rej 17 | .settings/ 18 | .*.sw[nop] 19 | .sw[nop] 20 | *.tmp 21 | *.vim 22 | .vscode 23 | tags 24 | cscope.out 25 | # gnu global 26 | GPATH 27 | GRTAGS 28 | GSYMS 29 | GTAGS 30 | .cache 31 | 32 | # Compiled source # 33 | ################### 34 | *.a 35 | *.com 36 | *.class 37 | *.dll 38 | *.exe 39 | *.o 40 | *.o.d 41 | *.py[ocd] 42 | *.so 43 | 44 | # Packages # 45 | ############ 46 | # it's better to unpack these files and commit the raw source 47 | # git has its own built in compression methods 48 | *.7z 49 | *.bz2 50 | *.bzip2 51 | *.dmg 52 | *.gz 53 | *.iso 54 | *.jar 55 | *.rar 56 | *.tar 57 | *.tbz2 58 | *.tgz 59 | *.zip 60 | 61 | # Python files # 62 | ################ 63 | # setup.py working directory 64 | build 65 | build-install 66 | # sphinx build directory 67 | _build 68 | # setup.py dist directory 69 | dist 70 | doc/build 71 | doc/cdoc/build 72 | # Egg metadata 73 | *.egg-info 74 | # The shelf plugin uses this dir 75 | ./.shelf 76 | MANIFEST 77 | .cache 78 | 79 | # Paver generated files # 80 | ######################### 81 | /release 82 | 83 | # Logs and databases # 84 | ###################### 85 | *.log 86 | *.sql 87 | *.sqlite 88 | 89 | # Patches # 90 | ########### 91 | *.patch 92 | *.diff 93 | 94 | # OS generated files # 95 | ###################### 96 | .DS_Store* 97 | .VolumeIcon.icns 98 | .fseventsd 99 | Icon? 100 | .gdb_history 101 | ehthumbs.db 102 | Thumbs.db 103 | .directory 104 | 105 | # pytest generated files # 106 | ########################## 107 | /.pytest_cache 108 | 109 | # Poetry lock file # 110 | #################### 111 | poetry.lock 112 | 113 | # Things specific to this project # 114 | ################################### 115 | doc/source/_api_stubs 116 | 117 | # Misc files that we do not require # 118 | .asv/ 119 | .hypothesis/ 120 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2019, NumPy Developers. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of the NumPy Developers nor the names of any 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NumPy Financial 2 | 3 | The `numpy-financial` package contains a collection of elementary financial 4 | functions. 5 | 6 | The [financial functions in NumPy](https://numpy.org/doc/1.17/reference/routines.financial.html) 7 | are deprecated and eventually will be removed from NumPy; see 8 | [NEP-32](https://numpy.org/neps/nep-0032-remove-financial-functions.html) 9 | for more information. This package is the replacement for the original 10 | NumPy financial functions. 11 | 12 | The source code for this package is available at https://github.com/numpy/numpy-financial. 13 | 14 | The importable name of the package is `numpy_financial`. The recommended 15 | alias is `npf`. For example, 16 | 17 | ``` 18 | >>> import numpy_financial as npf 19 | >>> npf.irr([-250000, 100000, 150000, 200000, 250000, 300000]) 20 | 0.5672303344358536 21 | ``` 22 | -------------------------------------------------------------------------------- /asv.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | // The version of the config file format. Do not change, unless 3 | // you know what you are doing. 4 | "version": 1, 5 | 6 | // The name of the project being benchmarked 7 | "project": "NumPy-Financial", 8 | 9 | // The project's homepage 10 | "project_url": "https://numpy.org/numpy-financial//", 11 | 12 | // The URL or local path of the source code repository for the 13 | // project being benchmarked 14 | "repo": ".", 15 | 16 | // The Python project's subdirectory in your repo. If missing or 17 | // the empty string, the project is assumed to be located at the root 18 | // of the repository. 19 | // "repo_subdir": "", 20 | 21 | // Customizable commands for building the project. 22 | // See asv.conf.json documentation. 23 | // To build the package using pyproject.toml (PEP518), uncomment the following lines 24 | "build_command": [ 25 | "python -m pip install build", 26 | "python -m build", 27 | "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" 28 | ], 29 | // To build the package using setuptools and a setup.py file, uncomment the following lines 30 | // "build_command": [ 31 | // "python setup.py build", 32 | // "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" 33 | // ], 34 | 35 | // Customizable commands for installing and uninstalling the project. 36 | // See asv.conf.json documentation. 37 | // "install_command": ["in-dir={env_dir} python -mpip install {wheel_file}"], 38 | // "uninstall_command": ["return-code=any python -mpip uninstall -y {project}"], 39 | 40 | // List of branches to benchmark. If not provided, defaults to "master" 41 | // (for git) or "default" (for mercurial). 42 | "branches": ["HEAD"], 43 | 44 | // The DVCS being used. If not set, it will be automatically 45 | // determined from "repo" by looking at the protocol in the URL 46 | // (if remote), or by looking for special directories, such as 47 | // ".git" (if local). 48 | "dvcs": "git", 49 | 50 | // The tool to use to create environments. May be "conda", 51 | // "virtualenv", "mamba" (above 3.8) 52 | // or other value depending on the plugins in use. 53 | // If missing or the empty string, the tool will be automatically 54 | // determined by looking for tools on the PATH environment 55 | // variable. 56 | "environment_type": "virtualenv", 57 | 58 | // timeout in seconds for installing any dependencies in environment 59 | // defaults to 10 min 60 | //"install_timeout": 600, 61 | 62 | // the base URL to show a commit for the project. 63 | // "show_commit_url": "http://github.com/owner/project/commit/", 64 | 65 | // The Pythons you'd like to test against. If not provided, defaults 66 | // to the current version of Python used to run `asv`. 67 | // "pythons": ["2.7", "3.8"], 68 | 69 | // The list of conda channel names to be searched for benchmark 70 | // dependency packages in the specified order 71 | // "conda_channels": ["conda-forge", "defaults"], 72 | 73 | // A conda environment file that is used for environment creation. 74 | // "conda_environment_file": "environment.yml", 75 | 76 | // The matrix of dependencies to test. Each key of the "req" 77 | // requirements dictionary is the name of a package (in PyPI) and 78 | // the values are version numbers. An empty list or empty string 79 | // indicates to just test against the default (latest) 80 | // version. null indicates that the package is to not be 81 | // installed. If the package to be tested is only available from 82 | // PyPi, and the 'environment_type' is conda, then you can preface 83 | // the package name by 'pip+', and the package will be installed 84 | // via pip (with all the conda available packages installed first, 85 | // followed by the pip installed packages). 86 | // 87 | // The ``@env`` and ``@env_nobuild`` keys contain the matrix of 88 | // environment variables to pass to build and benchmark commands. 89 | // An environment will be created for every combination of the 90 | // cartesian product of the "@env" variables in this matrix. 91 | // Variables in "@env_nobuild" will be passed to every environment 92 | // during the benchmark phase, but will not trigger creation of 93 | // new environments. A value of ``null`` means that the variable 94 | // will not be set for the current combination. 95 | // 96 | // "matrix": { 97 | // "req": { 98 | // "numpy": ["1.6", "1.7"], 99 | // "six": ["", null], // test with and without six installed 100 | // "pip+emcee": [""] // emcee is only available for install with pip. 101 | // }, 102 | // "env": {"ENV_VAR_1": ["val1", "val2"]}, 103 | // "env_nobuild": {"ENV_VAR_2": ["val3", null]}, 104 | // }, 105 | 106 | // Combinations of libraries/python versions can be excluded/included 107 | // from the set to test. Each entry is a dictionary containing additional 108 | // key-value pairs to include/exclude. 109 | // 110 | // An exclude entry excludes entries where all values match. The 111 | // values are regexps that should match the whole string. 112 | // 113 | // An include entry adds an environment. Only the packages listed 114 | // are installed. The 'python' key is required. The exclude rules 115 | // do not apply to includes. 116 | // 117 | // In addition to package names, the following keys are available: 118 | // 119 | // - python 120 | // Python version, as in the *pythons* variable above. 121 | // - environment_type 122 | // Environment type, as above. 123 | // - sys_platform 124 | // Platform, as in sys.platform. Possible values for the common 125 | // cases: 'linux2', 'win32', 'cygwin', 'darwin'. 126 | // - req 127 | // Required packages 128 | // - env 129 | // Environment variables 130 | // - env_nobuild 131 | // Non-build environment variables 132 | // 133 | // "exclude": [ 134 | // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows 135 | // {"environment_type": "conda", "req": {"six": null}}, // don't run without six on conda 136 | // {"env": {"ENV_VAR_1": "val2"}}, // skip val2 for ENV_VAR_1 137 | // ], 138 | // 139 | // "include": [ 140 | // // additional env for python2.7 141 | // {"python": "2.7", "req": {"numpy": "1.8"}, "env_nobuild": {"FOO": "123"}}, 142 | // // additional env if run on windows+conda 143 | // {"platform": "win32", "environment_type": "conda", "python": "2.7", "req": {"libpython": ""}}, 144 | // ], 145 | 146 | // The directory (relative to the current directory) that benchmarks are 147 | // stored in. If not provided, defaults to "benchmarks" 148 | // "benchmark_dir": "benchmarks", 149 | 150 | // The directory (relative to the current directory) to cache the Python 151 | // environments in. If not provided, defaults to "env" 152 | "env_dir": ".asv/env", 153 | 154 | // The directory (relative to the current directory) that raw benchmark 155 | // results are stored in. If not provided, defaults to "results". 156 | "results_dir": ".asv/results", 157 | 158 | // The directory (relative to the current directory) that the html tree 159 | // should be written to. If not provided, defaults to "html". 160 | "html_dir": ".asv/html", 161 | 162 | // The number of characters to retain in the commit hashes. 163 | // "hash_length": 8, 164 | 165 | // `asv` will cache results of the recent builds in each 166 | // environment, making them faster to install next time. This is 167 | // the number of builds to keep, per environment. 168 | // "build_cache_size": 2, 169 | 170 | // The commits after which the regression search in `asv publish` 171 | // should start looking for regressions. Dictionary whose keys are 172 | // regexps matching to benchmark names, and values corresponding to 173 | // the commit (exclusive) after which to start looking for 174 | // regressions. The default is to start from the first commit 175 | // with results. If the commit is `null`, regression detection is 176 | // skipped for the matching benchmark. 177 | // 178 | // "regressions_first_commits": { 179 | // "some_benchmark": "352cdf", // Consider regressions only after this commit 180 | // "another_benchmark": null, // Skip regression detection altogether 181 | // }, 182 | 183 | // The thresholds for relative change in results, after which `asv 184 | // publish` starts reporting regressions. Dictionary of the same 185 | // form as in ``regressions_first_commits``, with values 186 | // indicating the thresholds. If multiple entries match, the 187 | // maximum is taken. If no entry matches, the default is 5%. 188 | // 189 | // "regressions_thresholds": { 190 | // "some_benchmark": 0.01, // Threshold of 1% 191 | // "another_benchmark": 0.5, // Threshold of 50% 192 | // }, 193 | } 194 | -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /benchmarks/benchmarks.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import numpy_financial as npf 4 | 5 | 6 | class Npv2D: 7 | 8 | param_names = ["n_cashflows", "cashflow_lengths", "rates_lengths"] 9 | params = [ 10 | (1, 10, 100), 11 | (1, 10, 100), 12 | (1, 10, 100), 13 | ] 14 | 15 | def __init__(self): 16 | self.rates = None 17 | self.cashflows = None 18 | 19 | def setup(self, n_cashflows, cashflow_lengths, rates_lengths): 20 | rng = np.random.default_rng(0) 21 | cf_shape = (n_cashflows, cashflow_lengths) 22 | self.cashflows = rng.standard_normal(cf_shape) 23 | self.rates = rng.standard_normal(rates_lengths) 24 | 25 | def time_for_loop(self, n_cashflows, cashflow_lengths, rates_lengths): 26 | for rate in self.rates: 27 | for cashflow in self.cashflows: 28 | npf.npv(rate, cashflow) 29 | 30 | def time_broadcast(self, n_cashflows, cashflow_lengths, rates_lengths): 31 | npf.npv(self.rates, self.cashflows) 32 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /doc/source/_includes/release-notes.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ------------- 3 | 1.1.0 4 | ~~~~~ 5 | * **1.1.0 is not released yet.** 6 | 7 | 1.0.0 8 | ~~~~~ 9 | * The transition of the source code from NumPy to this package is complete. 10 | 11 | 0.2.0 12 | ~~~~~ 13 | * Removed the use of `numpy.core.overrides.array_function_dispatch` to create 14 | wrappers of the financial functions. 15 | * Support NumPy versions back to 1.15. 16 | 17 | 0.1.0 18 | ~~~~~ 19 | * This is the initial release of numpy-financial. The functions were 20 | copied from NumPy 1.17. 21 | -------------------------------------------------------------------------------- /doc/source/_static/numpy_financial_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/numpy/numpy-financial/HEAD/doc/source/_static/numpy_financial_favicon.png -------------------------------------------------------------------------------- /doc/source/_static/numpy_financial_logov.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | NumPy 11 | Financial 12 | 13 | -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. currentmodule:: numpy_financial 5 | 6 | .. autosummary:: 7 | :toctree: _api_stubs/ 8 | 9 | fv 10 | ipmt 11 | irr 12 | mirr 13 | nper 14 | npv 15 | pmt 16 | ppmt 17 | pv 18 | rate 19 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | import numpy_financial 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'numpy-financial' 23 | copyright = '2023, numpy-financial developers' 24 | author = 'numpy-financial developers' 25 | 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'numpydoc', 35 | 'sphinx.ext.mathjax', 36 | 'myst_parser', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = [] 46 | 47 | version = numpy_financial.__version__ 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | html_theme = 'pydata_sphinx_theme' 55 | html_copy_source = False 56 | 57 | # Add any paths that contain custom static files (such as style sheets) here, 58 | # relative to this directory. They are copied after the builtin static files, 59 | # so a file named "default.css" will overwrite the builtin "default.css". 60 | html_static_path = ['_static'] 61 | 62 | html_logo = "_static/numpy_financial_logov.svg" 63 | html_favicon = "_static/numpy_financial_favicon.png" 64 | -------------------------------------------------------------------------------- /doc/source/dev/building_the_docs.md: -------------------------------------------------------------------------------- 1 | # Building the docs 2 | 3 | This guide goes through how to build the NumPy-Financial documentation with spin and conda 4 | 5 | ## Assumptions 6 | 7 | This guide assumes that you have set up poetry and a virtual environment. If you have 8 | not done this please read [building_with_spin](building_with_spin). 9 | 10 | You can check that conda and spin are installed by running: 11 | 12 | ```shell 13 | conda -V 14 | ``` 15 | 16 | ```shell 17 | spin -V 18 | ``` 19 | 20 | ## Building the documentation 21 | 22 | spin handles building the documentation for us. All we have to do is invoke the built-in command. 23 | 24 | ```shell 25 | spin docs -j 1 26 | ``` 27 | 28 | This will create the docs as a html document in the ``doc/build`` directory. Note that there are several options 29 | available, however, only the html documentation is built officially. 30 | -------------------------------------------------------------------------------- /doc/source/dev/building_with_spin.md: -------------------------------------------------------------------------------- 1 | # Building with spin and conda 2 | 3 | ## Installing conda 4 | numpy-financial uses [spin](https://github.com/scientific-python/spin) and conda 5 | to manage dependencies, build wheels and sdists, and publish to PyPI this page 6 | documents how to work with spin and conda. 7 | 8 | To install poetry follow their [official guide](https://docs.anaconda.com/free/miniconda/miniconda-install/) 9 | it is recommended to use the official installer for local development. 10 | 11 | To check your installation try to check the version of miniconda: 12 | 13 | ```shell 14 | conda -V 15 | ``` 16 | 17 | ## Setting up a virtual environment using conda 18 | 19 | Once conda is installed it is time to set up the virtual environment. To do 20 | this run: 21 | 22 | ```shell 23 | conda env create -f environment.yml 24 | ``` 25 | 26 | This command looks for dependencies in the ``environment.yml`` file, 27 | resolves them to the most recent version and installs the dependencies 28 | in a virtual environment. It is now possible to launch an interactive REPL 29 | by running the following command: 30 | 31 | ```shell 32 | conda activate numpy-financial-dev 33 | ``` 34 | 35 | ## Building NumPy-Financial 36 | 37 | NumPy-Financial is built using a combination of Python and Cython. We therefore 38 | require a build step. This can be run using 39 | 40 | ```shell 41 | spin build 42 | ``` 43 | 44 | ## Running the test suite 45 | 46 | NumPy-Financial has an extensive test suite, which can be run with the 47 | following command: 48 | 49 | ```shell 50 | spin test 51 | ``` 52 | 53 | ## Building distributions 54 | 55 | It is possible to manually build distributions for numpy-financial using 56 | poetry. This is possible via the `build` command: 57 | 58 | ```shell 59 | spin build 60 | ``` 61 | 62 | The `build` command creates a `dist` directory containing a wheel and sdist 63 | file. 64 | -------------------------------------------------------------------------------- /doc/source/dev/checking_out_an_upstream_pr.md: -------------------------------------------------------------------------------- 1 | # Editing another person's pull request 2 | 3 | ## Respect 4 | 5 | Please be respectful of other's work. 6 | 7 | ## Expected setup 8 | 9 | This guide expects that you have set up your git environment as is outlined in [getting_the_code](getting_the_code.md). 10 | In particular, it assumes that you have: 11 | 12 | 1. a remote called ``origin`` for your fork of NumPy-Financial 13 | 2. a remote called ``upstream`` for the original fork of NumPy-Financial 14 | 15 | You can check this by running: 16 | 17 | ```shell 18 | git remote -v 19 | ``` 20 | 21 | Which should output lines similar to the below: 22 | 23 | ``` 24 | origin https://github.com//numpy-financial.git (fetch) 25 | origin https://github.com//numpy-financial.git (push) 26 | upstream https://github.com/numpy/numpy-financial.git (fetch) 27 | upstream https://github.com/numpy/numpy-financial.git (push) 28 | ``` 29 | 30 | ## Accessing the pull request 31 | 32 | You will need to find the pull request ID from the pull request you are looking at. Then you can fetch the pull request and create a branch in the process by: 33 | 34 | ```shell 35 | git fetch upstream pull//head: 36 | ``` 37 | 38 | Where: 39 | 40 | * ```` is the id that you found from the pull request 41 | * ```` is the name that you would like to give to the branch once it is created. 42 | 43 | Note that the branch name can be anything you want, however it has to be unique. 44 | 45 | ## Switching to the new branch 46 | 47 | ```shell 48 | git switch 49 | ``` 50 | -------------------------------------------------------------------------------- /doc/source/dev/getting_the_code.md: -------------------------------------------------------------------------------- 1 | # Getting the code 2 | 3 | This document explains how to get the source code for NumPy-Financial using Git. 4 | 5 | ## Code Location 6 | 7 | NumPy-Financial is hosted on GitHub. The repository URL is https://github.com/numpy/numpy-financial. 8 | 9 | ## Creating a fork 10 | 11 | To create a fork click the green "fork" button at the top right of the page. This creates a new repository on your 12 | GitHub profile. This will have the URL: https://github.com//numpy-financial. 13 | 14 | ## Cloning the repository 15 | 16 | Now that you have forked the repository you will need to clone it. This copies the repository from GitHub to the local 17 | machine. To clone a repository enter the following commands in the terminal. 18 | 19 | ```shell 20 | git clone https://github.com//numpy-financial.git 21 | ``` 22 | 23 | Hooray! You now have a working copy of NumPy-Financial. 24 | 25 | ## Adding the upstream repo 26 | 27 | Now that your fork of NumPy-Financial is available locally, it is worth adding the upstream repository as a remote. 28 | 29 | You can view the current remotes by running: 30 | 31 | ```shell 32 | git remote -v 33 | ``` 34 | 35 | This should produce some output similar to: 36 | 37 | ```shell 38 | origin https://github.com//numpy-financial.git (fetch) 39 | origin https://github.com//numpy-financial.git (push) 40 | ``` 41 | 42 | Now tell git that there is a remote repository that we will call ``upstream`` pointing to the numpy-financial repository: 43 | 44 | ```shell 45 | git remote add upstream https://github.com/numpy/numpy-financial.git 46 | ``` 47 | 48 | We can now check the remotes again: 49 | 50 | ```shell 51 | git remote -v 52 | ``` 53 | 54 | which gives two additional lines as output: 55 | 56 | ```shell 57 | origin https://github.com//numpy-financial.git (fetch) 58 | origin https://github.com//numpy-financial.git (push) 59 | upstream https://github.com/numpy/numpy-financial.git (fetch) 60 | upstream https://github.com/numpy/numpy-financial.git (push) 61 | ``` 62 | 63 | 64 | ## Pulling from upstream by default 65 | 66 | We want to be able to get the changes from the upstream repo by default. This way you pull the most recent changes into your repo. 67 | 68 | To set up your repository to read from the remote that we called `upstream`: 69 | 70 | ```shell 71 | git config branch.main.remote upstream 72 | git config branch.main.merge refs/heads/main 73 | ``` 74 | 75 | ## Updating the code with other's changes 76 | 77 | From time to time you may want to pull down the latest code. Do this with: 78 | 79 | ```shell 80 | git fetch 81 | ``` 82 | 83 | The `git fetch` command downloads commits, files and refs from a remote repo into your local repo. 84 | 85 | Then run: 86 | 87 | ```shell 88 | git merge --ff-only 89 | ``` 90 | 91 | The `git merge` command is Git's way of putting independent branches back together. The `--ff-only` flag tells git to 92 | use `fast-forward` merges. This is preferable as fast-forward merge avoids creating merge commits. -------------------------------------------------------------------------------- /doc/source/dev/index.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | getting_the_code.md 8 | building_with_spin.md 9 | running_the_benchmarks.md 10 | building_the_docs.md 11 | checking_out_an_upstream_pr.md 12 | 13 | 14 | .. include:: ../_includes/release-notes.rst 15 | -------------------------------------------------------------------------------- /doc/source/dev/running_the_benchmarks.md: -------------------------------------------------------------------------------- 1 | # Running the benchmarks 2 | 3 | This document outlines how to setup and run the benchmarks using [asv](https://asv.readthedocs.io/en/v0.6.1/). 4 | 5 | ## Running the benchmarks 6 | 7 | To run the benchmarks with ``asv``, simply enter: 8 | 9 | ```shell 10 | asv run 11 | ``` 12 | 13 | ## Viewing the results 14 | 15 | There are two steps to viewing the results locally. The results need to be published and the launched in a local web browser. 16 | 17 | To publish the results use: 18 | 19 | ```shell 20 | asv publish 21 | ``` 22 | 23 | And then to view the results: 24 | 25 | ```shell 26 | asv preview 27 | ``` 28 | 29 | This will launch a local web browser from which you can view the results 30 | 31 | ## Dry runs 32 | 33 | One common use case is to use ``asv`` in development, there are several useful flags that should be used: 34 | 35 | ```shell 36 | asv --python=same --quick --dry-run 37 | ``` 38 | 39 | We are adding three flags, these flags are: 40 | 41 | 1. `--python=same` uses the same environment as your development environment (saves time to avoid building environments) 42 | 2. `--quick` only runs the benchmarks once 43 | 3. `--dry-run` to not save the results of the benchmarks 44 | 45 | These can be useful for getting quick feedback during development, but should not be used as anything other than a rough guide. -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | 2 | numpy-financial |version| 3 | ========================= 4 | 5 | The `numpy-financial` package is a collection of elementary financial 6 | functions. 7 | 8 | The `financial functions in NumPy `_ 9 | are deprecated and eventually will be removed from NumPy; see 10 | `NEP-32 `_ 11 | for more information. This package is the replacement for the deprecated 12 | NumPy financial functions. 13 | 14 | The source code for this package is available at https://github.com/numpy/numpy-financial. 15 | 16 | The importable name of the package is `numpy_financial`. The recommended 17 | alias is `npf`. For example, 18 | 19 | >>> import numpy_financial as npf 20 | >>> npf.irr([-250000, 100000, 150000, 200000, 250000, 300000]) 21 | 0.5672303344358536 22 | 23 | .. toctree:: 24 | :hidden: 25 | :maxdepth: 4 26 | 27 | api 28 | dev/index 29 | 30 | 31 | Index and Search 32 | ---------------- 33 | 34 | * :ref:`genindex` 35 | * :ref:`search` 36 | -------------------------------------------------------------------------------- /docweb/README.txt: -------------------------------------------------------------------------------- 1 | This directory contains the files for the top-level online documentation. 2 | If these files are changed, the changes must be copied over to the top directory 3 | of the gh-pages branch. 4 | -------------------------------------------------------------------------------- /docweb/css/home.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin: auto; 4 | padding-right: 1em; 5 | padding-left: 1em; 6 | max-width: 48em; 7 | color: black; 8 | font-family: sans-serif; 9 | font-size: 100%; 10 | line-height: 125%; 11 | color: #333; 12 | } 13 | pre { 14 | border: 1px dotted gray; 15 | background-color: #E8E8E8; 16 | color: #101010; 17 | padding: 0.5em; 18 | } 19 | code { 20 | font-family: monospace; 21 | font-size: 130%; 22 | font-weight: bold; 23 | } 24 | h1 a, h2 a, h3 a, h4 a, h5 a { 25 | text-decoration: none; 26 | color: #336699; 27 | } 28 | h1, h2, h3, h4, h5 { 29 | font-family: sans-serif; 30 | font-weight: bold; 31 | color: #336699; } 32 | 33 | h1 { 34 | font-size: 130%; 35 | } 36 | 37 | h2 { 38 | font-size: 110%; 39 | font-style: italic; 40 | } 41 | 42 | h3 { 43 | font-size: 95%; 44 | font-style: italic; 45 | } 46 | 47 | h4 { 48 | font-size: 90%; 49 | font-style: italic; 50 | } 51 | 52 | h5 { 53 | font-size: 90%; 54 | font-style: italic; 55 | } 56 | 57 | h1.title { 58 | font-size: 200%; 59 | font-weight: bold; 60 | padding-top: 0.2em; 61 | padding-bottom: 0.2em; 62 | text-align: left; 63 | border: none; 64 | } 65 | 66 | dt code { 67 | font-weight: bold; 68 | } 69 | dd p { 70 | margin-top: 0; 71 | } 72 | 73 | ul { 74 | padding: 0px 0px 0px 16px; 75 | } 76 | 77 | ul li { 78 | margin: 0px; 79 | padding: 0px; 80 | } 81 | 82 | .link2 { 83 | border: solid 1px #000; 84 | border-radius: 3x; 85 | -moz-border-radius: 3px; 86 | -webkit-border-radius: 3px; 87 | background-color: #FFF; 88 | color: #336699; 89 | text-decoration: none; 90 | text-align: center; 91 | width: 180px; 92 | margin: 0px 0px 2px 0px; 93 | padding: 2px 0 0px 0px; 94 | display: inline-block; 95 | } 96 | .link2:hover { background-color: #FFF060; } 97 | 98 | aside h3 { 99 | margin: 10px 0px 2px 0px; 100 | } 101 | 102 | /* 103 | * Specific id styles. 104 | */ 105 | 106 | #logo { 107 | border: solid 1px #000; 108 | padding: 5px 5px 3px 5px; 109 | background-color: #FFFFEA; 110 | } 111 | 112 | #asidelinks { 113 | width: 25%; 114 | float: right; 115 | color: black; 116 | background-color: #E8F4FF; 117 | padding: 0px 10px 10px 10px; 118 | border: solid 1px #000; 119 | margin: 0em 0.75em 0em 0.5em; 120 | box-shadow: 5px 8px 9px #b0b0b0; 121 | } 122 | 123 | #footer { 124 | padding-top: 1em; 125 | font-size: 70%; 126 | color: gray; 127 | text-align: center; 128 | } 129 | -------------------------------------------------------------------------------- /docweb/images/numpy_financial_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/numpy/numpy-financial/HEAD/docweb/images/numpy_financial_favicon.png -------------------------------------------------------------------------------- /docweb/images/numpy_financial_logoh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | NumPy-Financial 11 | 12 | -------------------------------------------------------------------------------- /docweb/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | numpy-financial 7 | 13 | 14 | 17 | 18 | numpy-financial 19 | 20 | 21 | 22 | 23 |

logo

24 | 31 |

32 | The numpy-financial Python package is a collection of elementary 33 | financial functions. These functions were copied to this package 34 | from version 1.17 of NumPy. 35 |

36 |

The financial functions in NumPy 37 | are deprecated and eventually will be removed from NumPy; see 38 | NEP-32 39 | for more information. This package is the replacement for the deprecated 40 | NumPy financial functions. 41 |

42 | 43 |

Installation

44 |

45 | The package is available on PyPI, and may be installed 46 | with pip: 47 | 48 |

 49 |   $ pip install numpy-financial
 50 | 
51 | 52 | The package requires NumPy version 1.15 or later. 53 |

54 | 55 |

Using numpy_financial

56 |

57 | The importable name of the package is numpy_financial. 58 | The recommended alias is npf. For example, 59 |

60 |
 61 | >>> import numpy_financial as npf
 62 | >>> npf.irr([-250000, 100000, 150000, 200000, 250000, 300000])
 63 | 0.5672303344358536
 64 | 
65 | 66 |

Switching from numpy to numpy_financial

67 |
    68 |
  • 69 |

    Code that imports the function names like this 70 |

     71 |   from numpy import npv, irr
     72 | 
    73 | requires only that the import be changed to 74 |
     75 |   from numpy_financial import npv, irr
     76 | 
    77 |

    78 |
  • 79 |
  • 80 |

    For code that uses the numpy namespace like this 81 |

     82 |   import numpy as np
     83 | 
     84 |   x = np.array([-250000, 100000, 150000, 200000, 250000, 300000])
     85 |   r = np.irr(x)
     86 | 
    87 | the import statement for the numpy-financial package must be added, 88 | and the calls of the financial functions must be changed to use 89 | the npf namespace: 90 |
     91 |   import numpy as np
     92 |   import numpy_financial as npf
     93 | 
     94 |   x = np.array([-250000, 100000, 150000, 200000, 250000, 300000])
     95 |   r = npf.irr(x)
     96 | 
    97 |

    98 |
  • 99 |
100 | 101 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # To use: 2 | # $ conda env create -f environment.yml # `mamba` works too for this command 3 | # $ conda activate npf-dev 4 | # 5 | name: npf-dev 6 | channels: 7 | - conda-forge 8 | dependencies: 9 | # Runtime dependencies 10 | - python 11 | - numpy>=2.0.0 12 | # Build 13 | - cython>=3.0.9 14 | - compilers 15 | - meson 16 | - meson-python 17 | - ninja 18 | - spin 19 | - pkg-config 20 | # Tests 21 | - pytest 22 | - pytest-xdist 23 | - hypothesis 24 | # Docs 25 | - myst-parser 26 | - numpydoc 27 | - pydata-sphinx-theme>=0.15.0 28 | # Lint 29 | - ruff>=0.11.5 30 | # Benchmarks 31 | - asv>=0.6.0 32 | # Misc tools 33 | - ipython 34 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'numpy-financial', 3 | 'cython', 'c', 4 | version: '2.0.0.dev', 5 | license: 'BSD-3', 6 | meson_version: '>=1.4.0', 7 | ) 8 | 9 | cc = meson.get_compiler('c') 10 | cy = meson.get_compiler('cython') 11 | 12 | if not cy.version().version_compare('>=3.0.9') 13 | error('NumPy-Financial requires Cython >= 3.0.9') 14 | endif 15 | 16 | py = import('python').find_installation(pure: false) 17 | py_dep = py.dependency() 18 | 19 | subdir('numpy_financial') 20 | -------------------------------------------------------------------------------- /numpy_financial/__init__.py: -------------------------------------------------------------------------------- 1 | """__init__ file. 2 | 3 | This file allows us to import the public functions from NumPy-Financial and 4 | tells us the version we are using. 5 | """ 6 | 7 | __version__ = "1.1.0.dev0" 8 | 9 | from ._financial import * 10 | -------------------------------------------------------------------------------- /numpy_financial/_cfinancial.pyi: -------------------------------------------------------------------------------- 1 | from typing import TypeAlias 2 | 3 | import numpy as np 4 | import numpy.typing as npt 5 | 6 | _ArrayF64: TypeAlias = npt.NDArray[np.float64] 7 | 8 | def nper( 9 | rates: _ArrayF64, 10 | pmts: _ArrayF64, 11 | pvs: _ArrayF64, 12 | fvs: _ArrayF64, 13 | whens: _ArrayF64, 14 | out: _ArrayF64, 15 | ) -> None: ... 16 | def npv(rates: _ArrayF64, values: _ArrayF64, out: _ArrayF64) -> None: ... 17 | -------------------------------------------------------------------------------- /numpy_financial/_cfinancial.pyx: -------------------------------------------------------------------------------- 1 | from libc.math cimport NAN, INFINITY, log 2 | cimport cython 3 | 4 | 5 | cdef double nper_inner_loop( 6 | const double rate_, 7 | const double pmt_, 8 | const double pv_, 9 | const double fv_, 10 | const double when_ 11 | ) nogil: 12 | if rate_ == 0.0 and pmt_ == 0.0: 13 | return INFINITY 14 | 15 | if rate_ == 0.0: 16 | with cython.cdivision(True): 17 | # We know that pmt_ != 0, we don't need to check for division by 0 18 | return -(fv_ + pv_) / pmt_ 19 | 20 | if rate_ <= -1.0: 21 | return NAN 22 | 23 | with cython.cdivision(True): 24 | # We know that rate_ != 0, we don't need to check for division by 0 25 | z = pmt_ * (1.0 + rate_ * when_) / rate_ 26 | return log((-fv_ + z) / (pv_ + z)) / log(1.0 + rate_) 27 | 28 | 29 | @cython.boundscheck(False) 30 | @cython.wraparound(False) 31 | def nper( 32 | const double[::1] rates, 33 | const double[::1] pmts, 34 | const double[::1] pvs, 35 | const double[::1] fvs, 36 | const double[::1] whens, 37 | double[:, :, :, :, ::1] out): 38 | 39 | cdef: 40 | Py_ssize_t rate_, pmt_, pv_, fv_, when_ 41 | 42 | for rate_ in range(rates.shape[0]): 43 | for pmt_ in range(pmts.shape[0]): 44 | for pv_ in range(pvs.shape[0]): 45 | for fv_ in range(fvs.shape[0]): 46 | for when_ in range(whens.shape[0]): 47 | # We can have several ``ZeroDivisionErrors``s here 48 | # At the moment we want to replicate the existing function as 49 | # closely as possible however we should return financially 50 | # sensible results here. 51 | try: 52 | res = nper_inner_loop( 53 | rates[rate_], pmts[pmt_], pvs[pv_], fvs[fv_], whens[when_] 54 | ) 55 | except ZeroDivisionError: 56 | res = NAN 57 | 58 | out[rate_, pmt_, pv_, fv_, when_] = res 59 | 60 | 61 | @cython.boundscheck(False) 62 | @cython.cdivision(True) 63 | def npv(const double[::1] rates, const double[:, ::1] values, double[:, ::1] out): 64 | cdef: 65 | Py_ssize_t i, j, t 66 | double acc 67 | 68 | with nogil: 69 | for i in range(rates.shape[0]): 70 | for j in range(values.shape[0]): 71 | acc = 0.0 72 | for t in range(values.shape[1]): 73 | if rates[i] == -1.0: 74 | acc = NAN 75 | break 76 | else: 77 | acc = acc + values[j, t] / ((1.0 + rates[i]) ** t) 78 | out[i, j] = acc -------------------------------------------------------------------------------- /numpy_financial/_financial.py: -------------------------------------------------------------------------------- 1 | """Some simple financial calculations. 2 | 3 | patterned after spreadsheet computations. 4 | 5 | There is some complexity in each function 6 | so that the functions behave like ufuncs with 7 | broadcasting and being able to be called with scalars 8 | or arrays (or other sequences). 9 | 10 | Functions support the :class:`decimal.Decimal` type unless 11 | otherwise stated. 12 | """ 13 | 14 | from collections.abc import Iterable, Mapping, Sequence 15 | from decimal import Decimal 16 | from typing import Any, Callable, Final, Literal, Protocol, TypeAlias, TypeVar, overload 17 | 18 | import numpy as np 19 | import numpy.typing as npt 20 | from numpy._typing import _NestedSequence # pyright: ignore[reportPrivateImportUsage] 21 | 22 | from numpy_financial import _cfinancial 23 | 24 | __all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate', 25 | 'irr', 'npv', 'mirr', 26 | 'NoRealSolutionError', 'IterationsExceededError'] 27 | 28 | _ArrayT = TypeVar("_ArrayT", bound=npt.NDArray[Any]) 29 | _ScalarT = TypeVar("_ScalarT", bound=np.generic) 30 | _ScalarT_co = TypeVar("_ScalarT_co", bound=np.generic, covariant=True) 31 | 32 | # accepts arrays and scalars 33 | class _CanArray(Protocol[_ScalarT_co]): 34 | def __array__(self, /) -> npt.NDArray[_ScalarT_co]: ... 35 | 36 | # accepts arrays, rejects scalars 37 | class _CanArrayAndLen(_CanArray[_ScalarT_co], Protocol[_ScalarT_co]): 38 | def __len__(self, /) -> int: ... 39 | 40 | # casts *as* float64 41 | _AsFloat: TypeAlias = float | np.float64 42 | _AsFloat1D: TypeAlias = _CanArrayAndLen[np.float64] | Sequence[_AsFloat] 43 | _AsFloatND: TypeAlias = _CanArrayAndLen[np.float64] | _NestedSequence[_AsFloat] 44 | 45 | _AsDecimal: TypeAlias = Decimal | int 46 | 47 | # *co*ercible to float64 (assuming NEP 50 promotion rules and `same_kind` casting) 48 | _co_float64: TypeAlias = ( 49 | np.float64 50 | | np.float32 51 | | np.float16 52 | | np.integer[Any] 53 | | np.bool_ 54 | ) 55 | _CoFloat: TypeAlias = float | _co_float64 56 | _CoFloat1D: TypeAlias = _CanArrayAndLen[_co_float64] | Sequence[_CoFloat] 57 | _CoFloatND: TypeAlias = _CanArrayAndLen[_co_float64] | _NestedSequence[_CoFloat] 58 | # concise aliases for `_CoFloat | _CoFloat1D` and `_CoFloat | _CoFloatND` 59 | _CoFloatOr1D: TypeAlias = float | _CanArray[_co_float64] | Sequence[_CoFloat] 60 | _CoFloatOrND: TypeAlias = float | _CanArray[_co_float64] | _NestedSequence[_CoFloat] 61 | 62 | # coercible to (presumed to be) number-like dtypes 63 | _co_numeric: TypeAlias = np.floating[Any] | np.integer[Any] | np.bool_ | np.object_ 64 | _CoNumeric: TypeAlias = float | Decimal | _co_numeric 65 | _CoNumeric1D: TypeAlias = _CanArrayAndLen[_co_numeric] | Sequence[_CoNumeric] 66 | _CoNumericND: TypeAlias = _CanArrayAndLen[_co_numeric] | _NestedSequence[_CoNumeric] 67 | _CoNumericOrND: TypeAlias = ( 68 | float | Decimal | _CanArray[_co_numeric] | _NestedSequence[_CoNumeric] 69 | ) 70 | 71 | _ArrayLike: TypeAlias = npt.ArrayLike | _NestedSequence[Decimal] | Decimal 72 | 73 | _WhenOut: TypeAlias = Literal[0, 1] 74 | _When: TypeAlias = str | int | npt.NDArray[Any] | Iterable[str | int] 75 | 76 | # 77 | 78 | _when_to_num: Final[Mapping[_When, _WhenOut]] = { 79 | 'end': 0, 80 | "begin": 1, 81 | "e": 0, 82 | "b": 1, 83 | 0: 0, 84 | 1: 1, 85 | "beginning": 1, 86 | "start": 1, 87 | "finish": 0, 88 | } 89 | 90 | 91 | class NoRealSolutionError(Exception): 92 | """No real solution to the problem.""" 93 | 94 | 95 | class IterationsExceededError(Exception): 96 | """Maximum number of iterations reached.""" 97 | 98 | 99 | def _get_output_array_shape(*arrays: npt.NDArray[Any]) -> tuple[int, ...]: 100 | return tuple(array.shape[0] for array in arrays) 101 | 102 | 103 | def _ufunc_like(array: np.generic | npt.NDArray[Any]) -> Any: 104 | try: 105 | # If size of array is one, return scalar 106 | return array.item() 107 | except ValueError: 108 | # Otherwise, return entire array 109 | return array.squeeze() 110 | 111 | @overload 112 | def _convert_when(when: _ArrayT) -> _ArrayT: ... 113 | @overload 114 | def _convert_when(when: str | int) -> _WhenOut: ... # type: ignore[overload-overlap] 115 | @overload 116 | def _convert_when(when: Iterable[str | int]) -> list[_WhenOut]: ... 117 | def _convert_when(when: Any) -> Any: 118 | # Test to see if when has already been converted to ndarray 119 | # This will happen if one function calls another, for example ppmt 120 | if isinstance(when, np.ndarray): 121 | return when 122 | try: 123 | return _when_to_num[when] 124 | except (KeyError, TypeError): 125 | return [_when_to_num[x] for x in when] 126 | 127 | @overload 128 | def fv( 129 | rate: _AsFloat, 130 | nper: _CoFloat, 131 | pmt: _CoFloat, 132 | pv: _CoFloat, 133 | when: _When = 'end', 134 | ) -> float: ... 135 | @overload 136 | def fv( 137 | rate: Decimal, 138 | nper: _AsDecimal, 139 | pmt: _AsDecimal, 140 | pv: _AsDecimal, 141 | when: _When = 'end', 142 | ) -> Decimal: ... 143 | @overload 144 | def fv( 145 | rate: _AsFloat1D, 146 | nper: _CoFloatOr1D, 147 | pmt: _CoFloatOr1D, 148 | pv: _CoFloatOr1D, 149 | when: _When = 'end', 150 | ) -> npt.NDArray[np.float64]: ... 151 | @overload 152 | def fv( 153 | rate: _ArrayLike, 154 | nper: _ArrayLike, 155 | pmt: _ArrayLike, 156 | pv: _ArrayLike, 157 | when: _When = 'end', 158 | ) -> Any: ... 159 | def fv(rate, nper, pmt, pv, when: _When = 'end'): 160 | """Compute the future value. 161 | 162 | Given: 163 | * a present value, `pv` 164 | * an interest `rate` compounded once per period, of which 165 | there are 166 | * `nper` total 167 | * a (fixed) payment, `pmt`, paid either 168 | * at the beginning (`when` = {'begin', 1}) or the end 169 | (`when` = {'end', 0}) of each period 170 | 171 | Return: 172 | the value at the end of the `nper` periods 173 | 174 | Parameters 175 | ---------- 176 | rate : scalar or array_like of shape(M, ) 177 | Rate of interest as decimal (not per cent) per period 178 | nper : scalar or array_like of shape(M, ) 179 | Number of compounding periods 180 | pmt : scalar or array_like of shape(M, ) 181 | Payment 182 | pv : scalar or array_like of shape(M, ) 183 | Present value 184 | when : {{'begin', 1}, {'end', 0}}, {string, int}, optional 185 | When payments are due ('begin' (1) or 'end' (0)). 186 | Defaults to {'end', 0}. 187 | 188 | Returns 189 | ------- 190 | out : ndarray 191 | Future values. If all input is scalar, returns a scalar float. If 192 | any input is array_like, returns future values for each input element. 193 | If multiple inputs are array_like, they all must have the same shape. 194 | 195 | Notes 196 | ----- 197 | The future value is computed by solving the equation:: 198 | 199 | fv + 200 | pv*(1+rate)**nper + 201 | pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) == 0 202 | 203 | or, when ``rate == 0``:: 204 | 205 | fv + pv + pmt * nper == 0 206 | 207 | References 208 | ---------- 209 | .. [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). 210 | Open Document Format for Office Applications (OpenDocument)v1.2, 211 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version, 212 | Pre-Draft 12. Organization for the Advancement of Structured Information 213 | Standards (OASIS). Billerica, MA, USA. [ODT Document]. 214 | Available: 215 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula 216 | OpenDocument-formula-20090508.odt 217 | 218 | Examples 219 | -------- 220 | >>> import numpy as np 221 | >>> import numpy_financial as npf 222 | 223 | What is the future value after 10 years of saving $100 now, with 224 | an additional monthly savings of $100. Assume the interest rate is 225 | 5% (annually) compounded monthly? 226 | 227 | >>> npf.fv(0.05/12, 10*12, -100, -100) 228 | 15692.92889433575 229 | 230 | By convention, the negative sign represents cash flow out (i.e. money not 231 | available today). Thus, saving $100 a month at 5% annual interest leads 232 | to $15,692.93 available to spend in 10 years. 233 | 234 | If any input is array_like, returns an array of equal shape. Let's 235 | compare different interest rates from the example above. 236 | 237 | >>> a = np.array((0.05, 0.06, 0.07))/12 238 | >>> npf.fv(a, 10*12, -100, -100) 239 | array([15692.92889434, 16569.87435405, 17509.44688102]) 240 | 241 | """ 242 | when = _convert_when(when) 243 | rate, nper, pmt, pv, when = np.broadcast_arrays(rate, nper, pmt, pv, when) 244 | 245 | fv_array = np.empty_like(rate) 246 | zero = rate == 0 247 | nonzero = ~zero 248 | 249 | fv_array[zero] = -(pv[zero] + pmt[zero] * nper[zero]) 250 | 251 | rate_nonzero = rate[nonzero] 252 | temp = (1 + rate_nonzero) ** nper[nonzero] 253 | fv_array[nonzero] = ( 254 | - pv[nonzero] * temp 255 | - pmt[nonzero] * (1 + rate_nonzero * when[nonzero]) / rate_nonzero 256 | * (temp - 1) 257 | ) # fmt: skip 258 | 259 | if np.ndim(fv_array) == 0: 260 | # Follow the ufunc convention of returning scalars for scalar 261 | # and 0d array inputs. 262 | return fv_array.item(0) 263 | return fv_array 264 | 265 | @overload 266 | def pmt( 267 | rate: _AsFloat, 268 | nper: _CoFloat, 269 | pv: _CoFloat, 270 | fv: _CoFloat = 0, 271 | when: _When = 'end', 272 | ) -> float: ... 273 | @overload 274 | def pmt( 275 | rate: Decimal, 276 | nper: _AsDecimal, 277 | pv: _AsDecimal, 278 | fv: _AsDecimal = 0, 279 | when: _When = 'end', 280 | ) -> Decimal: ... 281 | @overload 282 | def pmt( 283 | rate: _AsFloatND, 284 | nper: _CoFloatOrND, 285 | pv: _CoFloatOrND, 286 | fv: _CoFloatOrND = 0, 287 | when: _When = 'end', 288 | ) -> npt.NDArray[np.float64]: ... 289 | @overload 290 | def pmt( 291 | rate: _ArrayLike, 292 | nper: _ArrayLike, 293 | pv: _ArrayLike, 294 | fv: _ArrayLike = 0, 295 | when: _When = 'end', 296 | ) -> Any: ... 297 | def pmt(rate, nper, pv, fv: Any = 0, when: _When = 'end'): 298 | """Compute the payment against loan principal plus interest. 299 | 300 | Given: 301 | * a present value, `pv` (e.g., an amount borrowed) 302 | * a future value, `fv` (e.g., 0) 303 | * an interest `rate` compounded once per period, of which 304 | there are 305 | * `nper` total 306 | * and (optional) specification of whether payment is made 307 | at the beginning (`when` = {'begin', 1}) or the end 308 | (`when` = {'end', 0}) of each period 309 | 310 | Return: 311 | the (fixed) periodic payment. 312 | 313 | Parameters 314 | ---------- 315 | rate : array_like 316 | Rate of interest (per period) 317 | nper : array_like 318 | Number of compounding periods 319 | pv : array_like 320 | Present value 321 | fv : array_like, optional 322 | Future value (default = 0) 323 | when : {{'begin', 1}, {'end', 0}}, {string, int} 324 | When payments are due ('begin' (1) or 'end' (0)) 325 | 326 | Returns 327 | ------- 328 | out : ndarray 329 | Payment against loan plus interest. If all input is scalar, returns a 330 | scalar float. If any input is array_like, returns payment for each 331 | input element. If multiple inputs are array_like, they all must have 332 | the same shape. 333 | 334 | Notes 335 | ----- 336 | The payment is computed by solving the equation:: 337 | 338 | fv + 339 | pv*(1 + rate)**nper + 340 | pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) == 0 341 | 342 | or, when ``rate == 0``:: 343 | 344 | fv + pv + pmt * nper == 0 345 | 346 | for ``pmt``. 347 | 348 | Note that computing a monthly mortgage payment is only 349 | one use for this function. For example, pmt returns the 350 | periodic deposit one must make to achieve a specified 351 | future balance given an initial deposit, a fixed, 352 | periodically compounded interest rate, and the total 353 | number of periods. 354 | 355 | References 356 | ---------- 357 | .. [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). 358 | Open Document Format for Office Applications (OpenDocument)v1.2, 359 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version, 360 | Pre-Draft 12. Organization for the Advancement of Structured Information 361 | Standards (OASIS). Billerica, MA, USA. [ODT Document]. 362 | Available: 363 | http://www.oasis-open.org/committees/documents.php 364 | ?wg_abbrev=office-formulaOpenDocument-formula-20090508.odt 365 | 366 | Examples 367 | -------- 368 | >>> import numpy_financial as npf 369 | 370 | What is the monthly payment needed to pay off a $200,000 loan in 15 371 | years at an annual interest rate of 7.5%? 372 | 373 | >>> npf.pmt(0.075/12, 12*15, 200000) 374 | np.float64(-1854.0247200054619) 375 | 376 | In order to pay-off (i.e., have a future-value of 0) the $200,000 obtained 377 | today, a monthly payment of $1,854.02 would be required. Note that this 378 | example illustrates usage of `fv` having a default value of 0. 379 | 380 | """ 381 | when = _convert_when(when) 382 | (rate, nper, pv, fv, when) = map(np.array, [rate, nper, pv, fv, when]) 383 | temp = (1 + rate) ** nper 384 | mask = (rate == 0) 385 | masked_rate = np.where(mask, 1, rate) 386 | fact = np.where(mask != 0, nper, 387 | (1 + masked_rate * when) * (temp - 1) / masked_rate) 388 | return -(fv + pv * temp) / fact 389 | 390 | @overload 391 | def nper( 392 | rate: _CoNumeric, 393 | pmt: _CoNumeric, 394 | pv: _CoNumeric, 395 | fv: _CoNumeric = 0, 396 | when: _When = 'end', 397 | ) -> float: ... 398 | @overload 399 | def nper( 400 | rate: _CoNumericND, 401 | pmt: _CoNumericOrND, 402 | pv: _CoNumericOrND, 403 | fv: _CoNumericOrND = 0, 404 | when: _When = 'end', 405 | ) -> npt.NDArray[np.float64]: ... 406 | @overload 407 | def nper( 408 | rate: _ArrayLike, 409 | pmt: _ArrayLike, 410 | pv: _ArrayLike, 411 | fv: _ArrayLike = 0, 412 | when: _When = 'end', 413 | ) -> Any: ... 414 | def nper(rate, pmt, pv, fv: Any = 0, when: _When = 'end'): 415 | """Compute the number of periodic payments. 416 | 417 | :class:`decimal.Decimal` type is not supported. 418 | 419 | Parameters 420 | ---------- 421 | rate : array_like 422 | Rate of interest (per period) 423 | pmt : array_like 424 | Payment 425 | pv : array_like 426 | Present value 427 | fv : array_like, optional 428 | Future value 429 | when : {{'begin', 1}, {'end', 0}}, {string, int}, optional 430 | When payments are due ('begin' (1) or 'end' (0)) 431 | 432 | Notes 433 | ----- 434 | The number of periods ``nper`` is computed by solving the equation:: 435 | 436 | fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate*((1+rate)**nper-1) = 0 437 | 438 | but if ``rate = 0`` then:: 439 | 440 | fv + pv + pmt*nper = 0 441 | 442 | Examples 443 | -------- 444 | >>> import numpy as np 445 | >>> import numpy_financial as npf 446 | 447 | If you only had $150/month to pay towards the loan, how long would it take 448 | to pay-off a loan of $8,000 at 7% annual interest? 449 | 450 | >>> print(np.round(npf.nper(0.07/12, -150, 8000), 5)) 451 | 64.07335 452 | 453 | So, over 64 months would be required to pay off the loan. 454 | 455 | The same analysis could be done with several different interest rates 456 | and/or payments and/or total amounts to produce an entire table. 457 | 458 | >>> rates = [0.05, 0.06, 0.07] 459 | >>> payments = [100, 200, 300] 460 | >>> amounts = [7_000, 8_000, 9_000] 461 | >>> npf.nper(rates, payments, amounts).round(3) 462 | array([[[-30.827, -32.987, -34.94 ], 463 | [-20.734, -22.517, -24.158], 464 | [-15.847, -17.366, -18.78 ]], 465 | 466 | [[-28.294, -30.168, -31.857], 467 | [-19.417, -21.002, -22.453], 468 | [-15.025, -16.398, -17.67 ]], 469 | 470 | [[-26.234, -27.891, -29.381], 471 | [-18.303, -19.731, -21.034], 472 | [-14.311, -15.566, -16.722]]]) 473 | """ 474 | when = _convert_when(when) 475 | rates = np.atleast_1d(rate).astype(np.float64) 476 | pmts = np.atleast_1d(pmt).astype(np.float64) 477 | pvs = np.atleast_1d(pv).astype(np.float64) 478 | fvs = np.atleast_1d(fv).astype(np.float64) 479 | whens = np.atleast_1d(when).astype(np.float64) 480 | 481 | out_shape = _get_output_array_shape(rates, pmts, pvs, fvs, whens) 482 | out = np.empty(out_shape) 483 | _cfinancial.nper(rates, pmts, pvs, fvs, whens, out) 484 | return _ufunc_like(out) 485 | 486 | 487 | def _value_like(arr: npt.NDArray[Any], value: Decimal | float) -> Any: 488 | entry = arr.item(0) 489 | if isinstance(entry, Decimal): 490 | return Decimal(value) 491 | return np.array(value, dtype=arr.dtype).item(0) 492 | 493 | @overload 494 | def ipmt( 495 | rate: _AsFloat, 496 | per: _CoFloat, 497 | nper: _CoFloat, 498 | pv: _CoFloat, 499 | fv: _CoFloat = 0, 500 | when: _When = 'end', 501 | ) -> float: ... 502 | @overload 503 | def ipmt( 504 | rate: Decimal, 505 | per: _AsDecimal, 506 | nper: _AsDecimal, 507 | pv: _AsDecimal, 508 | fv: _AsDecimal = 0, 509 | when: _When = 'end', 510 | ) -> Decimal: ... 511 | @overload 512 | def ipmt( 513 | rate: _AsFloat1D, 514 | per: _CoFloatOr1D, 515 | nper: _CoFloatOr1D, 516 | pv: _CoFloatOr1D, 517 | fv: _CoFloatOr1D = 0, 518 | when: _When = 'end', 519 | ) -> npt.NDArray[np.float64]: ... 520 | @overload 521 | def ipmt( 522 | rate: _ArrayLike, 523 | per: _ArrayLike, 524 | nper: _ArrayLike, 525 | pv: _ArrayLike, 526 | fv: _ArrayLike = 0, 527 | when: _When = 'end', 528 | ) -> Any: ... 529 | def ipmt(rate, per, nper, pv, fv: Any = 0, when: _When = 'end') -> Any: 530 | """Compute the interest portion of a payment. 531 | 532 | Parameters 533 | ---------- 534 | rate : scalar or array_like of shape(M, ) 535 | Rate of interest as decimal (not per cent) per period 536 | per : scalar or array_like of shape(M, ) 537 | Interest paid against the loan changes during the life or the loan. 538 | The `per` is the payment period to calculate the interest amount. 539 | nper : scalar or array_like of shape(M, ) 540 | Number of compounding periods 541 | pv : scalar or array_like of shape(M, ) 542 | Present value 543 | fv : scalar or array_like of shape(M, ), optional 544 | Future value 545 | when : {{'begin', 1}, {'end', 0}}, {string, int}, optional 546 | When payments are due ('begin' (1) or 'end' (0)). 547 | Defaults to {'end', 0}. 548 | 549 | Returns 550 | ------- 551 | out : ndarray 552 | Interest portion of payment. If all input is scalar, returns a scalar 553 | float. If any input is array_like, returns interest payment for each 554 | input element. If multiple inputs are array_like, they all must have 555 | the same shape. 556 | 557 | See Also 558 | -------- 559 | ppmt, pmt, pv 560 | 561 | Notes 562 | ----- 563 | The total payment is made up of payment against principal plus interest. 564 | 565 | ``pmt = ppmt + ipmt`` 566 | 567 | Examples 568 | -------- 569 | >>> import numpy as np 570 | >>> import numpy_financial as npf 571 | 572 | What is the amortization schedule for a 1 year loan of $2500 at 573 | 8.24% interest per year compounded monthly? 574 | 575 | >>> principal = 2500.00 576 | 577 | The 'per' variable represents the periods of the loan. Remember that 578 | financial equations start the period count at 1! 579 | 580 | >>> per = np.arange(1*12) + 1 581 | >>> ipmt = npf.ipmt(0.0824/12, per, 1*12, principal) 582 | >>> ppmt = npf.ppmt(0.0824/12, per, 1*12, principal) 583 | 584 | Each element of the sum of the 'ipmt' and 'ppmt' arrays should equal 585 | 'pmt'. 586 | 587 | >>> pmt = npf.pmt(0.0824/12, 1*12, principal) 588 | >>> np.allclose(ipmt + ppmt, pmt) 589 | True 590 | 591 | >>> fmt = '{0:2d} {1:8.2f} {2:8.2f} {3:8.2f}' 592 | >>> for payment in per: 593 | ... index = payment - 1 594 | ... principal = principal + ppmt[index] 595 | ... print(fmt.format(payment, ppmt[index], ipmt[index], principal)) 596 | 1 -200.58 -17.17 2299.42 597 | 2 -201.96 -15.79 2097.46 598 | 3 -203.35 -14.40 1894.11 599 | 4 -204.74 -13.01 1689.37 600 | 5 -206.15 -11.60 1483.22 601 | 6 -207.56 -10.18 1275.66 602 | 7 -208.99 -8.76 1066.67 603 | 8 -210.42 -7.32 856.25 604 | 9 -211.87 -5.88 644.38 605 | 10 -213.32 -4.42 431.05 606 | 11 -214.79 -2.96 216.26 607 | 12 -216.26 -1.49 -0.00 608 | 609 | >>> interestpd = np.sum(ipmt) 610 | >>> np.round(interestpd, 2) 611 | np.float64(-112.98) 612 | 613 | """ 614 | when = _convert_when(when) 615 | rate, per, nper, pv, fv, when = np.broadcast_arrays(rate, per, nper, 616 | pv, fv, when) 617 | 618 | total_pmt = pmt(rate, nper, pv, fv, when) 619 | ipmt_array = np.array(_rbl(rate, per, total_pmt, pv, when) * rate) 620 | 621 | # Payments start at the first period, so payments before that 622 | # don't make any sense. 623 | ipmt_array[per < 1] = _value_like(ipmt_array, np.nan) 624 | # If payments occur at the beginning of a period and this is the 625 | # first period, then no interest has accrued. 626 | per1_and_begin = (when == 1) & (per == 1) 627 | ipmt_array[per1_and_begin] = _value_like(ipmt_array, 0) 628 | # If paying at the beginning we need to discount by one period. 629 | per_gt_1_and_begin = (when == 1) & (per > 1) 630 | ipmt_array[per_gt_1_and_begin] = ( 631 | ipmt_array[per_gt_1_and_begin] / (1 + rate[per_gt_1_and_begin]) 632 | ) 633 | 634 | if np.ndim(ipmt_array) == 0: 635 | # Follow the ufunc convention of returning scalars for scalar 636 | # and 0d array inputs. 637 | return ipmt_array.item(0) 638 | return ipmt_array 639 | 640 | 641 | @overload 642 | def _rbl( 643 | rate: _AsFloat, 644 | per: _CoFloat, 645 | pmt: _CoFloat, 646 | pv: _CoFloat, 647 | when: _When, 648 | ) -> float: ... 649 | @overload 650 | def _rbl( 651 | rate: Decimal, 652 | per: _AsDecimal, 653 | pmt: _AsDecimal, 654 | pv: _AsDecimal, 655 | when: _When, 656 | ) -> Decimal: ... 657 | @overload 658 | def _rbl( 659 | rate: _AsFloat1D, 660 | per: _CoFloatOr1D, 661 | pmt: _CoFloatOr1D, 662 | pv: _CoFloatOr1D, 663 | when: _When, 664 | ) -> npt.NDArray[np.float64]: ... 665 | @overload 666 | def _rbl( 667 | rate: _ArrayLike, 668 | per: _ArrayLike, 669 | pmt: _ArrayLike, 670 | pv: _ArrayLike, 671 | when: _When, 672 | ) -> Any: ... 673 | def _rbl(rate, per, pmt, pv, when: _When): 674 | """Remaining balance on loan. 675 | 676 | This function is here to simply have a different name for the 'fv' 677 | function to not interfere with the 'fv' keyword argument within the 'ipmt' 678 | function. It is the 'remaining balance on loan' which might be useful as 679 | it's own function, but is easily calculated with the 'fv' function. 680 | """ 681 | return fv(rate, (per - 1), pmt, pv, when) 682 | 683 | 684 | @overload 685 | def ppmt( 686 | rate: _AsFloat, 687 | per: _CoFloat, 688 | nper: _CoFloat, 689 | pv: _CoFloat, 690 | fv: _CoFloat = 0, 691 | when: _When = 'end', 692 | ) -> float: ... 693 | @overload 694 | def ppmt( 695 | rate: Decimal, 696 | per: _AsDecimal, 697 | nper: _AsDecimal, 698 | pv: _AsDecimal, 699 | fv: _AsDecimal = 0, 700 | when: _When = 'end', 701 | ) -> Decimal: ... 702 | @overload 703 | def ppmt( 704 | rate: _AsFloat1D, 705 | per: _CoFloatOr1D, 706 | nper: _CoFloatOr1D, 707 | pv: _CoFloatOr1D, 708 | fv: _CoFloatOr1D = 0, 709 | when: _When = 'end', 710 | ) -> npt.NDArray[np.float64]: ... 711 | @overload 712 | def ppmt( 713 | rate: _ArrayLike, 714 | per: _ArrayLike, 715 | nper: _ArrayLike, 716 | pv: _ArrayLike, 717 | fv: _ArrayLike = 0, 718 | when: _When = 'end', 719 | ) -> Any: ... 720 | def ppmt(rate, per, nper, pv, fv: Any = 0, when: _When = 'end'): 721 | """Compute the payment against loan principal. 722 | 723 | Parameters 724 | ---------- 725 | rate : array_like 726 | Rate of interest (per period) 727 | per : array_like, int 728 | Amount paid against the loan changes. The `per` is the period of 729 | interest. 730 | nper : array_like 731 | Number of compounding periods 732 | pv : array_like 733 | Present value 734 | fv : array_like, optional 735 | Future value 736 | when : {{'begin', 1}, {'end', 0}}, {string, int} 737 | When payments are due ('begin' (1) or 'end' (0)) 738 | 739 | See Also 740 | -------- 741 | pmt, pv, ipmt 742 | 743 | """ 744 | total = pmt(rate, nper, pv, fv, when) 745 | return total - ipmt(rate, per, nper, pv, fv, when) 746 | 747 | 748 | @overload 749 | def pv( 750 | rate: _AsFloat, 751 | nper: _CoFloat, 752 | pmt: _CoFloat, 753 | fv: _CoFloat = 0, 754 | when: _When = 'end', 755 | ) -> np.float64: ... 756 | @overload 757 | def pv( 758 | rate: Decimal, 759 | nper: _AsDecimal, 760 | pmt: _AsDecimal, 761 | fv: _AsDecimal = 0, 762 | when: _When = 'end', 763 | ) -> Decimal: ... 764 | @overload 765 | def pv( 766 | rate: _AsFloatND, 767 | nper: _CoFloatOrND, 768 | pmt: _CoFloatOrND, 769 | fv: _CoFloatOrND = 0, 770 | when: _When = 'end', 771 | ) -> npt.NDArray[np.float64]: ... 772 | @overload 773 | def pv( 774 | rate: _ArrayLike, 775 | nper: _ArrayLike, 776 | pmt: _ArrayLike, 777 | fv: _ArrayLike = 0, 778 | when: _When = 'end', 779 | ) -> Any: ... 780 | def pv(rate, nper, pmt, fv: Any = 0, when: _When = 'end'): 781 | """Compute the present value. 782 | 783 | Given: 784 | * a future value, `fv` 785 | * an interest `rate` compounded once per period, of which 786 | there are 787 | * `nper` total 788 | * a (fixed) payment, `pmt`, paid either 789 | * at the beginning (`when` = {'begin', 1}) or the end 790 | (`when` = {'end', 0}) of each period 791 | 792 | Return: 793 | the value now 794 | 795 | Parameters 796 | ---------- 797 | rate : array_like 798 | Rate of interest (per period) 799 | nper : array_like 800 | Number of compounding periods 801 | pmt : array_like 802 | Payment 803 | fv : array_like, optional 804 | Future value 805 | when : {{'begin', 1}, {'end', 0}}, {string, int}, optional 806 | When payments are due ('begin' (1) or 'end' (0)) 807 | 808 | Returns 809 | ------- 810 | out : ndarray, float 811 | Present value of a series of payments or investments. 812 | 813 | Notes 814 | ----- 815 | The present value is computed by solving the equation:: 816 | 817 | fv + 818 | pv*(1 + rate)**nper + 819 | pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) = 0 820 | 821 | or, when ``rate = 0``:: 822 | 823 | fv + pv + pmt * nper = 0 824 | 825 | for `pv`, which is then returned. 826 | 827 | References 828 | ---------- 829 | .. [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). 830 | Open Document Format for Office Applications (OpenDocument)v1.2, 831 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version, 832 | Pre-Draft 12. Organization for the Advancement of Structured Information 833 | Standards (OASIS). Billerica, MA, USA. [ODT Document]. 834 | Available: 835 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula 836 | OpenDocument-formula-20090508.odt 837 | 838 | Examples 839 | -------- 840 | >>> import numpy as np 841 | >>> import numpy_financial as npf 842 | 843 | What is the present value (e.g., the initial investment) 844 | of an investment that needs to total $15692.93 845 | after 10 years of saving $100 every month? Assume the 846 | interest rate is 5% (annually) compounded monthly. 847 | 848 | >>> npf.pv(0.05/12, 10*12, -100, 15692.93) 849 | np.float64(-100.00067131625819) 850 | 851 | By convention, the negative sign represents cash flow out 852 | (i.e., money not available today). Thus, to end up with 853 | $15,692.93 in 10 years saving $100 a month at 5% annual 854 | interest, one's initial deposit should also be $100. 855 | 856 | If any input is array_like, ``pv`` returns an array of equal shape. 857 | Let's compare different interest rates in the example above: 858 | 859 | >>> a = np.array((0.05, 0.04, 0.03))/12 860 | >>> npf.pv(a, 10*12, -100, 15692.93) 861 | array([ -100.00067132, -649.26771385, -1273.78633713]) 862 | 863 | So, to end up with the same $15692.93 under the same $100 per month 864 | "savings plan," for annual interest rates of 4% and 3%, one would 865 | need initial investments of $649.27 and $1273.79, respectively. 866 | 867 | """ 868 | when = _convert_when(when) 869 | rate, nper, pmt, fv, when = map(np.asarray, [rate, nper, pmt, fv, when]) 870 | temp = (1 + rate) ** nper 871 | fact = np.where(rate == 0, nper, (1 + rate * when) * (temp - 1) / rate) 872 | return -(fv + pmt * fact) / temp 873 | 874 | 875 | # Computed with Sage 876 | # (y + (r + 1)^n*x + p*((r + 1)^n - 1)*(r*w + 1)/r)/(n*(r + 1)^(n - 1)*x - 877 | # p*((r + 1)^n - 1)*(r*w + 1)/r^2 + n*p*(r + 1)^(n - 1)*(r*w + 1)/r + 878 | # p*((r + 1)^n - 1)*w/r) 879 | 880 | 881 | def _g_div_gp(r, n, p, x, y, w) -> Any: 882 | # Evaluate g(r_n)/g'(r_n), where g = 883 | # fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate * ((1+rate)**nper - 1) 884 | t1 = (r + 1) ** n 885 | t2 = (r + 1) ** (n - 1) 886 | g = y + t1 * x + p * (t1 - 1) * (r * w + 1) / r 887 | gp = (n * t2 * x 888 | - p * (t1 - 1) * (r * w + 1) / (r ** 2) 889 | + n * p * t2 * (r * w + 1) / r 890 | + p * (t1 - 1) * w / r) 891 | return g / gp 892 | 893 | 894 | # Use Newton's iteration until the change is less than 1e-6 895 | # for all values or a maximum of 100 iterations is reached. 896 | # Newton's rule is 897 | # r_{n+1} = r_{n} - g(r_n)/g'(r_n) 898 | # where 899 | # g(r) is the formula 900 | # g'(r) is the derivative with respect to r. 901 | def rate( 902 | nper, 903 | pmt, 904 | pv, 905 | fv, 906 | when: _When = 'end', 907 | guess: float | Decimal | None = None, 908 | tol: float | Decimal | None = None, 909 | maxiter: int = 100, 910 | *, 911 | raise_exceptions: bool = False, 912 | ) -> Any: 913 | """Compute the rate of interest per period. 914 | 915 | Parameters 916 | ---------- 917 | nper : array_like 918 | Number of compounding periods 919 | pmt : array_like 920 | Payment 921 | pv : array_like 922 | Present value 923 | fv : array_like 924 | Future value 925 | when : {{'begin', 1}, {'end', 0}}, {string, int}, optional 926 | When payments are due ('begin' (1) or 'end' (0)) 927 | guess : Number, optional 928 | Starting guess for solving the rate of interest, default 0.1 929 | tol : Number, optional 930 | Required tolerance for the solution, default 1e-6 931 | maxiter : int, optional 932 | Maximum iterations in finding the solution 933 | raise_exceptions: bool, optional 934 | Flag to raise an exception when at least one of the rates 935 | cannot be computed due to having reached the maximum number of 936 | iterations (IterationsExceededException). Set to False as default, 937 | thus returning NaNs for those rates. 938 | 939 | Notes 940 | ----- 941 | The rate of interest is computed by iteratively solving the 942 | (non-linear) equation:: 943 | 944 | fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate * ((1+rate)**nper - 1) = 0 945 | 946 | for ``rate``. 947 | 948 | References 949 | ---------- 950 | Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). Open Document 951 | Format for Office Applications (OpenDocument)v1.2, Part 2: Recalculated 952 | Formula (OpenFormula) Format - Annotated Version, Pre-Draft 12. 953 | Organization for the Advancement of Structured Information Standards 954 | (OASIS). Billerica, MA, USA. [ODT Document]. Available: 955 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula 956 | OpenDocument-formula-20090508.odt 957 | 958 | """ 959 | when = _convert_when(when) 960 | default_type = Decimal if isinstance(pmt, Decimal) else float 961 | 962 | # Handle casting defaults to Decimal if/when pmt is a Decimal and 963 | # guess and/or tol are not given default values 964 | if guess is None: 965 | guess = default_type('0.1') 966 | 967 | if tol is None: 968 | tol = default_type('1e-6') 969 | 970 | nper, pmt, pv, fv, when = map(np.asarray, [nper, pmt, pv, fv, when]) 971 | 972 | rn: Any = guess 973 | iterator = 0 974 | close: Any = False 975 | while (iterator < maxiter) and not np.all(close): 976 | rnp1 = rn - _g_div_gp(rn, nper, pmt, pv, fv, when) 977 | diff = abs(rnp1 - rn) 978 | close = diff < tol 979 | iterator += 1 980 | rn = rnp1 981 | 982 | if not np.all(close): 983 | if np.isscalar(rn): 984 | if raise_exceptions: 985 | raise IterationsExceededError('Maximum number of iterations exceeded.') 986 | return default_type(np.nan) 987 | else: 988 | # Return nan's in array of the same shape as rn 989 | # where the solution is not close to tol. 990 | if raise_exceptions: 991 | raise IterationsExceededError(f'Maximum iterations exceeded in ' 992 | f'{len(close) - close.sum()} rate(s).') 993 | rn[~close] = np.nan 994 | return rn 995 | 996 | 997 | def _irr_default_selection(eirr: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: 998 | """ default selection logic for IRR function when there are > 1 real solutions """ 999 | # check sign of all IRR solutions 1000 | same_sign = np.all(eirr > 0) if eirr[0] > 0 else np.all(eirr < 0) 1001 | 1002 | # if the signs of IRR solutions are not the same, first filter potential IRR 1003 | # by comparing the total positive and negative cash flows. 1004 | if not same_sign: 1005 | pos = sum(eirr[eirr > 0]) 1006 | neg = sum(eirr[eirr < 0]) 1007 | if pos >= neg: 1008 | eirr = eirr[eirr >= 0] 1009 | else: 1010 | eirr = eirr[eirr < 0] 1011 | 1012 | # pick the smallest one in magnitude and return 1013 | abs_eirr = np.abs(eirr) 1014 | return eirr[np.argmin(abs_eirr)] 1015 | 1016 | 1017 | _SelectionFunc: TypeAlias = Callable[ 1018 | [npt.NDArray[np.float64]], 1019 | npt.NDArray[np.float64], 1020 | ] 1021 | 1022 | 1023 | @overload 1024 | def irr( 1025 | values: Sequence[_CoFloat], 1026 | *, 1027 | raise_exceptions: bool = False, 1028 | selection_logic: _SelectionFunc = ..., 1029 | ) -> float: ... 1030 | @overload 1031 | def irr( 1032 | values: Sequence[Sequence[_CoFloat]], 1033 | *, 1034 | raise_exceptions: bool = False, 1035 | selection_logic: _SelectionFunc = ..., 1036 | ) -> npt.NDArray[np.float64]: ... 1037 | @overload 1038 | def irr( 1039 | values: _ArrayLike, 1040 | *, 1041 | raise_exceptions: bool = False, 1042 | selection_logic: _SelectionFunc = ..., 1043 | ) -> Any: ... 1044 | def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selection): 1045 | r"""Return the Internal Rate of Return (IRR). 1046 | 1047 | This is the "average" periodically compounded rate of return 1048 | that gives a net present value of 0.0; for a more complete explanation, 1049 | see Notes below. 1050 | 1051 | :class:`decimal.Decimal` type is not supported. 1052 | 1053 | Parameters 1054 | ---------- 1055 | values : array_like, shape(N,) 1056 | Input cash flows per time period. By convention, net "deposits" 1057 | are negative and net "withdrawals" are positive. Thus, for 1058 | example, at least the first element of `values`, which represents 1059 | the initial investment, will typically be negative. 1060 | raise_exceptions: bool, optional 1061 | Flag to raise an exception when the irr cannot be computed due to 1062 | either having all cashflows of the same sign (NoRealSolutionException) or 1063 | having reached the maximum number of iterations (IterationsExceededException). 1064 | Set to False as default, thus returning NaNs in the two previous 1065 | cases. 1066 | selection_logic: function, optional 1067 | Function for selection logic when more than 1 real solutions is found. 1068 | User may insert their own customised function for selection 1069 | of IRR values.The function should accept a one-dimensional array 1070 | of numbers and return a number. 1071 | 1072 | 1073 | Returns 1074 | ------- 1075 | out : float 1076 | Internal Rate of Return for periodic input values. 1077 | 1078 | Notes 1079 | ----- 1080 | The IRR is perhaps best understood through an example (illustrated 1081 | using np.irr in the Examples section below). Suppose one invests 100 1082 | units and then makes the following withdrawals at regular (fixed) 1083 | intervals: 39, 59, 55, 20. Assuming the ending value is 0, one's 100 1084 | unit investment yields 173 units; however, due to the combination of 1085 | compounding and the periodic withdrawals, the "average" rate of return 1086 | is neither simply 0.73/4 nor (1.73)^0.25-1. Rather, it is the solution 1087 | (for :math:`r`) of the equation: 1088 | 1089 | .. math:: -100 + \\frac{39}{1+r} + \\frac{59}{(1+r)^2} 1090 | + \\frac{55}{(1+r)^3} + \\frac{20}{(1+r)^4} = 0 1091 | 1092 | In general, for `values` :math:`= [v_0, v_1, ... v_M]`, 1093 | irr is the solution of the equation: [G]_ 1094 | 1095 | .. math:: \\sum_{t=0}^M{\\frac{v_t}{(1+irr)^{t}}} = 0 1096 | 1097 | References 1098 | ---------- 1099 | .. [G] L. J. Gitman, "Principles of Managerial Finance, Brief," 3rd ed., 1100 | Addison-Wesley, 2003, pg. 348. 1101 | 1102 | Examples 1103 | -------- 1104 | >>> import numpy_financial as npf 1105 | 1106 | >>> round(npf.irr([-100, 39, 59, 55, 20]), 5) 1107 | 0.28095 1108 | >>> round(npf.irr([-100, 0, 0, 74]), 5) 1109 | -0.0955 1110 | >>> round(npf.irr([-100, 100, 0, -7]), 5) 1111 | -0.0833 1112 | >>> round(npf.irr([-100, 100, 0, 7]), 5) 1113 | 0.06206 1114 | >>> round(npf.irr([-5, 10.5, 1, -8, 1]), 5) 1115 | 0.0886 1116 | >>> npf.irr([[-100, 0, 0, 74], [-100, 100, 0, 7]]).round(5) 1117 | array([-0.0955 , 0.06206]) 1118 | 1119 | """ 1120 | values = np.atleast_2d(values) 1121 | if values.ndim != 2: 1122 | raise ValueError("Cashflows must be a 2D array") 1123 | 1124 | irr_results = np.empty(values.shape[0]) 1125 | for i, row in enumerate(values): 1126 | # If all values are of the same sign, no solution exists 1127 | # We don't perform any further calculations and exit early 1128 | same_sign = np.all(row > 0) if row[0] > 0 else np.all(row < 0) 1129 | if same_sign: 1130 | if raise_exceptions: 1131 | raise NoRealSolutionError('No real solution exists for IRR since all ' 1132 | 'cashflows are of the same sign.') 1133 | irr_results[i] = np.nan 1134 | 1135 | # We aim to solve eirr such that NPV is exactly zero. This can be framed as 1136 | # simply finding the closest root of a polynomial to a given initial guess 1137 | # as follows: 1138 | # V0 V1 V2 V3 1139 | # NPV = ---------- + ---------- + ---------- + ---------- + ... = 0 1140 | # (1+eirr)^0 (1+eirr)^1 (1+eirr)^2 (1+eirr)^3 1141 | # 1142 | # by letting g = (1+eirr), we substitute to get 1143 | # 1144 | # NPV = V0 * 1/g^0 + V1 * 1/g^1 + V2 * 1/x^2 + V3 * 1/g^3 + ... = 0 1145 | # 1146 | # Multiplying by g^N this becomes 1147 | # 1148 | # V0 * g^N + V1 * g^{N-1} + V2 * g^{N-2} + V3 * g^{N-3} + ... = 0 1149 | # 1150 | # which we solve using Newton-Raphson and then reverse out the solution 1151 | # as eirr = g - 1 (if we are close enough to a solution) 1152 | else: 1153 | g = np.roots(row) 1154 | eirr = np.real(g[np.isreal(g)]) - 1 1155 | 1156 | # Realistic IRR 1157 | eirr = eirr[eirr >= -1] 1158 | 1159 | # If no real solution 1160 | if len(eirr) == 0: 1161 | if raise_exceptions: 1162 | raise NoRealSolutionError("No real solution is found for IRR.") 1163 | irr_results[i] = np.nan 1164 | # If only one real solution 1165 | elif len(eirr) == 1: 1166 | irr_results[i] = eirr[0] 1167 | else: 1168 | irr_results[i] = selection_logic(eirr) 1169 | 1170 | return _ufunc_like(irr_results) 1171 | 1172 | 1173 | @overload 1174 | def npv(rate: _CoNumeric, values: _CoNumericND) -> float: ... 1175 | @overload 1176 | def npv(rate: _CoNumeric1D, values: _CoNumericND) -> npt.NDArray[np.float64]: ... 1177 | @overload 1178 | def npv(rate: _ArrayLike, values: _ArrayLike) -> Any: ... 1179 | def npv(rate, values): 1180 | r"""Return the NPV (Net Present Value) of a cash flow series. 1181 | 1182 | Parameters 1183 | ---------- 1184 | rate : scalar or array_like shape(K, ) 1185 | The discount rate. 1186 | values : array_like, shape(M, ) or shape(M, N) 1187 | The values of the time series of cash flows. The (fixed) time 1188 | interval between cash flow "events" must be the same as that for 1189 | which `rate` is given (i.e., if `rate` is per year, then precisely 1190 | a year is understood to elapse between each cash flow event). By 1191 | convention, investments or "deposits" are negative, income or 1192 | "withdrawals" are positive; `values` must begin with the initial 1193 | investment, thus `values[0]` will typically be negative. 1194 | 1195 | Returns 1196 | ------- 1197 | out : float or array shape(K, M) 1198 | The NPV of the input cash flow series `values` at the discount 1199 | `rate`. `out` follows the ufunc convention of returning scalars 1200 | instead of single element arrays. 1201 | 1202 | Warnings 1203 | -------- 1204 | ``npv`` considers a series of cashflows starting in the present (t = 0). 1205 | NPV can also be defined with a series of future cashflows, paid at the 1206 | end, rather than the start, of each period. If future cashflows are used, 1207 | the first cashflow `values[0]` must be zeroed and added to the net 1208 | present value of the future cashflows. This is demonstrated in the 1209 | examples. 1210 | 1211 | Notes 1212 | ----- 1213 | Returns the result of: [G]_ 1214 | 1215 | .. math :: \\sum_{t=0}^{M-1}{\\frac{values_t}{(1+rate)^{t}}} 1216 | 1217 | References 1218 | ---------- 1219 | .. [G] L. J. Gitman, "Principles of Managerial Finance, Brief," 3rd ed., 1220 | Addison-Wesley, 2003, pg. 346. 1221 | 1222 | Examples 1223 | -------- 1224 | >>> import numpy as np 1225 | >>> import numpy_financial as npf 1226 | 1227 | Consider a potential project with an initial investment of $40 000 and 1228 | projected cashflows of $5 000, $8 000, $12 000 and $30 000 at the end of 1229 | each period discounted at a rate of 8% per period. To find the project's 1230 | net present value: 1231 | 1232 | >>> rate, cashflows = 0.08, [-40_000, 5_000, 8_000, 12_000, 30_000] 1233 | >>> np.round(npf.npv(rate, cashflows), 5) 1234 | np.float64(3065.22267) 1235 | 1236 | It may be preferable to split the projected cashflow into an initial 1237 | investment and expected future cashflows. In this case, the value of 1238 | the initial cashflow is zero and the initial investment is later added 1239 | to the future cashflows net present value: 1240 | 1241 | >>> initial_cashflow = cashflows[0] 1242 | >>> cashflows[0] = 0 1243 | >>> np.round(npf.npv(rate, cashflows) + initial_cashflow, 5) 1244 | np.float64(3065.22267) 1245 | 1246 | The NPV calculation may be applied to several ``rates`` and ``cashflows`` 1247 | simulatneously. This produces an array of shape ``(len(rates), len(cashflows))``. 1248 | 1249 | >>> rates = [0.00, 0.05, 0.10] 1250 | >>> cashflows = [[-4_000, 500, 800], [-5_000, 600, 900]] 1251 | >>> npf.npv(rates, cashflows).round(2) 1252 | array([[-2700. , -3500. ], 1253 | [-2798.19, -3612.24], 1254 | [-2884.3 , -3710.74]]) 1255 | """ 1256 | values_inner = np.atleast_2d(values).astype(np.float64) 1257 | rate_inner = np.atleast_1d(rate).astype(np.float64) 1258 | 1259 | if rate_inner.ndim != 1: 1260 | msg = "invalid shape for rates. Rate must be either a scalar or 1d array" 1261 | raise ValueError(msg) 1262 | 1263 | if values_inner.ndim != 2: 1264 | msg = "invalid shape for values. Values must be either a 1d or 2d array" 1265 | raise ValueError(msg) 1266 | 1267 | output_shape = _get_output_array_shape(rate_inner, values_inner) 1268 | out = np.empty(output_shape) 1269 | _cfinancial.npv(rate_inner, values_inner, out) 1270 | return _ufunc_like(out) 1271 | 1272 | 1273 | @overload 1274 | def mirr( 1275 | values: _CoNumericND, 1276 | finance_rate: _CoNumeric, 1277 | reinvest_rate: _CoNumeric, 1278 | *, 1279 | raise_exceptions: bool = False, 1280 | ) -> float: ... 1281 | @overload 1282 | def mirr( 1283 | values: _CoNumericND, 1284 | finance_rate: _CoNumeric1D, 1285 | reinvest_rate: _CoNumeric1D, 1286 | *, 1287 | raise_exceptions: bool = False, 1288 | ) -> npt.NDArray[np.float64]: ... 1289 | @overload 1290 | def mirr( 1291 | values: _ArrayLike, 1292 | finance_rate: _ArrayLike, 1293 | reinvest_rate: _ArrayLike, 1294 | *, 1295 | raise_exceptions: bool = False, 1296 | ) -> Any: ... 1297 | def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions: bool = False): 1298 | r""" 1299 | Return the Modified Internal Rate of Return (MIRR). 1300 | 1301 | MIRR is a financial metric that takes into account both the cost of 1302 | the investment and the return on reinvested cash flows. It is useful 1303 | for evaluating the profitability of an investment with multiple cash 1304 | inflows and outflows. 1305 | 1306 | Parameters 1307 | ---------- 1308 | values : array_like, 1D or 2D 1309 | Cash flows, where the first value is considered a sunk cost at time zero. 1310 | It must contain at least one positive and one negative value. 1311 | finance_rate : scalar or 1D array 1312 | Interest rate paid on the cash flows. 1313 | reinvest_rate : scalar or D array 1314 | Interest rate received on the cash flows upon reinvestment. 1315 | raise_exceptions: bool, optional 1316 | Flag to raise an exception when the MIRR cannot be computed due to 1317 | having all cash flows of the same sign (NoRealSolutionException). 1318 | Set to False as default,thus returning NaNs in the previous case. 1319 | 1320 | Returns 1321 | ------- 1322 | out : float or 2D array 1323 | Modified internal rate of return 1324 | 1325 | Notes 1326 | ----- 1327 | The MIRR formula is as follows: 1328 | 1329 | .. math:: 1330 | 1331 | MIRR = 1332 | \\left( \\frac{{FV_{positive}}}{{PV_{negative}}} \\right)^{\\frac{{1}}{{n-1}}} 1333 | * (1+r) - 1 1334 | 1335 | where: 1336 | - \(FV_{positive}\) is the future value of positive cash flows, 1337 | - \(PV_{negative}\) is the present value of negative cash flows, 1338 | - \(n\) is the number of periods. 1339 | - \(r\) is the reinvestment rate. 1340 | 1341 | Examples 1342 | -------- 1343 | >>> import numpy_financial as npf 1344 | 1345 | Consider a project with an initial investment of -$100 1346 | and projected cash flows of $50, -$60, and $70 at the end of each period. 1347 | The project has a finance rate of 10% and a reinvestment rate of 12%. 1348 | 1349 | >>> npf.mirr([-100, 50, -60, 70], 0.10, 0.12) 1350 | -0.03909366594356467 1351 | 1352 | It is also possible to supply multiple cashflows or pairs of 1353 | finance and reinvstment rates, note that in this case the number of elements 1354 | in each of the rates arrays must match. 1355 | 1356 | >>> values = [ 1357 | ... [-4500, -800, 800, 800, 600], 1358 | ... [-120000, 39000, 30000, 21000, 37000], 1359 | ... [100, 200, -50, 300, -200], 1360 | ... ] 1361 | >>> finance_rate = [0.05, 0.08, 0.10] 1362 | >>> reinvestment_rate = [0.08, 0.10, 0.12] 1363 | >>> npf.mirr(values, finance_rate, reinvestment_rate) 1364 | array([[-0.1784449 , -0.17328716, -0.1684366 ], 1365 | [ 0.04627293, 0.05437856, 0.06252201], 1366 | [ 0.35712458, 0.40628857, 0.44435295]]) 1367 | 1368 | Now, let's consider the scenario where all cash flows are negative. 1369 | 1370 | >>> npf.mirr([-100, -50, -60, -70], 0.10, 0.12) 1371 | nan 1372 | 1373 | Finally, let's explore the situation where all cash flows are positive, 1374 | and the `raise_exceptions` parameter is set to True. 1375 | 1376 | >>> npf.mirr([ 1377 | ... 100, 50, 60, 70], 1378 | ... 0.10, 0.12, 1379 | ... raise_exceptions=True 1380 | ... ) #doctest: +NORMALIZE_WHITESPACE 1381 | Traceback (most recent call last): 1382 | ... 1383 | numpy_financial._financial.NoRealSolutionError: 1384 | No real solution exists for MIRR since all cashflows are of the same sign. 1385 | """ 1386 | values_inner = np.atleast_2d(values).astype(np.float64) 1387 | finance_rate_inner = np.atleast_1d(finance_rate).astype(np.float64) 1388 | reinvest_rate_inner = np.atleast_1d(reinvest_rate).astype(np.float64) 1389 | n = values_inner.shape[1] 1390 | 1391 | if finance_rate_inner.size != reinvest_rate_inner.size: 1392 | if raise_exceptions: 1393 | raise ValueError("finance_rate and reinvest_rate must have the same size") 1394 | return np.nan 1395 | 1396 | out_shape = _get_output_array_shape(values_inner, finance_rate_inner) 1397 | out = np.empty(out_shape) 1398 | 1399 | for i, v in enumerate(values_inner): 1400 | for j, (rr, fr) in enumerate( 1401 | zip(reinvest_rate_inner, finance_rate_inner, strict=True) 1402 | ): 1403 | pos = v > 0 1404 | neg = v < 0 1405 | 1406 | if not (pos.any() and neg.any()): 1407 | if raise_exceptions: 1408 | raise NoRealSolutionError("No real solution exists for MIRR since" 1409 | " all cashflows are of the same sign.") 1410 | out[i, j] = np.nan 1411 | else: 1412 | numer = np.abs(npv(rr, v * pos)) 1413 | denom = np.abs(npv(fr, v * neg)) 1414 | out[i, j] = (numer / denom) ** (1 / (n - 1)) * (1 + rr) - 1 1415 | return _ufunc_like(out) 1416 | -------------------------------------------------------------------------------- /numpy_financial/meson.build: -------------------------------------------------------------------------------- 1 | py.extension_module( 2 | '_cfinancial', 3 | '_cfinancial.pyx', 4 | install: true, 5 | subdir: 'numpy_financial', 6 | ) 7 | 8 | python_sources = [ 9 | '__init__.py', 10 | '_financial.py', 11 | '_cfinancial.pyi', 12 | 'py.typed', 13 | ] 14 | 15 | py.install_sources( 16 | python_sources, 17 | subdir: 'numpy_financial', 18 | ) 19 | 20 | install_subdir('tests', install_dir: py.get_install_dir() / 'numpy_financial') 21 | -------------------------------------------------------------------------------- /numpy_financial/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /numpy_financial/tests/strategies.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from hypothesis import strategies as st 3 | from hypothesis.extra import numpy as npst 4 | 5 | real_scalar_dtypes = st.one_of( 6 | npst.floating_dtypes(), 7 | npst.integer_dtypes(), 8 | npst.unsigned_integer_dtypes() 9 | ) 10 | nicely_behaved_doubles = npst.from_dtype( 11 | np.dtype("f8"), 12 | allow_nan=False, 13 | allow_infinity=False, 14 | allow_subnormal=False, 15 | ) 16 | cashflow_array_strategy = npst.arrays( 17 | dtype=npst.floating_dtypes(sizes=64), 18 | shape=npst.array_shapes(min_dims=1, max_dims=2, min_side=0, max_side=25), 19 | elements=nicely_behaved_doubles, 20 | ) 21 | cashflow_list_strategy = cashflow_array_strategy.map(lambda x: x.tolist()) 22 | cashflow_array_like_strategy = st.one_of( 23 | cashflow_array_strategy, 24 | cashflow_list_strategy, 25 | ) 26 | short_nicely_behaved_doubles = npst.arrays( 27 | dtype=npst.floating_dtypes(sizes=64), 28 | shape=npst.array_shapes(min_dims=0, max_dims=1, min_side=0, max_side=5), 29 | elements=nicely_behaved_doubles, 30 | ) 31 | 32 | when_strategy = st.sampled_from( 33 | ['end', 'begin', 'e', 'b', 0, 1, 'beginning', 'start', 'finish'] 34 | ) 35 | -------------------------------------------------------------------------------- /numpy_financial/tests/test_financial.py: -------------------------------------------------------------------------------- 1 | import math 2 | import warnings 3 | from decimal import Decimal 4 | 5 | # Don't use 'import numpy as np', to avoid accidentally testing 6 | # the versions in numpy instead of numpy_financial. 7 | import numpy 8 | import pytest 9 | from hypothesis import assume, given 10 | from numpy.testing import ( 11 | assert_, 12 | assert_allclose, 13 | assert_equal, 14 | assert_raises, 15 | ) 16 | 17 | import numpy_financial as npf 18 | from numpy_financial.tests.strategies import ( 19 | cashflow_array_like_strategy, 20 | cashflow_array_strategy, 21 | short_nicely_behaved_doubles, 22 | when_strategy, 23 | ) 24 | 25 | 26 | def assert_decimal_close(actual, expected, tol=Decimal("1e-7")): 27 | # Check if both actual and expected are iterable (like arrays) 28 | if hasattr(actual, "__iter__") and hasattr(expected, "__iter__"): 29 | for a, e in zip(actual, expected, strict=True): 30 | assert abs(a - e) <= tol 31 | else: 32 | # For single value comparisons 33 | assert abs(actual - expected) <= tol 34 | 35 | 36 | class TestFinancial(object): 37 | def test_when(self): 38 | # begin 39 | assert_equal( 40 | npf.rate(10, 20, -3500, 10000, 1), npf.rate(10, 20, -3500, 10000, "begin") 41 | ) 42 | # end 43 | assert_equal( 44 | npf.rate(10, 20, -3500, 10000), npf.rate(10, 20, -3500, 10000, "end") 45 | ) 46 | assert_equal( 47 | npf.rate(10, 20, -3500, 10000, 0), npf.rate(10, 20, -3500, 10000, "end") 48 | ) 49 | 50 | # begin 51 | assert_equal(npf.pv(0.07, 20, 12000, 0, 1), npf.pv(0.07, 20, 12000, 0, "begin")) 52 | # end 53 | assert_equal(npf.pv(0.07, 20, 12000, 0), npf.pv(0.07, 20, 12000, 0, "end")) 54 | assert_equal(npf.pv(0.07, 20, 12000, 0, 0), npf.pv(0.07, 20, 12000, 0, "end")) 55 | 56 | # begin 57 | assert_equal( 58 | npf.pmt(0.08 / 12, 5 * 12, 15000.0, 0, 1), 59 | npf.pmt(0.08 / 12, 5 * 12, 15000.0, 0, "begin"), 60 | ) 61 | # end 62 | assert_equal( 63 | npf.pmt(0.08 / 12, 5 * 12, 15000.0, 0), 64 | npf.pmt(0.08 / 12, 5 * 12, 15000.0, 0, "end"), 65 | ) 66 | assert_equal( 67 | npf.pmt(0.08 / 12, 5 * 12, 15000.0, 0, 0), 68 | npf.pmt(0.08 / 12, 5 * 12, 15000.0, 0, "end"), 69 | ) 70 | 71 | # begin 72 | assert_equal( 73 | npf.nper(0.075, -2000, 0, 100000.0, 1), 74 | npf.nper(0.075, -2000, 0, 100000.0, "begin"), 75 | ) 76 | # end 77 | assert_equal( 78 | npf.nper(0.075, -2000, 0, 100000.0), 79 | npf.nper(0.075, -2000, 0, 100000.0, "end"), 80 | ) 81 | assert_equal( 82 | npf.nper(0.075, -2000, 0, 100000.0, 0), 83 | npf.nper(0.075, -2000, 0, 100000.0, "end"), 84 | ) 85 | 86 | def test_decimal_with_when(self): 87 | """ 88 | Test that decimals are still supported if the when argument is passed 89 | """ 90 | # begin 91 | assert_equal( 92 | npf.rate( 93 | Decimal("10"), 94 | Decimal("20"), 95 | Decimal("-3500"), 96 | Decimal("10000"), 97 | Decimal("1"), 98 | ), 99 | npf.rate( 100 | Decimal("10"), 101 | Decimal("20"), 102 | Decimal("-3500"), 103 | Decimal("10000"), 104 | "begin", 105 | ), 106 | ) 107 | # end 108 | assert_equal( 109 | npf.rate(Decimal("10"), Decimal("20"), Decimal("-3500"), Decimal("10000")), 110 | npf.rate( 111 | Decimal("10"), Decimal("20"), Decimal("-3500"), Decimal("10000"), "end" 112 | ), 113 | ) 114 | assert_equal( 115 | npf.rate( 116 | Decimal("10"), 117 | Decimal("20"), 118 | Decimal("-3500"), 119 | Decimal("10000"), 120 | Decimal("0"), 121 | ), 122 | npf.rate( 123 | Decimal("10"), Decimal("20"), Decimal("-3500"), Decimal("10000"), "end" 124 | ), 125 | ) 126 | 127 | # begin 128 | assert_equal( 129 | npf.pv( 130 | Decimal("0.07"), 131 | Decimal("20"), 132 | Decimal("12000"), 133 | Decimal("0"), 134 | Decimal("1"), 135 | ), 136 | npf.pv( 137 | Decimal("0.07"), Decimal("20"), Decimal("12000"), Decimal("0"), "begin" 138 | ), 139 | ) 140 | # end 141 | assert_equal( 142 | npf.pv(Decimal("0.07"), Decimal("20"), Decimal("12000"), Decimal("0")), 143 | npf.pv( 144 | Decimal("0.07"), Decimal("20"), Decimal("12000"), Decimal("0"), "end" 145 | ), 146 | ) 147 | assert_equal( 148 | npf.pv( 149 | Decimal("0.07"), 150 | Decimal("20"), 151 | Decimal("12000"), 152 | Decimal("0"), 153 | Decimal("0"), 154 | ), 155 | npf.pv( 156 | Decimal("0.07"), Decimal("20"), Decimal("12000"), Decimal("0"), "end" 157 | ), 158 | ) 159 | 160 | 161 | class TestPV: 162 | def test_pv(self): 163 | assert_allclose(npf.pv(0.07, 20, 12000, 0), -127128.17, rtol=1e-2) 164 | 165 | def test_pv_decimal(self): 166 | assert_equal( 167 | npf.pv(Decimal("0.07"), Decimal("20"), Decimal("12000"), Decimal("0")), 168 | Decimal("-127128.1709461939327295222005"), 169 | ) 170 | 171 | 172 | class TestRate: 173 | def test_rate(self): 174 | assert_allclose(npf.rate(10, 0, -3500, 10000), 0.1107, rtol=1e-4) 175 | 176 | @pytest.mark.parametrize("number_type", [Decimal, float]) 177 | @pytest.mark.parametrize("when", [0, 1, "end", "begin"]) 178 | def test_rate_with_infeasible_solution(self, number_type, when): 179 | """ 180 | Test when no feasible rate can be found. 181 | 182 | Rate will return NaN, if the Newton Raphson method cannot find a 183 | feasible rate within the required tolerance or number of iterations. 184 | This can occur if both `pmt` and `pv` have the same sign, as it is 185 | impossible to repay a loan by making further withdrawls. 186 | """ 187 | result = npf.rate( 188 | number_type(12.0), 189 | number_type(400.0), 190 | number_type(10000.0), 191 | number_type(5000.0), 192 | when=when, 193 | ) 194 | is_nan = Decimal.is_nan if number_type == Decimal else numpy.isnan 195 | assert is_nan(result) 196 | 197 | def test_rate_decimal(self): 198 | rate = npf.rate(Decimal("10"), Decimal("0"), Decimal("-3500"), Decimal("10000")) 199 | assert_equal(Decimal("0.1106908537142689284704528100"), rate) 200 | 201 | def test_gh48(self): 202 | """ 203 | Test the correct result is returned with only infeasible solutions 204 | converted to nan. 205 | """ 206 | des = [-0.39920185, -0.02305873, -0.41818459, 0.26513414, numpy.nan] 207 | nper = 2 208 | pmt = 0 209 | pv = [-593.06, -4725.38, -662.05, -428.78, -13.65] 210 | fv = [214.07, 4509.97, 224.11, 686.29, -329.67] 211 | actual = npf.rate(nper, pmt, pv, fv) 212 | assert_allclose(actual, des) 213 | 214 | def test_rate_maximum_iterations_exception_scalar(self): 215 | # Test that if the maximum number of iterations is reached, 216 | # then npf.rate returns IterationsExceededException 217 | # when raise_exceptions is set to True. 218 | assert_raises( 219 | npf.IterationsExceededError, 220 | npf.rate, 221 | Decimal(12.0), 222 | Decimal(400.0), 223 | Decimal(10000.0), 224 | Decimal(5000.0), 225 | raise_exceptions=True, 226 | ) 227 | 228 | def test_rate_maximum_iterations_exception_array(self): 229 | # Test that if the maximum number of iterations is reached in at least 230 | # one rate, then npf.rate returns IterationsExceededException 231 | # when raise_exceptions is set to True. 232 | nper = 2 233 | pmt = 0 234 | pv = [-593.06, -4725.38, -662.05, -428.78, -13.65] 235 | fv = [214.07, 4509.97, 224.11, 686.29, -329.67] 236 | assert_raises( 237 | npf.IterationsExceededError, 238 | npf.rate, 239 | nper, 240 | pmt, 241 | pv, 242 | fv, 243 | raise_exceptions=True, 244 | ) 245 | 246 | 247 | class TestNpv: 248 | def test_npv(self): 249 | assert_allclose( 250 | npf.npv(0.05, [-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0]), 251 | 122.89, 252 | rtol=1e-2, 253 | ) 254 | 255 | @given(rates=short_nicely_behaved_doubles, values=cashflow_array_strategy) 256 | def test_fuzz(self, rates, values): 257 | npf.npv(rates, values) 258 | 259 | @pytest.mark.parametrize("rates", ([[1, 2, 3]], numpy.empty(shape=(1, 1, 1)))) 260 | def test_invalid_rates_shape(self, rates): 261 | cashflows = [1, 2, 3] 262 | with pytest.raises(ValueError): 263 | npf.npv(rates, cashflows) 264 | 265 | @pytest.mark.parametrize("cf", ([[[1, 2, 3]]], numpy.empty(shape=(1, 1, 1)))) 266 | def test_invalid_cashflows_shape(self, cf): 267 | rates = [1, 2, 3] 268 | with pytest.raises(ValueError): 269 | npf.npv(rates, cf) 270 | 271 | @pytest.mark.parametrize("rate", (-1, -1.0)) 272 | def test_rate_of_negative_one_returns_nan(self, rate): 273 | cashflow = numpy.arange(5) 274 | assert numpy.isnan(npf.npv(rate, cashflow)) 275 | 276 | 277 | class TestPmt: 278 | def test_pmt_simple(self): 279 | res = npf.pmt(0.08 / 12, 5 * 12, 15000) 280 | tgt = -304.145914 281 | assert_allclose(res, tgt) 282 | 283 | def test_pmt_zero_rate(self): 284 | # Test the edge case where rate == 0.0 285 | res = npf.pmt(0.0, 5 * 12, 15000) 286 | tgt = -250.0 287 | assert_allclose(res, tgt) 288 | 289 | def test_pmt_broadcast(self): 290 | # Test the case where we use broadcast and 291 | # the arguments passed in are arrays. 292 | res = npf.pmt([[0.0, 0.8], [0.3, 0.8]], [12, 3], [2000, 20000]) 293 | tgt = numpy.array([[-166.66667, -19311.258], [-626.90814, -19311.258]]) 294 | assert_allclose(res, tgt) 295 | 296 | def test_pmt_decimal_simple(self): 297 | res = npf.pmt(Decimal("0.08") / Decimal("12"), 5 * 12, 15000) 298 | tgt = Decimal("-304.1459143262052370338701494") 299 | assert_equal(res, tgt) 300 | 301 | def test_pmt_decimal_zero_rate(self): 302 | # Test the edge case where rate == 0.0 303 | res = npf.pmt(Decimal("0"), Decimal("60"), Decimal("15000")) 304 | tgt = -250 305 | assert_equal(res, tgt) 306 | 307 | def test_pmt_decimal_broadcast(self): 308 | # Test the case where we use broadcast and 309 | # the arguments passed in are arrays. 310 | res = npf.pmt( 311 | [[Decimal("0"), Decimal("0.8")], [Decimal("0.3"), Decimal("0.8")]], 312 | [Decimal("12"), Decimal("3")], 313 | [Decimal("2000"), Decimal("20000")], 314 | ) 315 | tgt = numpy.array( 316 | [ 317 | [ 318 | Decimal("-166.6666666666666666666666667"), 319 | Decimal("-19311.25827814569536423841060"), 320 | ], 321 | [ 322 | Decimal("-626.9081401700757748402586600"), 323 | Decimal("-19311.25827814569536423841060"), 324 | ], 325 | ] 326 | ) 327 | 328 | # Cannot use the `assert_allclose` because it uses isfinite under 329 | # the covers which does not support the Decimal type 330 | # See issue: https://github.com/numpy/numpy/issues/9954 331 | assert_equal(res[0][0], tgt[0][0]) 332 | assert_equal(res[0][1], tgt[0][1]) 333 | assert_equal(res[1][0], tgt[1][0]) 334 | assert_equal(res[1][1], tgt[1][1]) 335 | 336 | 337 | class TestMirr: 338 | @pytest.mark.parametrize( 339 | "values,finance_rate,reinvest_rate,expected", 340 | [ 341 | ( 342 | [-4500, -800, 800, 800, 600, 600, 800, 800, 700, 3000], 343 | 0.08, 344 | 0.055, 345 | 0.0666, 346 | ), 347 | ([-120000, 39000, 30000, 21000, 37000, 46000], 0.10, 0.12, 0.126094), 348 | ([100, 200, -50, 300, -200], 0.05, 0.06, 0.3428), 349 | ([39000, 30000, 21000, 37000, 46000], 0.10, 0.12, None), 350 | ], 351 | ) 352 | def test_mirr(self, values, finance_rate, reinvest_rate, expected): 353 | result = npf.mirr(values, finance_rate, reinvest_rate) 354 | 355 | if expected: 356 | decimal_part_len = len(str(expected).split(".")[1]) 357 | difference = 10**-decimal_part_len 358 | assert_allclose(result, expected, atol=difference) 359 | else: 360 | assert_(numpy.isnan(result)) 361 | 362 | def test_mirr_broadcast(self): 363 | values = [ 364 | [-4500, -800, 800, 800, 600], 365 | [-120000, 39000, 30000, 21000, 37000], 366 | [100, 200, -50, 300, -200], 367 | ] 368 | finance_rate = [0.05, 0.08, 0.10] 369 | reinvestment_rate = [0.08, 0.10, 0.12] 370 | # Found using Google sheets 371 | expected = numpy.array([ 372 | [-0.1784449, -0.17328716, -0.1684366], 373 | [0.04627293, 0.05437856, 0.06252201], 374 | [0.35712458, 0.40628857, 0.44435295] 375 | ]) 376 | actual = npf.mirr(values, finance_rate, reinvestment_rate) 377 | assert_allclose(actual, expected) 378 | 379 | def test_mirr_no_real_solution_exception(self): 380 | # Test that if there is no solution because all the cashflows 381 | # have the same sign, then npf.mirr returns NoRealSolutionException 382 | # when raise_exceptions is set to True. 383 | val = [39000, 30000, 21000, 37000, 46000] 384 | 385 | with pytest.raises(npf.NoRealSolutionError): 386 | npf.mirr(val, 0.10, 0.12, raise_exceptions=True) 387 | 388 | @given( 389 | values=cashflow_array_like_strategy, 390 | finance_rate=short_nicely_behaved_doubles, 391 | reinvestment_rate=short_nicely_behaved_doubles, 392 | ) 393 | def test_fuzz(self, values, finance_rate, reinvestment_rate): 394 | assume(finance_rate.size == reinvestment_rate.size) 395 | 396 | # NumPy warns us of arithmetic overflow/underflow 397 | # this only occurs when hypothesis generates extremely large values 398 | # that are unlikely to ever occur in the real world. 399 | with warnings.catch_warnings(): 400 | warnings.simplefilter("ignore") 401 | npf.mirr(values, finance_rate, reinvestment_rate) 402 | 403 | @given( 404 | values=cashflow_array_like_strategy, 405 | finance_rate=short_nicely_behaved_doubles, 406 | reinvestment_rate=short_nicely_behaved_doubles, 407 | ) 408 | def test_mismatching_rates_raise(self, values, finance_rate, reinvestment_rate): 409 | assume(finance_rate.size != reinvestment_rate.size) 410 | with pytest.raises(ValueError): 411 | npf.mirr(values, finance_rate, reinvestment_rate, raise_exceptions=True) 412 | 413 | 414 | class TestNper: 415 | def test_basic_values(self): 416 | assert_allclose( 417 | npf.nper([0, 0.075], -2000, 0, 100000), 418 | [50, 21.544944], # Computed using Google Sheet's NPER 419 | rtol=1e-5, 420 | ) 421 | 422 | def test_gh_18(self): 423 | with numpy.errstate(divide="raise"): 424 | assert_allclose( 425 | npf.nper(0.1, 0, -500, 1500), 426 | 11.52670461, # Computed using Google Sheet's NPER 427 | ) 428 | 429 | def test_infinite_payments(self): 430 | with numpy.errstate(divide="raise"): 431 | result = npf.nper(0, -0.0, 1000) 432 | assert_(result == numpy.inf) 433 | 434 | def test_no_interest(self): 435 | assert_(npf.nper(0, -100, 1000) == 10) 436 | 437 | def test_broadcast(self): 438 | assert_allclose( 439 | npf.nper(0.075, -2000, 0, 100000.0, [0, 1]), [21.5449442, 20.76156441], 4 440 | ) 441 | 442 | @given( 443 | rates=short_nicely_behaved_doubles, 444 | payments=short_nicely_behaved_doubles, 445 | present_values=short_nicely_behaved_doubles, 446 | future_values=short_nicely_behaved_doubles, 447 | whens=when_strategy, 448 | ) 449 | def test_fuzz(self, rates, payments, present_values, future_values, whens): 450 | npf.nper(rates, payments, present_values, future_values, whens) 451 | 452 | 453 | class TestPpmt: 454 | def test_float(self): 455 | assert_allclose(npf.ppmt(0.1 / 12, 1, 60, 55000), -710.25, rtol=1e-4) 456 | 457 | def test_decimal(self): 458 | result = npf.ppmt( 459 | Decimal("0.1") / Decimal("12"), 460 | Decimal("1"), 461 | Decimal("60"), 462 | Decimal("55000"), 463 | ) 464 | assert_equal( 465 | result, 466 | Decimal("-710.2541257864217612489830917"), 467 | ) 468 | 469 | @pytest.mark.parametrize("when", [1, "begin"]) 470 | def test_when_is_begin(self, when): 471 | assert_allclose( 472 | npf.ppmt(0.1 / 12, 1, 60, 55000, 0, when), 473 | -1158.929712, # Computed using Google Sheet's PPMT 474 | rtol=1e-9, 475 | ) 476 | 477 | @pytest.mark.parametrize("when", [None, 0, "end"]) 478 | def test_when_is_end(self, when): 479 | args = (0.1 / 12, 1, 60, 55000, 0) 480 | result = npf.ppmt(*args) if when is None else npf.ppmt(*args, when) 481 | assert_allclose( 482 | result, 483 | -710.254126, # Computed using Google Sheet's PPMT 484 | rtol=1e-9, 485 | ) 486 | 487 | @pytest.mark.parametrize("when", [Decimal("1"), "begin"]) 488 | def test_when_is_begin_decimal(self, when): 489 | result = npf.ppmt( 490 | Decimal("0.08") / Decimal("12"), 491 | Decimal("1"), 492 | Decimal("60"), 493 | Decimal("15000."), 494 | Decimal("0"), 495 | when, 496 | ) 497 | assert_decimal_close( 498 | result, 499 | Decimal("-302.131703"), # Computed using Google Sheet's PPMT 500 | tol=1e-5, 501 | ) 502 | 503 | @pytest.mark.parametrize("when", [None, Decimal("0"), "end"]) 504 | def test_when_is_end_decimal(self, when): 505 | args = ( 506 | Decimal("0.08") / Decimal("12"), 507 | Decimal("1"), 508 | Decimal("60"), 509 | Decimal("15000."), 510 | Decimal("0"), 511 | ) 512 | result = npf.ppmt(*args) if when is None else npf.ppmt(*args, when) 513 | assert_decimal_close( 514 | result, 515 | Decimal("-204.145914"), # Computed using Google Sheet's PPMT 516 | tol=1e-5, 517 | ) 518 | 519 | @pytest.mark.parametrize( 520 | "args", 521 | [ 522 | (0.1 / 12, 0, 60, 15000), 523 | (Decimal("0.012"), Decimal("0"), Decimal("60"), Decimal("15000")), 524 | ], 525 | ) 526 | def test_invalid_per(self, args): 527 | # Note that math.isnan() handles Decimal NaN correctly. 528 | assert math.isnan(npf.ppmt(*args)) 529 | 530 | @pytest.mark.parametrize( 531 | "when, desired", 532 | [ 533 | ( 534 | None, 535 | [-75.62318601, -76.25337923, -76.88882405, -77.52956425], 536 | ), 537 | ( 538 | [0, 1, "end", "begin"], 539 | [-75.62318601, -75.62318601, -76.88882405, -76.88882405], 540 | ), 541 | ], 542 | ) 543 | def test_broadcast(self, when, desired): 544 | args = (0.1 / 12, numpy.arange(1, 5), 24, 2000, 0) 545 | result = npf.ppmt(*args) if when is None else npf.ppmt(*args, when) 546 | assert_allclose(result, desired, rtol=1e-5) 547 | 548 | @pytest.mark.parametrize( 549 | "when, desired", 550 | [ 551 | ( 552 | None, 553 | [ 554 | Decimal("-75.62318601"), 555 | Decimal("-76.25337923"), 556 | Decimal("-76.88882405"), 557 | Decimal("-77.52956425"), 558 | ], 559 | ), 560 | ( 561 | [Decimal("0"), Decimal("1"), "end", "begin"], 562 | [ 563 | Decimal("-75.62318601"), 564 | Decimal("-75.62318601"), 565 | Decimal("-76.88882405"), 566 | Decimal("-76.88882405"), 567 | ], 568 | ), 569 | ], 570 | ) 571 | def test_broadcast_decimal(self, when, desired): 572 | args = ( 573 | Decimal("0.1") / Decimal("12"), 574 | numpy.arange(1, 5), 575 | Decimal("24"), 576 | Decimal("2000"), 577 | Decimal("0"), 578 | ) 579 | result = npf.ppmt(*args) if when is None else npf.ppmt(*args, when) 580 | assert_decimal_close(result, desired, tol=1e-8) 581 | 582 | 583 | class TestIpmt: 584 | def test_float(self): 585 | assert_allclose( 586 | npf.ipmt(0.1 / 12, 1, 24, 2000), 587 | -16.666667, # Computed using Google Sheet's IPMT 588 | rtol=1e-6, 589 | ) 590 | 591 | def test_decimal(self): 592 | result = npf.ipmt(Decimal("0.1") / Decimal("12"), 1, 24, 2000) 593 | assert result == Decimal("-16.66666666666666666666666667") 594 | 595 | @pytest.mark.parametrize("when", [1, "begin"]) 596 | def test_when_is_begin(self, when): 597 | assert npf.ipmt(0.1 / 12, 1, 24, 2000, 0, when) == 0 598 | 599 | @pytest.mark.parametrize("when", [None, 0, "end"]) 600 | def test_when_is_end(self, when): 601 | if when is None: 602 | result = npf.ipmt(0.1 / 12, 1, 24, 2000) 603 | else: 604 | result = npf.ipmt(0.1 / 12, 1, 24, 2000, 0, when) 605 | assert_allclose(result, -16.666667, rtol=1e-6) 606 | 607 | @pytest.mark.parametrize("when", [Decimal("1"), "begin"]) 608 | def test_when_is_begin_decimal(self, when): 609 | result = npf.ipmt( 610 | Decimal("0.1") / Decimal("12"), 611 | Decimal("1"), 612 | Decimal("24"), 613 | Decimal("2000"), 614 | Decimal("0"), 615 | when, 616 | ) 617 | assert result == 0 618 | 619 | @pytest.mark.parametrize("when", [None, Decimal("0"), "end"]) 620 | def test_when_is_end_decimal(self, when): 621 | # Computed using Google Sheet's IPMT 622 | desired = Decimal("-16.666667") 623 | args = ( 624 | Decimal("0.1") / Decimal("12"), 625 | Decimal("1"), 626 | Decimal("24"), 627 | Decimal("2000"), 628 | Decimal("0"), 629 | ) 630 | result = npf.ipmt(*args) if when is None else npf.ipmt(*args, when) 631 | assert_decimal_close(result, desired, tol=1e-5) 632 | 633 | @pytest.mark.parametrize( 634 | "per, desired", 635 | [ 636 | (0, numpy.nan), 637 | (1, 0), 638 | (2, -594.107158), 639 | (3, -592.971592), 640 | ], 641 | ) 642 | def test_gh_17(self, per, desired): 643 | # All desired results computed using Google Sheet's IPMT 644 | rate = 0.001988079518355057 645 | result = npf.ipmt(rate, per, 360, 300000, when="begin") 646 | if numpy.isnan(desired): 647 | assert numpy.isnan(result) 648 | else: 649 | assert_allclose(result, desired, rtol=1e-6) 650 | 651 | def test_broadcasting(self): 652 | desired = [numpy.nan, -16.66666667, -16.03647345, -15.40102862, -14.76028842] 653 | assert_allclose( 654 | npf.ipmt(0.1 / 12, numpy.arange(5), 24, 2000), 655 | desired, 656 | rtol=1e-6, 657 | ) 658 | 659 | def test_decimal_broadcasting(self): 660 | desired = [ 661 | Decimal("-16.66666667"), 662 | Decimal("-16.03647345"), 663 | Decimal("-15.40102862"), 664 | Decimal("-14.76028842"), 665 | ] 666 | result = npf.ipmt( 667 | Decimal("0.1") / Decimal("12"), 668 | list(range(1, 5)), 669 | Decimal("24"), 670 | Decimal("2000"), 671 | ) 672 | assert_decimal_close(result, desired, tol=1e-4) 673 | 674 | def test_0d_inputs(self): 675 | args = (0.1 / 12, 1, 24, 2000) 676 | # Scalar inputs should return a scalar. 677 | assert numpy.isscalar(npf.ipmt(*args)) 678 | args = (numpy.array(args[0]),) + args[1:] 679 | # 0d array inputs should return a scalar. 680 | assert numpy.isscalar(npf.ipmt(*args)) 681 | 682 | 683 | class TestFv: 684 | def test_float(self): 685 | assert_allclose( 686 | npf.fv(0.075, 20, -2000, 0, 0), 687 | 86609.362673042924, 688 | rtol=1e-10, 689 | ) 690 | 691 | def test_decimal(self): 692 | assert_decimal_close( 693 | npf.fv(Decimal("0.075"), Decimal("20"), Decimal("-2000"), 0, 0), 694 | Decimal("86609.36267304300040536731624"), 695 | tol=1e-10, 696 | ) 697 | 698 | @pytest.mark.parametrize("when", [1, "begin"]) 699 | def test_when_is_begin_float(self, when): 700 | assert_allclose( 701 | npf.fv(0.075, 20, -2000, 0, when), 702 | 93105.064874, # Computed using Google Sheet's FV 703 | rtol=1e-10, 704 | ) 705 | 706 | @pytest.mark.parametrize("when", [Decimal("1"), "begin"]) 707 | def test_when_is_begin_decimal(self, when): 708 | result = npf.fv( 709 | Decimal("0.075"), 710 | Decimal("20"), 711 | Decimal("-2000"), 712 | Decimal("0"), 713 | when, 714 | ) 715 | assert_decimal_close(result, Decimal("93105.064874"), tol=5) 716 | 717 | @pytest.mark.parametrize("when", [None, 0, "end"]) 718 | def test_when_is_end_float(self, when): 719 | args = (0.075, 20, -2000, 0) 720 | result = npf.fv(*args) if when is None else npf.fv(*args, when) 721 | assert_allclose( 722 | result, 723 | 86609.362673, # Computed using Google Sheet's FV 724 | rtol=1e-10, 725 | ) 726 | 727 | @pytest.mark.parametrize("when", [None, Decimal("0"), "end"]) 728 | def test_when_is_end_decimal(self, when): 729 | args = ( 730 | Decimal("0.075"), 731 | Decimal("20"), 732 | Decimal("-2000"), 733 | Decimal("0"), 734 | ) 735 | result = npf.fv(*args) if when is None else npf.fv(*args, when) 736 | assert_decimal_close(result, Decimal("86609.362673"), tol=5) 737 | 738 | def test_broadcast(self): 739 | result = npf.fv([[0.1], [0.2]], 5, 100, 0, [0, 1]) 740 | # All values computed using Google Sheet's FV 741 | desired = [[-610.510000, -671.561000], [-744.160000, -892.992000]] 742 | assert_allclose(result, desired, rtol=1e-10) 743 | 744 | def test_some_rates_zero(self): 745 | # Check that the logical indexing is working correctly. 746 | assert_allclose( 747 | npf.fv([0, 0.1], 5, 100, 0), 748 | [-500, -610.51], # Computed using Google Sheet's FV 749 | rtol=1e-10, 750 | ) 751 | 752 | 753 | class TestIrr: 754 | def test_npv_irr_congruence(self): 755 | # IRR is defined as the rate required for the present value of 756 | # a series of cashflows to be zero, so we should have 757 | # 758 | # NPV(IRR(x), x) = 0. 759 | cashflows = numpy.array([-40000, 5000, 8000, 12000, 30000]) 760 | assert_allclose( 761 | npf.npv(npf.irr(cashflows), cashflows), 762 | 0, 763 | atol=1e-9, 764 | rtol=0, 765 | ) 766 | 767 | @pytest.mark.parametrize( 768 | "v, desired", 769 | [ 770 | ([-150000, 15000, 25000, 35000, 45000, 60000], 0.0524), 771 | ([-100, 0, 0, 74], -0.0955), 772 | ([-100, 39, 59, 55, 20], 0.28095), 773 | ([-100, 100, 0, -7], -0.0833), 774 | ([-100, 100, 0, 7], 0.06206), 775 | ([-5, 10.5, 1, -8, 1], 0.0886), 776 | ], 777 | ) 778 | def test_basic_values(self, v, desired): 779 | assert_allclose(npf.irr(v), desired, rtol=1e-2) 780 | 781 | def test_trailing_zeros(self): 782 | assert_allclose( 783 | npf.irr([-5, 10.5, 1, -8, 1, 0, 0, 0]), 784 | 0.0886, 785 | rtol=1e-2, 786 | ) 787 | 788 | @pytest.mark.parametrize( 789 | "v", 790 | [ 791 | (1, 2, 3), 792 | (-1, -2, -3), 793 | ], 794 | ) 795 | def test_numpy_gh_6744(self, v): 796 | # Test that if there is no solution then npf.irr returns nan. 797 | assert numpy.isnan(npf.irr(v)) 798 | 799 | def test_gh_15(self): 800 | v = [ 801 | -3000.0, 802 | 2.3926932267015667e-07, 803 | 4.1672087103345505e-16, 804 | 5.3965110036378706e-25, 805 | 5.1962551071806174e-34, 806 | 3.7202955645436402e-43, 807 | 1.9804961711632469e-52, 808 | 7.8393517651814181e-62, 809 | 2.3072565113911438e-71, 810 | 5.0491839233308912e-81, 811 | 8.2159177668499263e-91, 812 | 9.9403244366963527e-101, 813 | 8.942410813633967e-111, 814 | 5.9816122646481191e-121, 815 | 2.9750309031844241e-131, 816 | 1.1002067043497954e-141, 817 | 3.0252876563518021e-152, 818 | 6.1854121948207909e-163, 819 | 9.4032980015353301e-174, 820 | 1.0629218520017728e-184, 821 | 8.9337141847171845e-196, 822 | 5.5830607698467935e-207, 823 | 2.5943122036622652e-218, 824 | 8.9635842466507006e-230, 825 | 2.3027710094332358e-241, 826 | 4.3987510596745562e-253, 827 | 6.2476630372575209e-265, 828 | 6.598046841695288e-277, 829 | 5.1811095266842017e-289, 830 | 3.0250999925830644e-301, 831 | 1.3133070599585015e-313, 832 | ] 833 | result = npf.irr(v) 834 | assert numpy.isfinite(result) 835 | # Very rough approximation taken from the issue. 836 | desired = -0.9999999990596069 837 | assert_allclose(result, desired, rtol=1e-9) 838 | 839 | def test_gh_39(self): 840 | cashflows = numpy.array( 841 | [ 842 | -217500.0, 843 | -217500.0, 844 | 108466.80462450592, 845 | 101129.96439328062, 846 | 93793.12416205535, 847 | 86456.28393083003, 848 | 79119.44369960476, 849 | 71782.60346837944, 850 | 64445.76323715414, 851 | 57108.92300592884, 852 | 49772.08277470355, 853 | 42435.24254347826, 854 | 35098.40231225296, 855 | 27761.56208102766, 856 | 20424.721849802358, 857 | 13087.88161857707, 858 | 5751.041387351768, 859 | -1585.7988438735192, 860 | -8922.639075098821, 861 | -16259.479306324123, 862 | -23596.31953754941, 863 | -30933.159768774713, 864 | -38270.0, 865 | -45606.8402312253, 866 | -52943.680462450604, 867 | -60280.520693675906, 868 | -67617.36092490121, 869 | ] 870 | ) 871 | assert_allclose(npf.irr(cashflows), 0.12) 872 | 873 | def test_gh_44(self): 874 | # "true" value as calculated by Google sheets 875 | cf = [-1678.87, 771.96, 1814.05, 3520.30, 3552.95, 3584.99, 4789.91, -1] 876 | assert_allclose(npf.irr(cf), 1.00426, rtol=1e-4) 877 | 878 | def test_irr_no_real_solution_exception(self): 879 | # Test that if there is no solution because all the cashflows 880 | # have the same sign, then npf.irr returns NoRealSolutionException 881 | # when raise_exceptions is set to True. 882 | cashflows = numpy.array([40000, 5000, 8000, 12000, 30000]) 883 | 884 | with pytest.raises(npf.NoRealSolutionError): 885 | npf.irr(cashflows, raise_exceptions=True) 886 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "mesonpy" 3 | requires = [ 4 | "meson-python>=0.15.0", 5 | "Cython>=3.0.9", 6 | "numpy>=1.23.5", 7 | ] 8 | 9 | [project] 10 | name = "numpy-financial" 11 | version = "2.0.0" 12 | requires-python = ">=3.10" 13 | description = "Simple financial functions" 14 | license = "BSD-3-Clause" 15 | authors = [{name = "Travis E. Oliphant et al."}] 16 | maintainers = [{ name = "Numpy Financial Developers", email = "numpy-discussion@python.org" }] 17 | readme = "README.md" 18 | homepage = "https://numpy.org/numpy-financial/latest/" 19 | repository = "https://github.com/numpy/numpy-financial" 20 | documentation = "https://numpy.org/numpy-financial/latest/#functions" 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Intended Audience :: Developers", 24 | "Intended Audience :: Financial and Insurance Industry", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: 3 :: Only", 31 | "Topic :: Software Development", 32 | "Topic :: Office/Business :: Financial :: Accounting", 33 | "Topic :: Office/Business :: Financial :: Investment", 34 | "Topic :: Office/Business :: Financial :: Spreadsheet", 35 | "Operating System :: Microsoft :: Windows", 36 | "Operating System :: POSIX", 37 | "Operating System :: Unix", 38 | "Operating System :: MacOS", 39 | "Typing :: Typed", 40 | ] 41 | 42 | [project.optional-dependencies] 43 | test = [ 44 | "pytest", 45 | "pytest-xdist", 46 | "hypothesis", 47 | ] 48 | doc = [ 49 | "sphinx>=7.0", 50 | "numpydoc>=1.5", 51 | "pydata-sphinx-theme>=0.15", 52 | "myst-parser>=2.0.0", 53 | ] 54 | dev = [ 55 | "ruff>=0.11.5", 56 | "asv>=0.6.0", 57 | ] 58 | 59 | 60 | [tool.mypy] 61 | exclude = [ 62 | "^benchmarks/.*", 63 | "^doc/.*", 64 | "^docweb/.*", 65 | "^numpy_financial/tests/.*", 66 | ] 67 | enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] 68 | local_partial_types = true 69 | warn_unreachable = false 70 | warn_unused_ignores = true 71 | strict_bytes = true 72 | 73 | 74 | [tool.pyright] 75 | pythonPlatform = "All" 76 | include = ["numpy_financial"] 77 | exclude = [ 78 | "benchmarks", 79 | "doc", 80 | "docweb", 81 | "numpy_financial/tests", 82 | ] 83 | stubPath = "." 84 | typeCheckingMode = "standard" 85 | 86 | 87 | [tool.spin] 88 | package = 'numpy_financial' 89 | 90 | [tool.spin.commands] 91 | "Build" = [ 92 | "spin.cmds.meson.build", 93 | "spin.cmds.meson.test", 94 | "spin.cmds.build.sdist", 95 | "spin.cmds.pip.install", 96 | ] 97 | "Documentation" = [ 98 | "spin.cmds.meson.docs" 99 | ] 100 | "Environments" = [ 101 | "spin.cmds.meson.shell", 102 | "spin.cmds.meson.ipython", 103 | "spin.cmds.meson.python", 104 | "spin.cmds.meson.run" 105 | ] 106 | --------------------------------------------------------------------------------