Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
6 changes: 0 additions & 6 deletions benchmarks/benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,11 @@ def setup(self, n_cashflows, cashflow_lengths, rates_lengths):
self.cashflows_decimal = _to_decimal_array_2d(self.cashflows)
self.rates_decimal = _to_decimal_array_1d(self.rates)

def time_broadcast(self, n_cashflows, cashflow_lengths, rates_lengths):
npf.npv(self.rates, self.cashflows)

def time_for_loop(self, n_cashflows, cashflow_lengths, rates_lengths):
for rate in self.rates:
for cashflow in self.cashflows:
npf.npv(rate, cashflow)

def time_broadcast_decimal(self, n_cashflows, cashflow_lengths, rates_lengths):
npf.npv(self.rates_decimal, self.cashflows_decimal)

def time_for_loop_decimal(self, n_cashflows, cashflow_lengths, rates_lengths):
for rate in self.rates_decimal:
for cashflow in self.cashflows_decimal:
Expand Down
110 changes: 8 additions & 102 deletions numpy_financial/_financial.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

from decimal import Decimal

import numba as nb
import numpy as np

__all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate',
Expand Down Expand Up @@ -47,36 +46,6 @@ def _convert_when(when):
return [_when_to_num[x] for x in when]


def _return_ufunc_like(array):
try:
# If size of array is one, return scalar
return array.item()
except ValueError:
# Otherwise, return entire array
return array


def _is_object_array(array):
return array.dtype == np.dtype("O")


def _use_decimal_dtype(*arrays):
return any(_is_object_array(array) for array in arrays)


def _to_decimal_array_1d(array):
return np.array([Decimal(x) for x in array.tolist()])


def _to_decimal_array_2d(array):
decimals = [Decimal(x) for row in array.tolist() for x in row]
return np.array(decimals).reshape(array.shape)


def _get_output_array_shape(*arrays):
return tuple(array.shape[0] for array in arrays)


def fv(rate, nper, pmt, pv, when='end'):
"""Compute the future value.

Expand Down Expand Up @@ -856,27 +825,6 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
return np.nan


@nb.njit(parallel=True)
def _npv_native(rates, values, out):
for i in nb.prange(rates.shape[0]):
for j in nb.prange(values.shape[0]):
acc = 0.0
for t in range(values.shape[1]):
acc += values[j, t] / ((1.0 + rates[i]) ** t)
out[i, j] = acc


# We require ``forceobj=True`` here to support decimal.Decimal types
@nb.jit(forceobj=True)
def _npv_decimal(rates, values, out):
for i in range(rates.shape[0]):
for j in range(values.shape[0]):
acc = Decimal("0.0")
for t in range(values.shape[1]):
acc += values[j, t] / ((Decimal("1.0") + rates[i]) ** t)
out[i, j] = acc


def npv(rate, values):
r"""Return the NPV (Net Present Value) of a cash flow series.

Expand Down Expand Up @@ -944,58 +892,16 @@ def npv(rate, values):
>>> np.round(npf.npv(rate, cashflows) + initial_cashflow, 5)
3065.22267

The NPV calculation may be applied to several ``rates`` and ``cashflows``
simulatneously. This produces an array of shape
``(len(rates), len(cashflows))``.

>>> rates = [0.00, 0.05, 0.10]
>>> cashflows = [[-4_000, 500, 800], [-5_000, 600, 900]]
>>> npf.npv(rates, cashflows).round(2)
array([[-2700. , -3500. ],
[-2798.19, -3612.24],
[-2884.3 , -3710.74]])

The NPV calculation also supports `decimal.Decimal` types, for example
if using Decimal ``rates``:

>>> rates = [Decimal("0.00"), Decimal("0.05"), Decimal("0.10")]
>>> cashflows = [[-4_000, 500, 800], [-5_000, 600, 900]]
>>> npf.npv(rates, cashflows)
array([[Decimal('-2700.0'), Decimal('-3500.0')],
[Decimal('-2798.185941043083900226757370'),
Decimal('-3612.244897959183673469387756')],
[Decimal('-2884.297520661157024793388430'),
Decimal('-3710.743801652892561983471074')]], dtype=object)

