Skip to content

Commit efbff01

Browse files
committed
Updated irr function
calculate IRR by calculating all its real roots
1 parent 1656639 commit efbff01

File tree

1 file changed

+33
-36
lines changed

1 file changed

+33
-36
lines changed

numpy_financial/_financial.py

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ def rate(
727727
return rn
728728

729729

730-
def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
730+
def irr(values):
731731
r"""Return the Internal Rate of Return (IRR).
732732
733733
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):
743743
are negative and net "withdrawals" are positive. Thus, for
744744
example, at least the first element of `values`, which represents
745745
the initial investment, will typically be negative.
746-
guess : float, optional
747-
Initial guess of the IRR for the iterative solver. If no guess is
748-
given an heuristic is used to estimate the guess through the ratio of
749-
positive to negative cash lows
750-
tol : float, optional
751-
Required tolerance to accept solution. Default is 1e-12.
752-
maxiter : int, optional
753-
Maximum iterations to perform in finding a solution. Default is 100.
754-
raise_exceptions: bool, optional
755-
Flag to raise an exception when the irr cannot be computed due to
756-
either having all cashflows of the same sign (NoRealSolutionException) or
757-
having reached the maximum number of iterations (IterationsExceededException).
758-
Set to False as default, thus returning NaNs in the two previous
759-
cases.
760746
761747
Returns
762748
-------
@@ -816,13 +802,6 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
816802
'cashflows are of the same sign.')
817803
return np.nan
818804

819-
# If no value is passed for `guess`, then make a heuristic estimate
820-
if guess is None:
821-
positive_cashflow = values > 0
822-
inflow = values.sum(where=positive_cashflow)
823-
outflow = -values.sum(where=~positive_cashflow)
824-
guess = inflow / outflow - 1
825-
826805
# We aim to solve eirr such that NPV is exactly zero. This can be framed as
827806
# simply finding the closest root of a polynomial to a given initial guess
828807
# as follows:
@@ -840,20 +819,38 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
840819
#
841820
# which we solve using Newton-Raphson and then reverse out the solution
842821
# as eirr = g - 1 (if we are close enough to a solution)
843-
npv_ = np.polynomial.Polynomial(values[::-1])
844-
d_npv = npv_.deriv()
845-
g = 1 + guess
846-
847-
for _ in range(maxiter):
848-
delta = npv_(g) / d_npv(g)
849-
if abs(delta) < tol:
850-
return g - 1
851-
g -= delta
852-
853-
if raise_exceptions:
854-
raise IterationsExceededError('Maximum number of iterations exceeded.')
855-
856-
return np.nan
822+
823+
g = np.roots(values)
824+
IRR = np.real(g[np.isreal(g)]) - 1
825+
826+
# realistic IRR
827+
IRR = IRR[IRR >= -1]
828+
829+
# if no real solution
830+
if len(IRR) == 0:
831+
raise NoRealSolutionError("No real solution is found for IRR.")
832+
833+
# if only one real solution
834+
if len(IRR) == 1:
835+
return IRR[0]
836+
837+
# below is for the situation when there are more than 2 real solutions.
838+
# check sign of all IRR solutions
839+
same_sign = np.all(IRR > 0) if IRR[0] > 0 else np.all(IRR < 0)
840+
841+
# if the signs of IRR solutions are not the same, first filter potential IRR
842+
# by comparing the total positive and negative cash flows.
843+
if not same_sign:
844+
pos = sum(cash_flow[cash_flow>0])
845+
neg = sum(cash_flow[cash_flow<0])
846+
if pos > neg:
847+
IRR = IRR[IRR > 0]
848+
else:
849+
IRR = IRR[IRR < 0]
850+
851+
# pick the smallest one in magnitude and return
852+
abs_IRR = np.abs(IRR)
853+
return IRR[np.argmin(abs_IRR)]
857854

858855

859856
@nb.njit(parallel=True)

0 commit comments

Comments
 (0)