├── .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 |
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 |
12 |
--------------------------------------------------------------------------------
/docweb/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | numpy-financial
7 |
13 |
14 |
17 |
18 | numpy-financial
19 |
20 |
21 |
22 |
23 |
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 |
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 |
--------------------------------------------------------------------------------