13
13
14
14
from decimal import Decimal
15
15
16
+ import numba as nb
16
17
import numpy as np
17
18
18
19
__all__ = ['fv' , 'pmt' , 'nper' , 'ipmt' , 'ppmt' , 'pv' , 'rate' ,
@@ -46,6 +47,36 @@ def _convert_when(when):
46
47
return [_when_to_num [x ] for x in when ]
47
48
48
49
50
+ def _return_ufunc_like (array ):
51
+ try :
52
+ # If size of array is one, return scalar
53
+ return array .item ()
54
+ except ValueError :
55
+ # Otherwise, return entire array
56
+ return array
57
+
58
+
59
+ def _is_object_array (array ):
60
+ return array .dtype == np .dtype ("O" )
61
+
62
+
63
+ def _use_decimal_dtype (* arrays ):
64
+ return any (_is_object_array (array ) for array in arrays )
65
+
66
+
67
+ def _to_decimal_array_1d (array ):
68
+ return np .array ([Decimal (x ) for x in array .tolist ()])
69
+
70
+
71
+ def _to_decimal_array_2d (array ):
72
+ decimals = [Decimal (x ) for row in array .tolist () for x in row ]
73
+ return np .array (decimals ).reshape (array .shape )
74
+
75
+
76
+ def _get_output_array_shape (* arrays ):
77
+ return tuple (array .shape [0 ] for array in arrays )
78
+
79
+
49
80
def fv (rate , nper , pmt , pv , when = 'end' ):
50
81
"""Compute the future value.
51
82
@@ -825,14 +856,35 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
825
856
return np .nan
826
857
827
858
859
+ @nb .njit (parallel = True )
860
+ def _npv_native (rates , values , out ):
861
+ for i in nb .prange (rates .shape [0 ]):
862
+ for j in nb .prange (values .shape [0 ]):
863
+ acc = 0.0
864
+ for t in range (values .shape [1 ]):
865
+ acc += values [j , t ] / ((1.0 + rates [i ]) ** t )
866
+ out [i , j ] = acc
867
+
868
+
869
+ # We require ``forceobj=True`` here to support decimal.Decimal types
870
+ @nb .jit (forceobj = True )
871
+ def _npv_decimal (rates , values , out ):
872
+ for i in range (rates .shape [0 ]):
873
+ for j in range (values .shape [0 ]):
874
+ acc = Decimal ("0.0" )
875
+ for t in range (values .shape [1 ]):
876
+ acc += values [j , t ] / ((Decimal ("1.0" ) + rates [i ]) ** t )
877
+ out [i , j ] = acc
878
+
879
+
828
880
def npv (rate , values ):
829
881
r"""Return the NPV (Net Present Value) of a cash flow series.
830
882
831
883
Parameters
832
884
----------
833
- rate : scalar
885
+ rate : scalar or array_like shape(K, )
834
886
The discount rate.
835
- values : array_like, shape(M, )
887
+ values : array_like, shape(M, ) or shape(M, N)
836
888
The values of the time series of cash flows. The (fixed) time
837
889
interval between cash flow "events" must be the same as that for
838
890
which `rate` is given (i.e., if `rate` is per year, then precisely
@@ -843,9 +895,10 @@ def npv(rate, values):
843
895
844
896
Returns
845
897
-------
846
- out : float
898
+ out : float or array shape(K, M)
847
899
The NPV of the input cash flow series `values` at the discount
848
- `rate`.
900
+ `rate`. `out` follows the ufunc convention of returning scalars
901
+ instead of single element arrays.
849
902
850
903
Warnings
851
904
--------
@@ -891,16 +944,58 @@ def npv(rate, values):
891
944
>>> np.round(npf.npv(rate, cashflows) + initial_cashflow, 5)
892
945
3065.22267
893
946
947
+ The NPV calculation may be applied to several ``rates`` and ``cashflows``
948
+ simulatneously. This produces an array of shape
949
+ ``(len(rates), len(cashflows))``.
950
+
951
+ >>> rates = [0.00, 0.05, 0.10]
952
+ >>> cashflows = [[-4_000, 500, 800], [-5_000, 600, 900]]
953
+ >>> npf.npv(rates, cashflows).round(2)
954
+ array([[-2700. , -3500. ],
955
+ [-2798.19, -3612.24],
956
+ [-2884.3 , -3710.74]])
957
+
958
+ The NPV calculation also supports `decimal.Decimal` types, for example
959
+ if using Decimal ``rates``:
960
+
961
+ >>> rates = [Decimal("0.00"), Decimal("0.05"), Decimal("0.10")]
962
+ >>> cashflows = [[-4_000, 500, 800], [-5_000, 600, 900]]
963
+ >>> npf.npv(rates, cashflows)
964
+ array([[Decimal('-2700.0'), Decimal('-3500.0')],
965
+ [Decimal('-2798.185941043083900226757370'),
966
+ Decimal('-3612.244897959183673469387756')],
967
+ [Decimal('-2884.297520661157024793388430'),
968
+ Decimal('-3710.743801652892561983471074')]], dtype=object)
969
+
970
+ This also works for Decimal cashflows.
971
+
894
972
"""
973
+ rates = np .atleast_1d (rate )
895
974
values = np .atleast_2d (values )
896
- timestep_array = np .arange (0 , values .shape [1 ])
897
- npv = (values / (1 + rate ) ** timestep_array ).sum (axis = 1 )
898
- try :
899
- # If size of array is one, return scalar
900
- return npv .item ()
901
- except ValueError :
902
- # Otherwise, return entire array
903
- return npv
975
+
976
+ if rates .ndim != 1 :
977
+ msg = "invalid shape for rates. Rate must be either a scalar or 1d array"
978
+ raise ValueError (msg )
979
+
980
+ if values .ndim != 2 :
981
+ msg = "invalid shape for values. Values must be either a 1d or 2d array"
982
+ raise ValueError (msg )
983
+
984
+ dtype = Decimal if _use_decimal_dtype (rates , values ) else np .float64
985
+
986
+ if dtype == Decimal :
987
+ rates = _to_decimal_array_1d (rates )
988
+ values = _to_decimal_array_2d (values )
989
+
990
+ shape = _get_output_array_shape (rates , values )
991
+ out = np .empty (shape = shape , dtype = dtype )
992
+
993
+ if dtype == Decimal :
994
+ _npv_decimal (rates , values , out )
995
+ else :
996
+ _npv_native (rates , values , out )
997
+
998
+ return _return_ufunc_like (out )
904
999
905
1000
906
1001
def mirr (values , finance_rate , reinvest_rate , * , raise_exceptions = False ):
0 commit comments