From efbff0140de015d33f637e0bf5ce9f8f86ed0cd4 Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Thu, 4 Jan 2024 21:35:32 -0500 Subject: [PATCH 1/9] Updated irr function calculate IRR by calculating all its real roots --- numpy_financial/_financial.py | 69 +++++++++++++++++------------------ 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 033495d..818d0f0 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): r"""Return the Internal Rate of Return (IRR). This is the "average" periodically compounded rate of return @@ -743,20 +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 - having reached the maximum number of iterations (IterationsExceededException). - Set to False as default, thus returning NaNs in the two previous - cases. Returns ------- @@ -816,13 +802,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 +819,38 @@ 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 - - for _ in range(maxiter): - delta = npv_(g) / d_npv(g) - if abs(delta) < tol: - return g - 1 - g -= delta - - if raise_exceptions: - raise IterationsExceededError('Maximum number of iterations exceeded.') - - return np.nan + + g = np.roots(values) + IRR = np.real(g[np.isreal(g)]) - 1 + + # realistic IRR + IRR = IRR[IRR >= -1] + + # if no real solution + if len(IRR) == 0: + raise NoRealSolutionError("No real solution is found for IRR.") + + # if only one real solution + if len(IRR) == 1: + return IRR[0] + + # below is for the situation when there are more than 2 real solutions. + # check sign of all IRR solutions + same_sign = np.all(IRR > 0) if IRR[0] > 0 else np.all(IRR < 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(cash_flow[cash_flow>0]) + neg = sum(cash_flow[cash_flow<0]) + if pos > neg: + IRR = IRR[IRR > 0] + else: + IRR = IRR[IRR < 0] + + # pick the smallest one in magnitude and return + abs_IRR = np.abs(IRR) + return IRR[np.argmin(abs_IRR)] @nb.njit(parallel=True) From 223e4832126f8318b1abe6859e374a264a14a7d2 Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Fri, 5 Jan 2024 02:12:28 -0500 Subject: [PATCH 2/9] Update _financial.py Corrected variable name `values` from ``cash_flow --- numpy_financial/_financial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 818d0f0..9dc32d5 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -841,8 +841,8 @@ def irr(values): # 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(cash_flow[cash_flow>0]) - neg = sum(cash_flow[cash_flow<0]) + pos = sum(values[values>0]) + neg = sum(values[values<0]) if pos > neg: IRR = IRR[IRR > 0] else: From 2291b912e2138bb799c637fb78be159c5d7e5597 Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Fri, 5 Jan 2024 22:47:42 -0500 Subject: [PATCH 3/9] Updated tests/test_financial.py and nump_financial/_financial.py - test_financial.py: Altered test cases - _financial.py: Added optional argument `raise_exceptions` --- numpy_financial/_financial.py | 12 ++++++++++-- tests/test_financial.py | 11 +---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 9dc32d5..f1e9e4d 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -727,7 +727,7 @@ def rate( return rn -def irr(values): +def irr(values, raise_exceptions=False): r"""Return the Internal Rate of Return (IRR). This is the "average" periodically compounded rate of return @@ -743,6 +743,12 @@ def irr(values): 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. + 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 + having reached the maximum number of iterations (IterationsExceededException). + Set to False as default, thus returning NaNs in the two previous + cases. Returns ------- @@ -828,7 +834,9 @@ def irr(values): # if no real solution if len(IRR) == 0: - raise NoRealSolutionError("No real solution is found for IRR.") + if raise_exceptions: + raise NoRealSolutionError("No real solution is found for IRR.") + return np.nan # if only one real solution if len(IRR) == 1: diff --git a/tests/test_financial.py b/tests/test_financial.py index 2f8f63d..0d16459 100644 --- a/tests/test_financial.py +++ b/tests/test_financial.py @@ -764,13 +764,4 @@ def test_irr_no_real_solution_exception(self): cashflows = numpy.array([40000, 5000, 8000, 12000, 30000]) 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) + npf.irr(cashflows, raise_exceptions=True) \ No newline at end of file From 228ffaadede689cd9bc9eff1f35d38e1f19222cc Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Sat, 6 Jan 2024 17:59:29 -0500 Subject: [PATCH 4/9] Updated - Fixed error and changed IRR -> internal_rate_of_return - Included 2 more test cases --- numpy_financial/_financial.py | 24 ++++++++++++------------ tests/test_financial.py | 5 ++++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index f1e9e4d..e381f11 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -827,38 +827,38 @@ def irr(values, raise_exceptions=False): # as eirr = g - 1 (if we are close enough to a solution) g = np.roots(values) - IRR = np.real(g[np.isreal(g)]) - 1 + internal_rate_of_return = np.real(g[np.isreal(g)]) - 1 # realistic IRR - IRR = IRR[IRR >= -1] + internal_rate_of_return = internal_rate_of_return[internal_rate_of_return >= -1] # if no real solution - if len(IRR) == 0: + if len(internal_rate_of_return) == 0: if raise_exceptions: raise NoRealSolutionError("No real solution is found for IRR.") return np.nan # if only one real solution - if len(IRR) == 1: - return IRR[0] + if len(internal_rate_of_return) == 1: + return internal_rate_of_return[0] # below is for the situation when there are more than 2 real solutions. # check sign of all IRR solutions - same_sign = np.all(IRR > 0) if IRR[0] > 0 else np.all(IRR < 0) + same_sign = np.all(internal_rate_of_return > 0) if internal_rate_of_return[0] > 0 else np.all(internal_rate_of_return < 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: - IRR = IRR[IRR > 0] - else: - IRR = IRR[IRR < 0] + if pos >= neg: + internal_rate_of_return = internal_rate_of_return[internal_rate_of_return >= 0] + else: + internal_rate_of_return = internal_rate_of_return[internal_rate_of_return < 0] # pick the smallest one in magnitude and return - abs_IRR = np.abs(IRR) - return IRR[np.argmin(abs_IRR)] + abs_internal_rate_of_return = np.abs(internal_rate_of_return) + return internal_rate_of_return[np.argmin(abs_internal_rate_of_return)] @nb.njit(parallel=True) diff --git a/tests/test_financial.py b/tests/test_financial.py index 0d16459..0b52170 100644 --- a/tests/test_financial.py +++ b/tests/test_financial.py @@ -680,6 +680,8 @@ def test_npv_irr_congruence(self): ([-100, 100, 0, -7], -0.0833), ([-100, 100, 0, 7], 0.06206), ([-5, 10.5, 1, -8, 1], 0.0886), + ([-10000, 4000, 200, 6800, -1000, 40000, -30000, -10000], 0.0000), + ([-10000, 4000, 2000, 1000, 3000], 0.0000), ]) def test_basic_values(self, v, desired): assert_almost_equal(npf.irr(v), desired, decimal=2) @@ -764,4 +766,5 @@ def test_irr_no_real_solution_exception(self): cashflows = numpy.array([40000, 5000, 8000, 12000, 30000]) with pytest.raises(npf.NoRealSolutionError): - npf.irr(cashflows, raise_exceptions=True) \ No newline at end of file + npf.irr(cashflows, raise_exceptions=True) + \ No newline at end of file From 16d439959f80141bbcecffcd1a40abff5c43dc28 Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:01:06 -0500 Subject: [PATCH 5/9] Update test_financial.py indentation --- tests/test_financial.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_financial.py b/tests/test_financial.py index 0b52170..e733b39 100644 --- a/tests/test_financial.py +++ b/tests/test_financial.py @@ -767,4 +767,3 @@ def test_irr_no_real_solution_exception(self): with pytest.raises(npf.NoRealSolutionError): npf.irr(cashflows, raise_exceptions=True) - \ No newline at end of file From 4c7b4a4197e77320809e59fff229792c3ec6705a Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:44:49 -0500 Subject: [PATCH 6/9] Update _financial.py for line too long error - internal_rate_of_return -> eirr - abs_internal_rate_of_return -> abs_eirr --- numpy_financial/_financial.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index e381f11..8746b25 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -827,24 +827,24 @@ def irr(values, raise_exceptions=False): # as eirr = g - 1 (if we are close enough to a solution) g = np.roots(values) - internal_rate_of_return = np.real(g[np.isreal(g)]) - 1 + eirr = np.real(g[np.isreal(g)]) - 1 # realistic IRR - internal_rate_of_return = internal_rate_of_return[internal_rate_of_return >= -1] + eirr = eirr[eirr>=-1] # if no real solution - if len(internal_rate_of_return) == 0: + if len(eirr) == 0: if raise_exceptions: raise NoRealSolutionError("No real solution is found for IRR.") return np.nan # if only one real solution - if len(internal_rate_of_return) == 1: - return internal_rate_of_return[0] + 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(internal_rate_of_return > 0) if internal_rate_of_return[0] > 0 else np.all(internal_rate_of_return < 0) + 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. @@ -852,13 +852,13 @@ def irr(values, raise_exceptions=False): pos = sum(values[values>0]) neg = sum(values[values<0]) if pos >= neg: - internal_rate_of_return = internal_rate_of_return[internal_rate_of_return >= 0] + eirr = eirr[eirr>=0] else: - internal_rate_of_return = internal_rate_of_return[internal_rate_of_return < 0] + eirr = eirr[eirr<0] # pick the smallest one in magnitude and return - abs_internal_rate_of_return = np.abs(internal_rate_of_return) - return internal_rate_of_return[np.argmin(abs_internal_rate_of_return)] + abs_eirr = np.abs(eirr) + return eirr[np.argmin(abs_eirr)] @nb.njit(parallel=True) From af10590d1ebd60cde141f40a699558fc9554b8f9 Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:52:57 -0500 Subject: [PATCH 7/9] Update test_financial.py --- tests/test_financial.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_financial.py b/tests/test_financial.py index 0041e5a..7bccfb2 100644 --- a/tests/test_financial.py +++ b/tests/test_financial.py @@ -805,7 +805,6 @@ def test_npv_irr_congruence(self): ([-100, 100, 0, 7], 0.06206), ([-5, 10.5, 1, -8, 1], 0.0886), ([-10000, 4000, 200, 6800, -1000, 40000, -30000, -10000], 0.0000), - ([-10000, 4000, 2000, 1000, 3000], 0.0000), ], ) def test_basic_values(self, v, desired): From c1a6322d38400b3404226f37d9637755268eefbc Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Sun, 21 Jan 2024 02:09:17 -0500 Subject: [PATCH 8/9] Update test_financial.py --- tests/test_financial.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_financial.py b/tests/test_financial.py index 7bccfb2..17240e9 100644 --- a/tests/test_financial.py +++ b/tests/test_financial.py @@ -804,7 +804,6 @@ def test_npv_irr_congruence(self): ([-100, 100, 0, -7], -0.0833), ([-100, 100, 0, 7], 0.06206), ([-5, 10.5, 1, -8, 1], 0.0886), - ([-10000, 4000, 200, 6800, -1000, 40000, -30000, -10000], 0.0000), ], ) def test_basic_values(self, v, desired): From 605bab24b347025fe7186e9302a9063f662ccb27 Mon Sep 17 00:00:00 2001 From: Jasper Lee <69416199+yatshunlee@users.noreply.github.com> Date: Sun, 10 Mar 2024 23:25:23 -0400 Subject: [PATCH 9/9] Update _financial.py --- numpy_financial/_financial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 8746b25..3bb975f 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -727,7 +727,7 @@ def rate( return rn -def irr(values, 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