Skip to content

Commit e50c610

Browse files
author
Javier López Peña
committed
Use heuristic for guess and reverse polynomial for stability
1 parent 4046a2c commit e50c610

File tree

1 file changed

+25
-15
lines changed

1 file changed

+25
-15
lines changed

numpy_financial/_financial.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010
Functions support the :class:`decimal.Decimal` type unless
1111
otherwise stated.
1212
"""
13-
from __future__ import division, absolute_import, print_function
13+
from __future__ import absolute_import, division, print_function
1414

1515
from decimal import Decimal
1616

1717
import numpy as np
1818

19-
2019
__all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate',
2120
'irr', 'npv', 'mirr']
2221

@@ -675,7 +674,7 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100):
675674
return rn
676675

677676

678-
def irr(values, guess=0.1, tol=1e-12, maxiter=100):
677+
def irr(values, guess=None, tol=1e-12, maxiter=100):
679678
"""
680679
Return the Internal Rate of Return (IRR).
681680
@@ -694,7 +693,8 @@ def irr(values, guess=0.1, tol=1e-12, maxiter=100):
694693
the initial investment, will typically be negative.
695694
guess : float, optional
696695
Initial guess of the IRR for the iterative solver. If no guess is
697-
given an initial guess of 0.1 (i.e. 10%) is assumed instead.
696+
given an heuristic is used to estimate the guess through the ratio of
697+
positive to negative cash lows
698698
tol : float, optional
699699
Required tolerance to accept solution. Default is 1e-12.
700700
maxiter : int, optional
@@ -755,28 +755,38 @@ def irr(values, guess=0.1, tol=1e-12, maxiter=100):
755755
if same_sign:
756756
return np.nan
757757

758+
# If no value is passed for `guess`, then make a heuristic estimate
759+
if guess is None:
760+
inflow = sum(x for x in values if x > 0)
761+
outflow = -sum(x for x in values if x < 0)
762+
guess = inflow / outflow - 1
763+
758764
# We aim to solve eirr such that NPV is exactly zero. This can be framed as
759765
# simply finding the closest root of a polynomial to a given initial guess
760766
# as follows:
761767
# V0 V1 V2 V3
762-
# NPV = ---------- + ---------- + ---------- + ---------- + ...
768+
# NPV = ---------- + ---------- + ---------- + ---------- + ... = 0
763769
# (1+eirr)^0 (1+eirr)^1 (1+eirr)^2 (1+eirr)^3
764770
#
765-
# by letting x = 1 / (1+eirr), we substitute to get
771+
# by letting g = (1+eirr), we substitute to get
772+
#
773+
# NPV = V0 * 1/g^0 + V1 * 1/g^1 + V2 * 1/x^2 + V3 * 1/g^3 + ... = 0
774+
#
775+
# Multiplying by g^N this becomes
776+
#
777+
# V0 * g^N + V1 * g^{N-1} + V2 * g^{N-2} + V3 * g^{N-3} + ... = 0
766778
#
767-
# NPV = V0 * x^0 + V1 * x^1 + V2 * x^2 + V3 * x^3 + ...
768-
#
769-
# which we solve using Newton-Raphson and then reverse out the solution
770-
# as eirr = 1/x - 1 (if we are close enough to a solution)
771-
npv_ = np.polynomial.Polynomial(values)
779+
# which we solve using Newton-Raphson and then reverse out the solution
780+
# as eirr = g - 1 (if we are close enough to a solution)
781+
npv_ = np.polynomial.Polynomial(values[::-1])
772782
d_npv = npv_.deriv()
773-
x = 1 / (1 + guess)
783+
g = 1 + guess
774784

775785
for _ in range(maxiter):
776-
delta = npv_(x) / d_npv(x)
786+
delta = npv_(g) / d_npv(g)
777787
if abs(delta) < tol:
778-
return 1 / (x - delta) - 1
779-
x -= delta
788+
return g - 1
789+
g -= delta
780790

781791
return np.nan
782792

0 commit comments

Comments
 (0)