Skip to content

Commit 37c4937

Browse files
authored
Merge pull request #62 from 101AlexMartin/irr-fix
Small addition to IRR function
2 parents fb63b04 + f57ae85 commit 37c4937

File tree

2 files changed

+91
-5
lines changed

2 files changed

+91
-5
lines changed

numpy_financial/_financial.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import numpy as np
1818

1919
__all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate',
20-
'irr', 'npv', 'mirr']
20+
'irr', 'npv', 'mirr',
21+
'NoRealSolutionException', 'IterationsExceededException']
2122

2223
_when_to_num = {'end': 0, 'begin': 1,
2324
'e': 0, 'b': 1,
@@ -26,6 +27,18 @@
2627
'start': 1,
2728
'finish': 0}
2829

30+
# Define custom Exceptions
31+
32+
class NoRealSolutionException(Exception):
33+
""" No real solution to the problem. """
34+
35+
pass
36+
37+
class IterationsExceededException(Exception):
38+
""" Maximum number of iterations reached. """
39+
40+
pass
41+
2942

3043
def _convert_when(when):
3144
# Test to see if when has already been converted to ndarray
@@ -598,7 +611,7 @@ def _g_div_gp(r, n, p, x, y, w):
598611
# where
599612
# g(r) is the formula
600613
# g'(r) is the derivative with respect to r.
601-
def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100):
614+
def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100, *, raise_exceptions=False):
602615
"""
603616
Compute the rate of interest per period.
604617
@@ -620,6 +633,11 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100):
620633
Required tolerance for the solution, default 1e-6
621634
maxiter : int, optional
622635
Maximum iterations in finding the solution
636+
raise_exceptions: bool, optional
637+
Flag to raise an exception when at least one of the rates
638+
cannot be computed due to having reached the maximum number of
639+
iterations (IterationsExceededException). Set to False as default,
640+
thus returning NaNs for those rates.
623641
624642
Notes
625643
-----
@@ -666,15 +684,20 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100):
666684

667685
if not np.all(close):
668686
if np.isscalar(rn):
687+
if raise_exceptions:
688+
raise IterationsExceededException('Maximum number of iterations exceeded.')
669689
return default_type(np.nan)
670690
else:
671691
# Return nan's in array of the same shape as rn
672692
# where the solution is not close to tol.
693+
if raise_exceptions:
694+
raise IterationsExceededException(f'Maximum number of iterations exceeded in '
695+
f'{len(close)-close.sum()} rate(s).')
673696
rn[~close] = np.nan
674697
return rn
675698

676699

677-
def irr(values, guess=None, tol=1e-12, maxiter=100):
700+
def irr(values, guess=None, *, tol=1e-12, maxiter=100, raise_exceptions=False):
678701
"""
679702
Return the Internal Rate of Return (IRR).
680703
@@ -699,6 +722,12 @@ def irr(values, guess=None, tol=1e-12, maxiter=100):
699722
Required tolerance to accept solution. Default is 1e-12.
700723
maxiter : int, optional
701724
Maximum iterations to perform in finding a solution. Default is 100.
725+
raise_exceptions: bool, optional
726+
Flag to raise an exception when the irr cannot be computed due to
727+
either having all cashflows of the same sign (NoRealSolutionException) or
728+
having reached the maximum number of iterations (IterationsExceededException).
729+
Set to False as default, thus returning NaNs in the two previous
730+
cases.
702731
703732
Returns
704733
-------
@@ -753,6 +782,9 @@ def irr(values, guess=None, tol=1e-12, maxiter=100):
753782
# we don't perform any further calculations and exit early
754783
same_sign = np.all(values > 0) if values[0] > 0 else np.all(values < 0)
755784
if same_sign:
785+
if raise_exceptions:
786+
raise NoRealSolutionException('No real solution exists for IRR since all '
787+
'cashflows are of the same sign.')
756788
return np.nan
757789

758790
# If no value is passed for `guess`, then make a heuristic estimate
@@ -789,6 +821,9 @@ def irr(values, guess=None, tol=1e-12, maxiter=100):
789821
return g - 1
790822
g -= delta
791823

824+
if raise_exceptions:
825+
raise IterationsExceededException('Maximum number of iterations exceeded.')
826+
792827
return np.nan
793828

794829

@@ -871,7 +906,7 @@ def npv(rate, values):
871906
return npv
872907

873908