This also works for Decimal cashflows.

"""
rates = np.atleast_1d(rate)
values = np.atleast_2d(values)

if rates.ndim != 1:
msg = "invalid shape for rates. Rate must be either a scalar or 1d array"
raise ValueError(msg)

if values.ndim != 2:
msg = "invalid shape for values. Values must be either a 1d or 2d array"
raise ValueError(msg)

dtype = Decimal if _use_decimal_dtype(rates, values) else np.float64

if dtype == Decimal:
rates = _to_decimal_array_1d(rates)
values = _to_decimal_array_2d(values)

shape = _get_output_array_shape(rates, values)
out = np.empty(shape=shape, dtype=dtype)

if dtype == Decimal:
_npv_decimal(rates, values, out)
else:
_npv_native(rates, values, out)

return _return_ufunc_like(out)
timestep_array = np.arange(0, values.shape[1])
npv = (values / (1 + rate) ** timestep_array).sum(axis=1)
try:
# If size of array is one, return scalar
return npv.item()
except ValueError:
# Otherwise, return entire array
return npv


def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development",
"Topic :: Office/Business :: Financial :: Accounting",
Expand All @@ -37,10 +38,8 @@ classifiers = [
packages = [{include = "numpy_financial"}]

[tool.poetry.dependencies]
python = "^3.9,<3.12"
python = "^3.9"
numpy = "^1.23"
numba = "^0.58.1"


[tool.poetry.group.test.dependencies]
pytest = "^7.4"
Expand Down
44 changes: 0 additions & 44 deletions tests/test_financial.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,50 +250,6 @@ def test_npv_decimal(self):
Decimal("122.894854950942692161628715"),
)

def test_npv_broadcast(self):
cashflows = [
[-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0],
[-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0],
[-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0],
[-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0],
]
expected_npvs = [[122.8948549, 122.8948549, 122.8948549, 122.8948549]]
actual_npvs = npf.npv(0.05, cashflows)
assert_allclose(actual_npvs, expected_npvs)

@pytest.mark.parametrize("dtype", [Decimal, float])
def test_npv_broadcast_equals_for_loop(self, dtype):
cashflows_str = [
["-15000.0", "1500.0", "2500.0", "3500.0", "4500.0", "6000.0"],
["-25000.0", "1500.0", "2500.0", "3500.0", "4500.0", "6000.0"],
["-35000.0", "1500.0", "2500.0", "3500.0", "4500.0", "6000.0"],
["-45000.0", "1500.0", "2500.0", "3500.0", "4500.0", "6000.0"],
]
rates_str = ["-0.05", "0.00", "0.05", "0.10", "0.15"]

cashflows = numpy.array([[dtype(x) for x in cf] for cf in cashflows_str])
rates = numpy.array([dtype(x) for x in rates_str])

expected = numpy.empty((len(rates), len(cashflows)), dtype=dtype)
for i, r in enumerate(rates):
for j, cf in enumerate(cashflows):
expected[i, j] = npf.npv(r, cf)

actual = npf.npv(rates, cashflows)
assert_equal(actual, expected)

@pytest.mark.parametrize("rates", ([[1, 2, 3]], numpy.empty(shape=(1, 1, 1))))
def test_invalid_rates_shape(self, rates):
cashflows = [1, 2, 3]
with pytest.raises(ValueError):
npf.npv(rates, cashflows)

@pytest.mark.parametrize("cf", ([[[1, 2, 3]]], numpy.empty(shape=(1, 1, 1))))
def test_invalid_cashflows_shape(self, cf):
rates = [1, 2, 3]
with pytest.raises(ValueError):
npf.npv(rates, cf)


class TestPmt:
def test_pmt_simple(self):
Expand Down