diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 033495d..3bb975f 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -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): r"""Return the Internal Rate of Return (IRR). This is the "average" periodically compounded rate of return @@ -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 @@ -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: @@ -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) + 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) diff --git a/tests/test_financial.py b/tests/test_financial.py index bb98724..17240e9 100644 --- a/tests/test_financial.py +++ b/tests/test_financial.py @@ -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)