1313
1414from decimal import Decimal
1515
16+ import numba as nb
1617import numpy as np
1718
1819__all__ = ['fv' , 'pmt' , 'nper' , 'ipmt' , 'ppmt' , 'pv' , 'rate' ,
@@ -46,6 +47,36 @@ def _convert_when(when):
4647 return [_when_to_num [x ] for x in when ]
4748
4849
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+
4980def fv (rate , nper , pmt , pv , when = 'end' ):
5081 """Compute the future value.
5182
@@ -825,14 +856,35 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
825856 return np .nan
826857
827858
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+
828880def npv (rate , values ):
829881 r"""Return the NPV (Net Present Value) of a cash flow series.
830882
831883 Parameters
832884 ----------
833- rate : scalar
885+ rate : scalar or array_like shape(K, )
834886 The discount rate.
835- values : array_like, shape(M, )
887+ values : array_like, shape(M, ) or shape(M, N)
836888 The values of the time series of cash flows. The (fixed) time
837889 interval between cash flow "events" must be the same as that for
838890 which `rate` is given (i.e., if `rate` is per year, then precisely
@@ -843,9 +895,10 @@ def npv(rate, values):
843895
844896 Returns
845897 -------
846- out : float
898+ out : float or array shape(K, M)
847899 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.
849902
850903 Warnings
851904 --------
@@ -891,16 +944,58 @@ def npv(rate, values):
891944 >>> np.round(npf.npv(rate, cashflows) + initial_cashflow, 5)
892945 3065.22267
893946
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+
894972 """
973+ rates = np .atleast_1d (rate )
895974 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 )
904999
9051000
9061001def mirr (values , finance_rate , reinvest_rate , * , raise_exceptions = False ):
0 commit comments