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

Updated irr function [issue 98] #99

Merged
merged 10 commits into from
Mar 21, 2024
59 changes: 32 additions & 27 deletions numpy_financial/_financial.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ def rate(
return rn


def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
def irr(values, raise_exceptions=False):
Kai-Striega marked this conversation as resolved.
Show resolved Hide resolved
r"""Return the Internal Rate of Return (IRR).

This is the "average" periodically compounded rate of return
Expand All @@ -743,14 +743,6 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
are negative and net "withdrawals" are positive. Thus, for
example, at least the first element of `values`, which represents
the initial investment, will typically be negative.
guess : float, optional
Initial guess of the IRR for the iterative solver. If no guess is
given an heuristic is used to estimate the guess through the ratio of
positive to negative cash lows
tol : float, optional
Required tolerance to accept solution. Default is 1e-12.
maxiter : int, optional
Maximum iterations to perform in finding a solution. Default is 100.
raise_exceptions: bool, optional
Flag to raise an exception when the irr cannot be computed due to
either having all cashflows of the same sign (NoRealSolutionException) or
Expand Down Expand Up @@ -816,13 +808,6 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
'cashflows are of the same sign.')
return np.nan

# If no value is passed for `guess`, then make a heuristic estimate
if guess is None:
positive_cashflow = values > 0
inflow = values.sum(where=positive_cashflow)
outflow = -values.sum(where=~positive_cashflow)
guess = inflow / outflow - 1

# We aim to solve eirr such that NPV is exactly zero. This can be framed as
# simply finding the closest root of a polynomial to a given initial guess
# as follows:
Expand All @@ -840,20 +825,40 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
#
# which we solve using Newton-Raphson and then reverse out the solution
# as eirr = g - 1 (if we are close enough to a solution)
npv_ = np.polynomial.Polynomial(values[::-1])
d_npv = npv_.deriv()
g = 1 + guess

g = np.roots(values)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the roots docs:

This forms part of the old polynomial API. Since version 1.4, the new polynomial API defined in numpy.polynomial is preferred. A summary of the differences can be found in the transition guide.

However I'm not sure if it's worth to construct a whole Polynomial object just for this one calculation...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm currently re-writing some of the functions to use numba, it looks like np.roots is supported, but the new Polynomial API is not. So I'd prefer this version.

eirr = np.real(g[np.isreal(g)]) - 1

for _ in range(maxiter):
delta = npv_(g) / d_npv(g)
if abs(delta) < tol:
return g - 1
g -= delta
# realistic IRR
eirr = eirr[eirr>=-1]

if raise_exceptions:
raise IterationsExceededError('Maximum number of iterations exceeded.')
# if no real solution
if len(eirr) == 0:
if raise_exceptions:
raise NoRealSolutionError("No real solution is found for IRR.")
return np.nan

return np.nan
# if only one real solution
if len(eirr) == 1:
return eirr[0]

# below is for the situation when there are more than 2 real solutions.
# check sign of all IRR solutions
same_sign = np.all(eirr > 0) if eirr[0] > 0 else np.all(eirr < 0)

# if the signs of IRR solutions are not the same, first filter potential IRR
# by comparing the total positive and negative cash flows.
if not same_sign:
pos = sum(values[values>0])
neg = sum(values[values<0])
if pos >= neg:
eirr = eirr[eirr>=0]
else:
eirr = eirr[eirr<0]

# pick the smallest one in magnitude and return
abs_eirr = np.abs(eirr)
return eirr[np.argmin(abs_eirr)]


@nb.njit(parallel=True)
Expand Down
9 changes: 0 additions & 9 deletions tests/test_financial.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,12 +914,3 @@ def test_irr_no_real_solution_exception(self):

with pytest.raises(npf.NoRealSolutionError):
npf.irr(cashflows, raise_exceptions=True)

def test_irr_maximum_iterations_exception(self):
# Test that if the maximum number of iterations is reached,
# then npf.irr returns IterationsExceededException
# when raise_exceptions is set to True.
cashflows = numpy.array([-40000, 5000, 8000, 12000, 30000])

with pytest.raises(npf.IterationsExceededError):
npf.irr(cashflows, maxiter=1, raise_exceptions=True)