Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAINT: Tidy up environment.yml #127

Merged
merged 11 commits into from
Jul 6, 2024
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 @@ -19,7 +19,7 @@ jobs:
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}
activate-environment: numpy-financial-dev
activate-environment: npf-dev
environment-file: environment.yml
auto-activate-base: false
- name: Conda metadata
Expand Down
7 changes: 3 additions & 4 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# To use:
# $ conda env create -f environment.yml # `mamba` works too for this command
# $ conda activate numpy-financial-dev
# $ conda activate npf-dev
#
name: numpy-financial-dev
name: npf-dev
channels:
- conda-forge
dependencies:
# Runtime dependencies
- python
- numpy
- numpy>=2.0.0
# Build
- cython>=3.0.9
- compilers
Expand All @@ -20,7 +20,6 @@ dependencies:
# Tests
- pytest
- pytest-xdist
- asv>=0.6.0
- hypothesis
# Docs
- myst-parser
Expand Down
77 changes: 51 additions & 26 deletions numpy_financial/_financial.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def pmt(rate, nper, pv, fv=0, when='end'):
years at an annual interest rate of 7.5%?

>>> npf.pmt(0.075/12, 12*15, 200000)
-1854.0247200054619
np.float64(-1854.0247200054619)

In order to pay-off (i.e., have a future-value of 0) the $200,000 obtained
today, a monthly payment of $1,854.02 would be required. Note that this
Expand Down Expand Up @@ -424,7 +424,7 @@ def ipmt(rate, per, nper, pv, fv=0, when='end'):

>>> interestpd = np.sum(ipmt)
>>> np.round(interestpd, 2)
-112.98
np.float64(-112.98)

"""
when = _convert_when(when)
Expand Down Expand Up @@ -562,7 +562,7 @@ def pv(rate, nper, pmt, fv=0, when='end'):
interest rate is 5% (annually) compounded monthly.

>>> npf.pv(0.05/12, 10*12, -100, 15692.93)
-100.00067131625819
np.float64(-100.00067131625819)

By convention, the negative sign represents cash flow out
(i.e., money not available today). Thus, to end up with
Expand Down Expand Up @@ -913,7 +913,7 @@ def npv(rate, values):

>>> rate, cashflows = 0.08, [-40_000, 5_000, 8_000, 12_000, 30_000]
>>> np.round(npf.npv(rate, cashflows), 5)
3065.22267
np.float64(3065.22267)

It may be preferable to split the projected cashflow into an initial
investment and expected future cashflows. In this case, the value of
Expand All @@ -923,7 +923,7 @@ def npv(rate, values):
>>> initial_cashflow = cashflows[0]
>>> cashflows[0] = 0
>>> np.round(npf.npv(rate, cashflows) + initial_cashflow, 5)
3065.22267
np.float64(3065.22267)

The NPV calculation may be applied to several ``rates`` and ``cashflows``
simulatneously. This produces an array of shape ``(len(rates), len(cashflows))``.
Expand Down Expand Up @@ -963,12 +963,12 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):

Parameters
----------
values : array_like
values : array_like, 1D or 2D
Cash flows, where the first value is considered a sunk cost at time zero.
It must contain at least one positive and one negative value.
finance_rate : scalar
finance_rate : scalar or 1D array
Interest rate paid on the cash flows.
reinvest_rate : scalar
reinvest_rate : scalar or D array
Interest rate received on the cash flows upon reinvestment.
raise_exceptions: bool, optional
Flag to raise an exception when the MIRR cannot be computed due to
Expand All @@ -977,7 +977,7 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):

Returns
-------
out : float
out : float or 2D array
Modified internal rate of return

Notes
Expand Down Expand Up @@ -1007,6 +1007,22 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
>>> npf.mirr([-100, 50, -60, 70], 0.10, 0.12)
-0.03909366594356467

It is also possible to supply multiple cashflows or pairs of
finance and reinvstment rates, note that in this case the number of elements
in each of the rates arrays must match.

>>> values = [
... [-4500, -800, 800, 800, 600],
... [-120000, 39000, 30000, 21000, 37000],
... [100, 200, -50, 300, -200],
... ]
>>> finance_rate = [0.05, 0.08, 0.10]
>>> reinvestment_rate = [0.08, 0.10, 0.12]
>>> npf.mirr(values, finance_rate, reinvestment_rate)
array([[-0.1784449 , -0.17328716, -0.1684366 ],
[ 0.04627293, 0.05437856, 0.06252201],
[ 0.35712458, 0.40628857, 0.44435295]])

Now, let's consider the scenario where all cash flows are negative.

>>> npf.mirr([-100, -50, -60, -70], 0.10, 0.12)
Expand All @@ -1025,22 +1041,31 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
numpy_financial._financial.NoRealSolutionError:
No real solution exists for MIRR since all cashflows are of the same sign.
"""
values = np.asarray(values)
n = values.size