874-
def mirr(values, finance_rate, reinvest_rate):
909+
def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
875910
"""
876911
Modified internal rate of return.
877912
@@ -885,6 +920,11 @@ def mirr(values, finance_rate, reinvest_rate):
885920
Interest rate paid on the cash flows
886921
reinvest_rate : scalar
887922
Interest rate received on the cash flows upon reinvestment
923+
raise_exceptions: bool, optional
924+
Flag to raise an exception when the mirr cannot be computed due to
925+
having all cashflows of the same sign (NoRealSolutionException).
926+
Set to False as default, thus returning NaNs in the previous
927+
case.
888928
889929
Returns
890930
-------
@@ -904,6 +944,9 @@ def mirr(values, finance_rate, reinvest_rate):
904944
pos = values > 0
905945
neg = values < 0
906946
if not (pos.any() and neg.any()):
947+
if raise_exceptions:
948+
raise NoRealSolutionException('No real solution exists for MIRR since'
949+
' all cashflows are of the same sign.')
907950
return np.nan
908951
numer = np.abs(npv(reinvest_rate, values*pos))
909952
denom = np.abs(npv(finance_rate, values*neg))

numpy_financial/tests/test_financial.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# the versions in numpy instead of numpy_financial.
66
import numpy
77
from numpy.testing import (
8-
assert_, assert_almost_equal, assert_allclose, assert_equal
8+
assert_, assert_almost_equal, assert_allclose, assert_equal, assert_raises
99
)
1010
import pytest
1111

@@ -136,6 +136,26 @@ def test_gh48(self):
136136
actual = npf.rate(nper, pmt, pv, fv)
137137
assert_allclose(actual, des)
138138

139+
def test_rate_maximum_iterations_exception_scalar(self):
140+
# Test that if the maximum number of iterations is reached,
141+
# then npf.rate returns IterationsExceededException
142+
# when raise_exceptions is set to True.
143+
assert_raises(npf.IterationsExceededException, npf.rate, Decimal(12.0),
144+
Decimal(400.0), Decimal(10000.0), Decimal(5000.0),
145+
raise_exceptions=True)
146+
147+
def test_rate_maximum_iterations_exception_array(self):
148+
# Test that if the maximum number of iterations is reached in at least
149+
# one rate, then npf.rate returns IterationsExceededException
150+
# when raise_exceptions is set to True.
151+
nper = 2
152+
pmt = 0
153+
pv = [-593.06, -4725.38, -662.05, -428.78, -13.65]
154+
fv = [214.07, 4509.97, 224.11, 686.29, -329.67]
155+
assert_raises(npf.IterationsExceededException, npf.rate, nper,
156+
pmt, pv, fv,
157+
raise_exceptions=True)
158+
139159

140160
class TestNpv:
141161
def test_npv(self):
@@ -248,6 +268,13 @@ def test_mirr_decimal(self):
248268
Decimal('37000'), Decimal('46000')]
249269
assert_(numpy.isnan(npf.mirr(val, Decimal('0.10'), Decimal('0.12'))))
250270

271+
def test_mirr_no_real_solution_exception(self):
272+
# Test that if there is no solution because all the cashflows
273+
# have the same sign, then npf.mirr returns NoRealSolutionException
274+
# when raise_exceptions is set to True.
275+
val = [39000, 30000, 21000, 37000, 46000]
276+
assert_raises(npf.NoRealSolutionException, npf.mirr, val, 0.10, 0.12, raise_exceptions=True)
277+
251278

252279
class TestNper:
253280
def test_basic_values(self):
@@ -575,6 +602,7 @@ def test_some_rates_zero(self):
575602

576603

577604
class TestIrr:
605+
578606
def test_npv_irr_congruence(self):
579607
# IRR is defined as the rate required for the present value of
580608
# a series of cashflows to be zero, so we should have
@@ -671,3 +699,18 @@ def test_gh_44(self):
671699
# "true" value as calculated by Google sheets
672700
cf = [-1678.87, 771.96, 1814.05, 3520.30, 3552.95, 3584.99, 4789.91, -1]
673701
assert_almost_equal(npf.irr(cf), 1.00426, 4)
702+
703+
def test_irr_no_real_solution_exception(self):
704+
# Test that if there is no solution because all the cashflows
705+
# have the same sign, then npf.irr returns NoRealSolutionException
706+
# when raise_exceptions is set to True.
707+
cashflows = numpy.array([40000, 5000, 8000, 12000, 30000])
708+
assert_raises(npf.NoRealSolutionException, npf.irr, cashflows, raise_exceptions=True)
709+
710+
def test_irr_maximum_iterations_exception(self):
711+
# Test that if the maximum number of iterations is reached,
712+
# then npf.irr returns IterationsExceededException
713+
# when raise_exceptions is set to True.
714+
cashflows = numpy.array([-40000, 5000, 8000, 12000, 30000])
715+
assert_raises(npf.IterationsExceededException, npf.irr, cashflows,
716+
maxiter=1, raise_exceptions=True)

0 commit comments

Comments
 (0)