-
-
Notifications
You must be signed in to change notification settings - Fork 87
Updated irr function [issue 98] #99
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
Changes from 3 commits
efbff01
223e483
2291b91
228ffaa
16d4399
4c7b4a4
c619b40
af10590
c1a6322
605bab2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the roots docs:
However I'm not sure if it's worth to construct a whole There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
IRR = np.real(g[np.isreal(g)]) - 1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: I don't like having all capital variable names, could you rename it to something meaningful? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IRR -> internal_rate_return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would work. Or maybe something like |
||
|
||
for _ in range(maxiter): | ||
delta = npv_(g) / d_npv(g) | ||
if abs(delta) < tol: | ||
return g - 1 | ||
g -= delta | ||
# realistic IRR | ||
IRR = IRR[IRR >= -1] | ||
|
||
if raise_exceptions: | ||
raise IterationsExceededError('Maximum number of iterations exceeded.') | ||
# if no real solution | ||
if len(IRR) == 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(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(values[values>0]) | ||
neg = sum(values[values<0]) | ||
if pos > neg: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand why you are trying to filter the by the total cashflow. Is this just some heuristic you have picked or is there a more formal process going on here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just some heuristic I have picked. It works when there are real roots with different signs. I can give you a simple example and walk you through it: real roots = -0.5 and 0.1 As the net sum > 0, we can kind of exclude the possibility that IRR is -0.5 and keep only 0.1. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense. Thanks for clarifying. |
||
IRR = IRR[IRR > 0] | ||
else: | ||
IRR = IRR[IRR < 0] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be indented further? |
||
|
||
# pick the smallest one in magnitude and return | ||
abs_IRR = np.abs(IRR) | ||
return IRR[np.argmin(abs_IRR)] | ||
|
||
|
||
@nb.njit(parallel=True) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a missing blank line here at the end of the file |
Uh oh!
There was an error while loading. Please reload this page.