# Without this explicit cast the 1/(n - 1) computation below
# becomes a float, which causes TypeError when using Decimal
# values.
if isinstance(finance_rate, Decimal):
n = Decimal(n)

pos = values > 0
neg = values < 0
if not (pos.any() and neg.any()):
values_inner = np.atleast_2d(values).astype(np.float64)
finance_rate_inner = np.atleast_1d(finance_rate).astype(np.float64)
reinvest_rate_inner = np.atleast_1d(reinvest_rate).astype(np.float64)
n = values_inner.shape[1]

if finance_rate_inner.size != reinvest_rate_inner.size:
if raise_exceptions:
raise NoRealSolutionError('No real solution exists for MIRR since'
' all cashflows are of the same sign.')
raise ValueError("finance_rate and reinvest_rate must have the same size")
return np.nan
numer = np.abs(npv(reinvest_rate, values * pos))
denom = np.abs(npv(finance_rate, values * neg))
return (numer / denom) ** (1 / (n - 1)) * (1 + reinvest_rate) - 1

out_shape = _get_output_array_shape(values_inner, finance_rate_inner)
out = np.empty(out_shape)

for i, v in enumerate(values_inner):
for j, (rr, fr) in enumerate(zip(reinvest_rate_inner, finance_rate_inner)):
pos = v > 0
neg = v < 0

if not (pos.any() and neg.any()):
if raise_exceptions:
raise NoRealSolutionError("No real solution exists for MIRR since"
" all cashflows are of the same sign.")
out[i, j] = np.nan
else:
numer = np.abs(npv(rr, v * pos))
denom = np.abs(npv(fr, v * neg))
out[i, j] = (numer / denom) ** (1 / (n - 1)) * (1 + rr) - 1
return _ufunc_like(out)
34 changes: 34 additions & 0 deletions numpy_financial/tests/strategies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import numpy as np
from hypothesis import strategies as st
from hypothesis.extra import numpy as npst

real_scalar_dtypes = st.one_of(
npst.floating_dtypes(),
npst.integer_dtypes(),
npst.unsigned_integer_dtypes()
)
nicely_behaved_doubles = npst.from_dtype(
np.dtype("f8"),
allow_nan=False,
allow_infinity=False,
allow_subnormal=False,
)
cashflow_array_strategy = npst.arrays(
dtype=npst.floating_dtypes(sizes=64),
shape=npst.array_shapes(min_dims=1, max_dims=2, min_side=0, max_side=25),
elements=nicely_behaved_doubles,
)
cashflow_list_strategy = cashflow_array_strategy.map(lambda x: x.tolist())
cashflow_array_like_strategy = st.one_of(
cashflow_array_strategy,
cashflow_list_strategy,
)
short_nicely_behaved_doubles = npst.arrays(
dtype=npst.floating_dtypes(sizes=64),
shape=npst.array_shapes(min_dims=0, max_dims=1, min_side=0, max_side=5),
elements=nicely_behaved_doubles,
)

when_strategy = st.sampled_from(
['end', 'begin', 'e', 'b', 0, 1, 'beginning', 'start', 'finish']
)
98 changes: 53 additions & 45 deletions numpy_financial/tests/test_financial.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import math
import warnings
from decimal import Decimal

import hypothesis.extra.numpy as npst
import hypothesis.strategies as st

# Don't use 'import numpy as np', to avoid accidentally testing
# the versions in numpy instead of numpy_financial.
import numpy
import pytest
from hypothesis import given, settings
from hypothesis import assume, given
from numpy.testing import (
assert_,
assert_allclose,
Expand All @@ -17,42 +15,11 @@
)

import numpy_financial as npf


def float_dtype():
return npst.floating_dtypes(sizes=[32, 64], endianness="<")


