Skip to content

Commit 10a386e

Browse files
committed
ENH: Use Newton's method to calculate IRR
1 parent 100817c commit 10a386e

File tree

1 file changed

+18
-49
lines changed

1 file changed

+18
-49
lines changed

numpy_financial/_financial.py

Lines changed: 18 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -671,36 +671,7 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100):
671671
return rn
672672

673673

674-
def _roots(p):
675-
"""Modified version of NumPy's roots function.
676-
677-
NumPy's roots uses the companion matrix method, which divides by
678-
p[0]. This can causes overflows/underflows. Instead form a
679-
modified companion matrix that is scaled by 2^c * p[0], where the
680-
exponent c is chosen to balance the magnitudes of the
681-
coefficients. Since scaling the matrix just scales the
682-
eigenvalues, we can remove the scaling at the end.
683-
684-
Scaling by a power of 2 is chosen to avoid rounding errors.
685-
686-
"""
687-
_, e = np.frexp(p)
688-
# Balance the most extreme exponents e_max and e_min by solving
689-
# the equation
690-
#
691-
# |c + e_max| = |c + e_min|.
692-
#
693-
# Round the exponent to an integer to avoid rounding errors.
694-
c = int(-0.5 * (np.max(e) + np.min(e)))
695-
p = np.ldexp(p, c)
696-
697-
A = np.diag(np.full(p.size - 2, p[0]), k=-1)
698-
A[0,:] = -p[1:]
699-
eigenvalues = np.linalg.eigvals(A)
700-
return eigenvalues / p[0]
701-
702-
703-
def irr(values):
674+
def irr(values, guess=0.1):
704675
"""
705676
Return the Internal Rate of Return (IRR).
706677
@@ -717,6 +688,9 @@ def irr(values):
717688
are negative and net "withdrawals" are positive. Thus, for
718689
example, at least the first element of `values`, which represents
719690
the initial investment, will typically be negative.
691+
guess : float, optional
692+
Initial guess of the IRR for the iterative solver. If no guess is
693+
given 0.1 is used instead.
720694
721695
Returns
722696
-------
@@ -767,28 +741,23 @@ def irr(values):
767741
if values.ndim != 1:
768742
raise ValueError("Cashflows must be a rank-1 array")
769743

770-
# Strip leading and trailing zeros. Since we only care about
771-
# positive roots we can neglect roots at zero.
772-
non_zero = np.nonzero(np.ravel(values))[0]
773-
values = values[int(non_zero[0]):int(non_zero[-1])+1]
744+
solution_found = False
745+
p = np.polynomial.Polynomial(values)
746+
pp = p.deriv()
747+
x = 1 / (1 + guess)
774748

775-
res = _roots(values[::-1])
749+
for i in range(100):
750+
x_new = x - (p(x) / pp(x))
751+
if abs(x_new - x) < 1e-12:
752+
solution_found = True
753+
break
754+
x = x_new
776755

777-
mask = (res.imag == 0) & (res.real > 0)
778-
if not mask.any():
756+
if solution_found:
757+
return 1 / x - 1
758+
else:
779759
return np.nan
780-
res = res[mask].real
781-
# NPV(rate) = 0 can have more than one solution so we return
782-
# only the solution closest to zero.
783-
rate = 1/res - 1
784-
785-
# If there are any positive solutions prefer those over negative
786-
# rates.
787-
if (rate > 0).any():
788-
rate = np.where(rate > 0, rate, np.inf)
789-
790-
rate = rate.item(np.argmin(np.abs(rate)))
791-
return rate
760+
792761

793762

794763
def npv(rate, values):

0 commit comments

Comments
 (0)