Skip to content

Avoid redundant computations in IRR calculation #60

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

Merged
merged 3 commits into from
Feb 22, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions numpy_financial/_financial.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
Functions support the :class:`decimal.Decimal` type unless
otherwise stated.
"""
from __future__ import division, absolute_import, print_function
from __future__ import absolute_import, division, print_function

from decimal import Decimal

import numpy as np


__all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate',
'irr', 'npv', 'mirr']

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


def irr(values, guess=0.1, tol=1e-12, maxiter=100):
def irr(values, guess=None, tol=1e-12, maxiter=100):
"""
Return the Internal Rate of Return (IRR).

Expand All @@ -694,7 +693,8 @@ def irr(values, guess=0.1, tol=1e-12, maxiter=100):
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 initial guess of 0.1 (i.e. 10%) is assumed instead.
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
Expand Down Expand Up @@ -755,28 +755,39 @@ def irr(values, guess=0.1, tol=1e-12, maxiter=100):
if 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:
# V0 V1 V2 V3
# NPV = ---------- + ---------- + ---------- + ---------- + ...
# NPV = ---------- + ---------- + ---------- + ---------- + ... = 0
# (1+eirr)^0 (1+eirr)^1 (1+eirr)^2 (1+eirr)^3
#
# by letting x = 1 / (1+eirr), we substitute to get
# by letting g = (1+eirr), we substitute to get
#
# NPV = V0 * 1/g^0 + V1 * 1/g^1 + V2 * 1/x^2 + V3 * 1/g^3 + ... = 0
#
# Multiplying by g^N this becomes
#
# V0 * g^N + V1 * g^{N-1} + V2 * g^{N-2} + V3 * g^{N-3} + ... = 0
#
# NPV = V0 * x^0 + V1 * x^1 + V2 * x^2 + V3 * x^3 + ...
#
# which we solve using Newton-Raphson and then reverse out the solution
# as eirr = 1/x - 1 (if we are close enough to a solution)
npv_ = np.polynomial.Polynomial(values)
# 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()
x = 1 / (1 + guess)
g = 1 + guess

for _ in range(maxiter):
x_new = x - (npv_(x) / d_npv(x))
if abs(x_new - x) < tol:
return 1 / x_new - 1
x = x_new
delta = npv_(g) / d_npv(g)
if abs(delta) < tol:
return g - 1
g -= delta

return np.nan

Expand Down