def int_dtype():
return npst.integer_dtypes(sizes=[32, 64], endianness="<")


def uint_dtype():
return npst.unsigned_integer_dtypes(sizes=[32, 64], endianness="<")


real_scalar_dtypes = st.one_of(float_dtype(), int_dtype(), uint_dtype())


cashflow_array_strategy = npst.arrays(
dtype=real_scalar_dtypes,
shape=npst.array_shapes(min_dims=1, max_dims=2, min_side=0, max_side=25),
)
cashflow_list_strategy = cashflow_array_strategy.map(lambda x: x.tolist())

cashflow_array_like_strategy = st.one_of(
from numpy_financial.tests.strategies import (
cashflow_array_like_strategy,
cashflow_array_strategy,
cashflow_list_strategy,
)

short_scalar_array_strategy = npst.arrays(
dtype=real_scalar_dtypes,
shape=npst.array_shapes(min_dims=0, max_dims=1, min_side=0, max_side=5),
)


when_strategy = st.sampled_from(
['end', 'begin', 'e', 'b', 0, 1, 'beginning', 'start', 'finish']
short_nicely_behaved_doubles,
when_strategy,
)


Expand Down Expand Up @@ -285,8 +252,7 @@ def test_npv(self):
rtol=1e-2,
)

@given(rates=short_scalar_array_strategy, values=cashflow_array_strategy)
@settings(deadline=None)
@given(rates=short_nicely_behaved_doubles, values=cashflow_array_strategy)
def test_fuzz(self, rates, values):
npf.npv(rates, values)

Expand Down Expand Up @@ -393,6 +359,23 @@ def test_mirr(self, values, finance_rate, reinvest_rate, expected):
else:
assert_(numpy.isnan(result))

def test_mirr_broadcast(self):
values = [
[-4500, -800, 800, 800, 600],
[-120000, 39000, 30000, 21000, 37000],
[100, 200, -50, 300, -200],
]
finance_rate = [0.05, 0.08, 0.10]
reinvestment_rate = [0.08, 0.10, 0.12]
# Found using Google sheets
expected = numpy.array([
[-0.1784449, -0.17328716, -0.1684366],
[0.04627293, 0.05437856, 0.06252201],
[0.35712458, 0.40628857, 0.44435295]
])
actual = npf.mirr(values, finance_rate, reinvestment_rate)
assert_allclose(actual, expected)

def test_mirr_no_real_solution_exception(self):
# Test that if there is no solution because all the cashflows
# have the same sign, then npf.mirr returns NoRealSolutionException
Expand All @@ -402,6 +385,31 @@ def test_mirr_no_real_solution_exception(self):
with pytest.raises(npf.NoRealSolutionError):
npf.mirr(val, 0.10, 0.12, raise_exceptions=True)

@given(
values=cashflow_array_like_strategy,
finance_rate=short_nicely_behaved_doubles,
reinvestment_rate=short_nicely_behaved_doubles,
)
def test_fuzz(self, values, finance_rate, reinvestment_rate):
assume(finance_rate.size == reinvestment_rate.size)

# NumPy warns us of arithmetic overflow/underflow
# this only occurs when hypothesis generates extremely large values
# that are unlikely to ever occur in the real world.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
npf.mirr(values, finance_rate, reinvestment_rate)

@given(
values=cashflow_array_like_strategy,
finance_rate=short_nicely_behaved_doubles,
reinvestment_rate=short_nicely_behaved_doubles,
)
def test_mismatching_rates_raise(self, values, finance_rate, reinvestment_rate):
assume(finance_rate.size != reinvestment_rate.size)
with pytest.raises(ValueError):
npf.mirr(values, finance_rate, reinvestment_rate, raise_exceptions=True)


class TestNper:
def test_basic_values(self):
Expand Down Expand Up @@ -432,10 +440,10 @@ def test_broadcast(self):
)

@given(
rates=short_scalar_array_strategy,
payments=short_scalar_array_strategy,
present_values=short_scalar_array_strategy,
future_values=short_scalar_array_strategy,
rates=short_nicely_behaved_doubles,
payments=short_nicely_behaved_doubles,
present_values=short_nicely_behaved_doubles,
future_values=short_nicely_behaved_doubles,
whens=when_strategy,
)
def test_fuzz(self, rates, payments, present_values, future_values, whens):
Expand Down
Loading